| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- 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<HomePage> createState() => HomePageState();
- }
- class HomePageState extends State<HomePage> {
- 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<void> _onSurrender() async {
- if (!_viewModel.surrender()) {
- await TipsDialog.show(context, '提示', '现阶段不能投降');
- return;
- }
- final ok = await TipsDialog.showByChoose(
- context,
- '提示',
- '是否要投降并重新开局?',
- '是',
- '否',
- );
- if (ok == true && mounted) {
- _resetGame();
- }
- }
- Future<void> _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<Position> 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;
- }
- }
|