game_controller.dart 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import 'dart:math';
  2. import 'package:flutter/material.dart';
  3. import 'package:get/get.dart';
  4. import 'package:gobang/models/chess.dart';
  5. import 'package:gobang/models/checkerboard.dart';
  6. import 'package:gobang/models/chess_shape.dart';
  7. import 'package:gobang/models/circle_shape.dart';
  8. import 'package:gobang/models/game_state.dart';
  9. import 'package:gobang/models/position.dart';
  10. import 'package:gobang/models/rect_shape.dart';
  11. import 'package:gobang/models/user_context.dart';
  12. import 'package:gobang/services/ai_service.dart';
  13. import 'package:gobang/services/chess_flyweight_factory.dart';
  14. import 'package:gobang/themes/black_theme_factory.dart';
  15. import 'package:gobang/themes/blue_theme_factory.dart';
  16. import 'package:gobang/themes/theme_factory.dart';
  17. import 'package:gobang/utils/constants.dart';
  18. import 'package:gobang/utils/tips_dialog.dart';
  19. /// 对局控制器:棋盘交互、AI、主题与阶段状态
  20. class GameController extends GetxController {
  21. final UserContext _userContext = UserContext();
  22. final Checkerboard board = Checkerboard.getInstance();
  23. final AiService ai = AiService.getInstance();
  24. final themeFactory = Rx<ThemeFactory>(BlueThemeFactory());
  25. final useCircle = true.obs;
  26. final gameOver = false.obs;
  27. final paintVersion = 0.obs;
  28. final statusText = '热身阶段 · 不能悔棋 · 不能投降'.obs;
  29. static const lightOn = Icon(Icons.lightbulb, color: Color(0xFFFFC857));
  30. static const lightOff = Icon(
  31. Icons.lightbulb_outline_rounded,
  32. color: Color(0xFFFFF8F0),
  33. );
  34. static const circleIcon = Icon(
  35. Icons.circle_outlined,
  36. color: Color(0xFFFFF8F0),
  37. );
  38. static const rectIcon = Icon(Icons.crop_square, color: Color(0xFFFFF8F0));
  39. Icon get currentLight =>
  40. themeFactory.value is BlackThemeFactory ? lightOff : lightOn;
  41. Icon get currentShape => useCircle.value ? circleIcon : rectIcon;
  42. Color get themeColor => themeFactory.value.getTheme().getThemeColor();
  43. @override
  44. void onInit() {
  45. super.onInit();
  46. ai.init();
  47. _refreshStatus();
  48. }
  49. void toggleTheme() {
  50. themeFactory.value = themeFactory.value is BlackThemeFactory
  51. ? BlueThemeFactory()
  52. : BlackThemeFactory();
  53. }
  54. void toggleShape() => useCircle.toggle();
  55. ChessShape _shapeFor(bool circle) =>
  56. circle ? CircleShape() : RectShape();
  57. void _refreshStatus() {
  58. final s = _userContext.state;
  59. if (s is StartState) {
  60. statusText.value = '热身阶段 · 不能悔棋 · 不能投降';
  61. } else if (s is MidState) {
  62. statusText.value = '入神阶段 · 可悔棋 ${3 - s.reg} 次 · 可投降';
  63. } else if (s is EndState) {
  64. statusText.value = '白热化阶段 · 悔棋已用完 · 可投降';
  65. }
  66. }
  67. void onBoardTap(Offset local, double boardWidth) {
  68. if (gameOver.value) return;
  69. final cell = boardWidth / (kBoardSize - 1);
  70. final snap = _snapToGrid(local, cell);
  71. if (snap == null) return;
  72. final (gridX, gridY, px, py) = snap;
  73. if (!ai.isLegal(gridX, gridY)) return;
  74. _userContext.play();
  75. final chess = ChessFlyweightFactory.getInstance().getChess('white');
  76. board.add(Position(
  77. px,
  78. py,
  79. chess,
  80. gridX: gridX,
  81. gridY: gridY,
  82. chessShape: _shapeFor(useCircle.value),
  83. ));
  84. ai.addChessman(gridX, gridY, 1);
  85. paintVersion.value++;
  86. _refreshStatus();
  87. if (ai.isWin(gridX, gridY, 1)) {
  88. gameOver.value = true;
  89. TipsDialog.show(Get.context!, '恭喜', '您打败了决策树算法').then((_) {
  90. resetGame();
  91. });
  92. return;
  93. }
  94. _playAi(cell);
  95. }
  96. (int, int, double, double)? _snapToGrid(Offset local, double cell) {
  97. final gx = (local.dx / cell).round().clamp(0, kBoardSize - 1);
  98. final gy = (local.dy / cell).round().clamp(0, kBoardSize - 1);
  99. final px = gx * cell;
  100. final py = gy * cell;
  101. final dist = sqrt(pow(local.dx - px, 2) + pow(local.dy - py, 2));
  102. if (dist > cell / 2 - 2) return null;
  103. return (gx, gy, px, py);
  104. }
  105. void _playAi(double cell) {
  106. final raw = ai.searchPosition();
  107. if (raw.dx < 0 || raw.dy < 0) return;
  108. final gridX = raw.dx.toInt();
  109. final gridY = raw.dy.toInt();
  110. board.add(Position(
  111. gridX * cell,
  112. gridY * cell,
  113. raw.chess,
  114. gridX: gridX,
  115. gridY: gridY,
  116. chessShape: CircleShape(),
  117. ));
  118. ai.addChessman(gridX, gridY, -1);
  119. paintVersion.value++;
  120. if (ai.isWin(gridX, gridY, -1)) {
  121. gameOver.value = true;
  122. TipsDialog.show(Get.context!, '很遗憾', '决策树算法打败了您').then((_) {
  123. resetGame();
  124. });
  125. }
  126. }
  127. void undo() {
  128. if (gameOver.value) {
  129. TipsDialog.show(Get.context!, '提示', '对局已结束,请重新开局');
  130. return;
  131. }
  132. if (!_userContext.regretChess()) {
  133. TipsDialog.show(Get.context!, '提示', '现阶段不能悔棋');
  134. return;
  135. }
  136. board.undo();
  137. _rebuildAiFromBoard();
  138. paintVersion.value++;
  139. _refreshStatus();
  140. }
  141. void _rebuildAiFromBoard() {
  142. ai.init();
  143. for (final po in board.state) {
  144. ai.addChessman(
  145. po.gridX,
  146. po.gridY,
  147. po.chess is WhiteChess ? 1 : -1,
  148. );
  149. }
  150. }
  151. Future<void> surrender() async {
  152. if (!_userContext.surrender()) {
  153. await TipsDialog.show(Get.context!, '提示', '现阶段不能投降');
  154. return;
  155. }
  156. final ok = await TipsDialog.showByChoose(
  157. Get.context!,
  158. '确认投降',
  159. '投降后将重新开局',
  160. '投降',
  161. '否',
  162. );
  163. if (ok == true) resetGame();
  164. }
  165. Future<void> restart() async {
  166. final ok = await TipsDialog.showByChoose(
  167. Get.context!,
  168. '提示',
  169. '是否重新开局?',
  170. '是',
  171. '否',
  172. );
  173. if (ok == true) resetGame();
  174. }
  175. void resetGame() {
  176. board.clean();
  177. _userContext.reset();
  178. ai.init();
  179. gameOver.value = false;
  180. paintVersion.value++;
  181. _refreshStatus();
  182. }
  183. }