import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:gobang/ai/ai.dart'; import 'package:gobang/bridge/circle_shape.dart'; import 'package:gobang/constants.dart'; import 'package:gobang/factory/black_theme_factory.dart'; import 'package:gobang/factory/blue_theme_factory.dart'; import 'package:gobang/factory/theme_factory.dart'; import 'package:gobang/flyweight/chess.dart'; import 'package:gobang/flyweight/position.dart'; import 'package:gobang/memorandum/checkerboard.dart'; import 'package:gobang/utils/tips_dialog.dart'; import 'package:gobang/viewModel/game_view_model.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @override State createState() => HomePageState(); } class HomePageState extends State { late ThemeFactory _themeFactory; final GameViewModel _viewModel = GameViewModel.getInstance(); final Checkerboard _board = Checkerboard.getInstance(); final Ai _ai = Ai.getInstance(); static const _lightOn = Icon(Icons.lightbulb, color: Colors.amberAccent); static const _lightOff = Icon(Icons.lightbulb_outline_rounded); static const _circleIcon = Icon(Icons.circle_outlined); static const _rectIcon = Icon(Icons.crop_square); Icon _currentLight = _lightOn; Icon _currentShape = _circleIcon; bool _useCircle = true; bool _gameOver = false; int _paintVersion = 0; double get _boardWidth => MediaQuery.of(context).size.width * 0.8; double get _cellSize => _boardWidth / (kBoardSize - 1); @override void initState() { super.initState(); _themeFactory = BlueThemeFactory(); _ai.init(); } @override Widget build(BuildContext context) { final themeColor = _themeFactory.getTheme().getThemeColor(); return Scaffold( appBar: AppBar( elevation: 0, backgroundColor: themeColor, title: const Text('南瓜五子棋'), actions: [ IconButton( onPressed: _toggleTheme, icon: _currentLight, ), IconButton( onPressed: _toggleShape, icon: _currentShape, ), ], ), body: Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [themeColor, Colors.white], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.only(top: 14, bottom: 30), child: Text( _viewModel.state, style: const TextStyle(color: Colors.white), ), ), GestureDetector( onTapDown: _onBoardTap, child: SizedBox( width: _boardWidth, height: _boardWidth, child: CustomPaint( size: Size.square(_boardWidth), painter: BoardPainter( pieces: List.unmodifiable(_board.state), version: _paintVersion, ), ), ), ), Padding( padding: const EdgeInsets.only(top: 16), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( onPressed: _onUndo, icon: const Icon(Icons.undo), ), IconButton( onPressed: _onSurrender, icon: const Icon( Icons.sports_handball, color: Colors.deepPurple, ), ), IconButton( onPressed: _onRestart, icon: const Icon( Icons.restart_alt, color: Colors.indigo, ), ), ], ), ), ], ), ), ), ); } void _toggleTheme() { setState(() { if (_themeFactory is BlackThemeFactory) { _currentLight = _lightOn; _themeFactory = BlueThemeFactory(); } else { _currentLight = _lightOff; _themeFactory = BlackThemeFactory(); } }); } void _toggleShape() { setState(() { _useCircle = !_useCircle; _currentShape = _useCircle ? _circleIcon : _rectIcon; }); } void _onBoardTap(TapDownDetails details) { if (_gameOver) return; final snap = _snapToGrid(details.localPosition); if (snap == null) return; final (gridX, gridY, px, py) = snap; if (!_ai.isLegal(gridX, gridY)) return; final chess = _viewModel.play(); final shape = _viewModel.shapeFor(_useCircle); final position = Position(px, py, chess, chessShape: shape); _board.add(position); _ai.addChessman(gridX, gridY, 1); _paintVersion++; if (_ai.isWin(gridX, gridY, 1)) { setState(() => _gameOver = true); TipsDialog.show(context, '恭喜', '您打败了决策树算法'); return; } _playAi(); setState(() {}); } /// 将触摸点吸附到最近交叉点;过远则返回 null (int, int, double, double)? _snapToGrid(Offset local) { final cell = _cellSize; final gx = (local.dx / cell).round().clamp(0, kBoardSize - 1); final gy = (local.dy / cell).round().clamp(0, kBoardSize - 1); final px = gx * cell; final py = gy * cell; final dist = sqrt(pow(local.dx - px, 2) + pow(local.dy - py, 2)); if (dist > cell / 2 - 2) return null; return (gx, gy, px, py); } void _playAi() { final raw = _ai.searchPosition(); if (raw.dx < 0 || raw.dy < 0) return; final gridX = raw.dx.toInt(); final gridY = raw.dy.toInt(); final cell = _cellSize; final position = Position( gridX * cell, gridY * cell, raw.chess, chessShape: CircleShape(), ); _board.add(position); _ai.addChessman(gridX, gridY, -1); _paintVersion++; if (_ai.isWin(gridX, gridY, -1)) { _gameOver = true; TipsDialog.show(context, '很遗憾', '决策树算法打败了您'); } } void _rebuildAiFromBoard() { _ai.init(); final cell = _cellSize; for (final po in _board.state) { final gx = (po.dx / cell).round(); final gy = (po.dy / cell).round(); _ai.addChessman(gx, gy, po.chess is WhiteChess ? 1 : -1); } } void _onUndo() { if (_gameOver) { TipsDialog.show(context, '提示', '对局已结束,请重新开局'); return; } if (!_viewModel.undo()) { TipsDialog.show(context, '提示', '现阶段不能悔棋'); return; } _board.undo(); _rebuildAiFromBoard(); setState(() => _paintVersion++); } Future _onSurrender() async { if (!_viewModel.surrender()) { await TipsDialog.show(context, '提示', '现阶段不能投降'); return; } final ok = await TipsDialog.showByChoose( context, '提示', '是否要投降并重新开局?', '是', '否', ); if (ok == true && mounted) { _resetGame(); } } Future _onRestart() async { final ok = await TipsDialog.showByChoose( context, '提示', '是否重新开局?', '是', '否', ); if (ok == true && mounted) { _resetGame(); } } void _resetGame() { setState(() { _board.clean(); _viewModel.reset(); _ai.init(); _gameOver = false; _paintVersion++; }); } } /// 棋盘 + 棋子同层绘制,避免双重 CustomPaint 与 paint 内副作用 class BoardPainter extends CustomPainter { BoardPainter({required this.pieces, required this.version}); final List pieces; final int version; @override void paint(Canvas canvas, Size size) { final cell = size.width / (kBoardSize - 1); final bg = Paint() ..isAntiAlias = true ..style = PaintingStyle.fill ..color = const Color(0x77cdb175); canvas.drawRect(Offset.zero & size, bg); final line = Paint() ..style = PaintingStyle.stroke ..color = CupertinoColors.systemGrey6 ..strokeWidth = 1; for (var i = 0; i < kBoardSize; i++) { final o = cell * i; canvas.drawLine(Offset(0, o), Offset(size.width, o), line); canvas.drawLine(Offset(o, 0), Offset(o, size.height), line); } final radius = cell / 2 - 2; final fill = Paint()..style = PaintingStyle.fill; for (final p in pieces) { fill.color = p.chess.color; final center = Offset(p.dx, p.dy); if (p.chessShape.shape == 1) { canvas.drawCircle(center, radius, fill); } else { canvas.drawRect( Rect.fromCircle(center: center, radius: radius), fill, ); } } } @override bool shouldRepaint(covariant BoardPainter oldDelegate) { return oldDelegate.version != version; } }