home_page.dart 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import 'dart:math';
  2. import 'package:flutter/cupertino.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:gobang/ai/ai.dart';
  5. import 'package:gobang/bridge/circle_shape.dart';
  6. import 'package:gobang/constants.dart';
  7. import 'package:gobang/factory/black_theme_factory.dart';
  8. import 'package:gobang/factory/blue_theme_factory.dart';
  9. import 'package:gobang/factory/theme_factory.dart';
  10. import 'package:gobang/flyweight/chess.dart';
  11. import 'package:gobang/flyweight/position.dart';
  12. import 'package:gobang/memorandum/checkerboard.dart';
  13. import 'package:gobang/utils/tips_dialog.dart';
  14. import 'package:gobang/viewModel/game_view_model.dart';
  15. class HomePage extends StatefulWidget {
  16. const HomePage({super.key});
  17. @override
  18. State<HomePage> createState() => HomePageState();
  19. }
  20. class HomePageState extends State<HomePage> {
  21. late ThemeFactory _themeFactory;
  22. final GameViewModel _viewModel = GameViewModel.getInstance();
  23. final Checkerboard _board = Checkerboard.getInstance();
  24. final Ai _ai = Ai.getInstance();
  25. static const _lightOn = Icon(Icons.lightbulb, color: Colors.amberAccent);
  26. static const _lightOff = Icon(Icons.lightbulb_outline_rounded);
  27. static const _circleIcon = Icon(Icons.circle_outlined);
  28. static const _rectIcon = Icon(Icons.crop_square);
  29. Icon _currentLight = _lightOn;
  30. Icon _currentShape = _circleIcon;
  31. bool _useCircle = true;
  32. bool _gameOver = false;
  33. int _paintVersion = 0;
  34. double get _boardWidth => MediaQuery.of(context).size.width * 0.8;
  35. double get _cellSize => _boardWidth / (kBoardSize - 1);
  36. @override
  37. void initState() {
  38. super.initState();
  39. _themeFactory = BlueThemeFactory();
  40. _ai.init();
  41. }
  42. @override
  43. Widget build(BuildContext context) {
  44. final themeColor = _themeFactory.getTheme().getThemeColor();
  45. return Scaffold(
  46. appBar: AppBar(
  47. elevation: 0,
  48. backgroundColor: themeColor,
  49. title: const Text('南瓜五子棋'),
  50. actions: [
  51. IconButton(
  52. onPressed: _toggleTheme,
  53. icon: _currentLight,
  54. ),
  55. IconButton(
  56. onPressed: _toggleShape,
  57. icon: _currentShape,
  58. ),
  59. ],
  60. ),
  61. body: Container(
  62. decoration: BoxDecoration(
  63. gradient: LinearGradient(
  64. colors: [themeColor, Colors.white],
  65. begin: Alignment.topCenter,
  66. end: Alignment.bottomCenter,
  67. ),
  68. ),
  69. child: Center(
  70. child: Column(
  71. mainAxisAlignment: MainAxisAlignment.center,
  72. children: [
  73. Padding(
  74. padding: const EdgeInsets.only(top: 14, bottom: 30),
  75. child: Text(
  76. _viewModel.state,
  77. style: const TextStyle(color: Colors.white),
  78. ),
  79. ),
  80. GestureDetector(
  81. onTapDown: _onBoardTap,
  82. child: SizedBox(
  83. width: _boardWidth,
  84. height: _boardWidth,
  85. child: CustomPaint(
  86. size: Size.square(_boardWidth),
  87. painter: BoardPainter(
  88. pieces: List.unmodifiable(_board.state),
  89. version: _paintVersion,
  90. ),
  91. ),
  92. ),
  93. ),
  94. Padding(
  95. padding: const EdgeInsets.only(top: 16),
  96. child: Row(
  97. mainAxisAlignment: MainAxisAlignment.center,
  98. children: [
  99. IconButton(
  100. onPressed: _onUndo,
  101. icon: const Icon(Icons.undo),
  102. ),
  103. IconButton(
  104. onPressed: _onSurrender,
  105. icon: const Icon(
  106. Icons.sports_handball,
  107. color: Colors.deepPurple,
  108. ),
  109. ),
  110. IconButton(
  111. onPressed: _onRestart,
  112. icon: const Icon(
  113. Icons.restart_alt,
  114. color: Colors.indigo,
  115. ),
  116. ),
  117. ],
  118. ),
  119. ),
  120. ],
  121. ),
  122. ),
  123. ),
  124. );
  125. }
  126. void _toggleTheme() {
  127. setState(() {
  128. if (_themeFactory is BlackThemeFactory) {
  129. _currentLight = _lightOn;
  130. _themeFactory = BlueThemeFactory();
  131. } else {
  132. _currentLight = _lightOff;
  133. _themeFactory = BlackThemeFactory();
  134. }
  135. });
  136. }
  137. void _toggleShape() {
  138. setState(() {
  139. _useCircle = !_useCircle;
  140. _currentShape = _useCircle ? _circleIcon : _rectIcon;
  141. });
  142. }
  143. void _onBoardTap(TapDownDetails details) {
  144. if (_gameOver) return;
  145. final snap = _snapToGrid(details.localPosition);
  146. if (snap == null) return;
  147. final (gridX, gridY, px, py) = snap;
  148. if (!_ai.isLegal(gridX, gridY)) return;
  149. final chess = _viewModel.play();
  150. final shape = _viewModel.shapeFor(_useCircle);
  151. final position = Position(px, py, chess, chessShape: shape);
  152. _board.add(position);
  153. _ai.addChessman(gridX, gridY, 1);
  154. _paintVersion++;
  155. if (_ai.isWin(gridX, gridY, 1)) {
  156. setState(() => _gameOver = true);
  157. TipsDialog.show(context, '恭喜', '您打败了决策树算法');
  158. return;
  159. }
  160. _playAi();
  161. setState(() {});
  162. }
  163. /// 将触摸点吸附到最近交叉点;过远则返回 null
  164. (int, int, double, double)? _snapToGrid(Offset local) {
  165. final cell = _cellSize;
  166. final gx = (local.dx / cell).round().clamp(0, kBoardSize - 1);
  167. final gy = (local.dy / cell).round().clamp(0, kBoardSize - 1);
  168. final px = gx * cell;
  169. final py = gy * cell;
  170. final dist = sqrt(pow(local.dx - px, 2) + pow(local.dy - py, 2));
  171. if (dist > cell / 2 - 2) return null;
  172. return (gx, gy, px, py);
  173. }
  174. void _playAi() {
  175. final raw = _ai.searchPosition();
  176. if (raw.dx < 0 || raw.dy < 0) return;
  177. final gridX = raw.dx.toInt();
  178. final gridY = raw.dy.toInt();
  179. final cell = _cellSize;
  180. final position = Position(
  181. gridX * cell,
  182. gridY * cell,
  183. raw.chess,
  184. chessShape: CircleShape(),
  185. );
  186. _board.add(position);
  187. _ai.addChessman(gridX, gridY, -1);
  188. _paintVersion++;
  189. if (_ai.isWin(gridX, gridY, -1)) {
  190. _gameOver = true;
  191. TipsDialog.show(context, '很遗憾', '决策树算法打败了您');
  192. }
  193. }
  194. void _rebuildAiFromBoard() {
  195. _ai.init();
  196. final cell = _cellSize;
  197. for (final po in _board.state) {
  198. final gx = (po.dx / cell).round();
  199. final gy = (po.dy / cell).round();
  200. _ai.addChessman(gx, gy, po.chess is WhiteChess ? 1 : -1);
  201. }
  202. }
  203. void _onUndo() {
  204. if (_gameOver) {
  205. TipsDialog.show(context, '提示', '对局已结束,请重新开局');
  206. return;
  207. }
  208. if (!_viewModel.undo()) {
  209. TipsDialog.show(context, '提示', '现阶段不能悔棋');
  210. return;
  211. }
  212. _board.undo();
  213. _rebuildAiFromBoard();
  214. setState(() => _paintVersion++);
  215. }
  216. Future<void> _onSurrender() async {
  217. if (!_viewModel.surrender()) {
  218. await TipsDialog.show(context, '提示', '现阶段不能投降');
  219. return;
  220. }
  221. final ok = await TipsDialog.showByChoose(
  222. context,
  223. '提示',
  224. '是否要投降并重新开局?',
  225. '是',
  226. '否',
  227. );
  228. if (ok == true && mounted) {
  229. _resetGame();
  230. }
  231. }
  232. Future<void> _onRestart() async {
  233. final ok = await TipsDialog.showByChoose(
  234. context,
  235. '提示',
  236. '是否重新开局?',
  237. '是',
  238. '否',
  239. );
  240. if (ok == true && mounted) {
  241. _resetGame();
  242. }
  243. }
  244. void _resetGame() {
  245. setState(() {
  246. _board.clean();
  247. _viewModel.reset();
  248. _ai.init();
  249. _gameOver = false;
  250. _paintVersion++;
  251. });
  252. }
  253. }
  254. /// 棋盘 + 棋子同层绘制,避免双重 CustomPaint 与 paint 内副作用
  255. class BoardPainter extends CustomPainter {
  256. BoardPainter({required this.pieces, required this.version});
  257. final List<Position> pieces;
  258. final int version;
  259. @override
  260. void paint(Canvas canvas, Size size) {
  261. final cell = size.width / (kBoardSize - 1);
  262. final bg = Paint()
  263. ..isAntiAlias = true
  264. ..style = PaintingStyle.fill
  265. ..color = const Color(0x77cdb175);
  266. canvas.drawRect(Offset.zero & size, bg);
  267. final line = Paint()
  268. ..style = PaintingStyle.stroke
  269. ..color = CupertinoColors.systemGrey6
  270. ..strokeWidth = 1;
  271. for (var i = 0; i < kBoardSize; i++) {
  272. final o = cell * i;
  273. canvas.drawLine(Offset(0, o), Offset(size.width, o), line);
  274. canvas.drawLine(Offset(o, 0), Offset(o, size.height), line);
  275. }
  276. final radius = cell / 2 - 2;
  277. final fill = Paint()..style = PaintingStyle.fill;
  278. for (final p in pieces) {
  279. fill.color = p.chess.color;
  280. final center = Offset(p.dx, p.dy);
  281. if (p.chessShape.shape == 1) {
  282. canvas.drawCircle(center, radius, fill);
  283. } else {
  284. canvas.drawRect(
  285. Rect.fromCircle(center: center, radius: radius),
  286. fill,
  287. );
  288. }
  289. }
  290. }
  291. @override
  292. bool shouldRepaint(covariant BoardPainter oldDelegate) {
  293. return oldDelegate.version != version;
  294. }
  295. }