|
|
@@ -1,331 +0,0 @@
|
|
|
-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;
|
|
|
- }
|
|
|
-}
|