import 'dart:math'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:gobang/models/chess.dart'; import 'package:gobang/models/checkerboard.dart'; import 'package:gobang/models/chess_shape.dart'; import 'package:gobang/models/circle_shape.dart'; import 'package:gobang/models/game_state.dart'; import 'package:gobang/models/position.dart'; import 'package:gobang/models/rect_shape.dart'; import 'package:gobang/models/user_context.dart'; import 'package:gobang/services/ai_service.dart'; import 'package:gobang/services/chess_flyweight_factory.dart'; import 'package:gobang/themes/black_theme_factory.dart'; import 'package:gobang/themes/blue_theme_factory.dart'; import 'package:gobang/themes/theme_factory.dart'; import 'package:gobang/utils/constants.dart'; import 'package:gobang/utils/tips_dialog.dart'; /// 对局控制器:棋盘交互、AI、主题与阶段状态 class GameController extends GetxController { final UserContext _userContext = UserContext(); final Checkerboard board = Checkerboard.getInstance(); final AiService ai = AiService.getInstance(); final themeFactory = Rx(BlueThemeFactory()); final useCircle = true.obs; final gameOver = false.obs; final paintVersion = 0.obs; final statusText = '热身阶段 · 不能悔棋 · 不能投降'.obs; static const lightOn = Icon(Icons.lightbulb, color: Color(0xFFFFC857)); static const lightOff = Icon( Icons.lightbulb_outline_rounded, color: Color(0xFFFFF8F0), ); static const circleIcon = Icon( Icons.circle_outlined, color: Color(0xFFFFF8F0), ); static const rectIcon = Icon(Icons.crop_square, color: Color(0xFFFFF8F0)); Icon get currentLight => themeFactory.value is BlackThemeFactory ? lightOff : lightOn; Icon get currentShape => useCircle.value ? circleIcon : rectIcon; Color get themeColor => themeFactory.value.getTheme().getThemeColor(); @override void onInit() { super.onInit(); ai.init(); _refreshStatus(); } void toggleTheme() { themeFactory.value = themeFactory.value is BlackThemeFactory ? BlueThemeFactory() : BlackThemeFactory(); } void toggleShape() => useCircle.toggle(); ChessShape _shapeFor(bool circle) => circle ? CircleShape() : RectShape(); void _refreshStatus() { final s = _userContext.state; if (s is StartState) { statusText.value = '热身阶段 · 不能悔棋 · 不能投降'; } else if (s is MidState) { statusText.value = '入神阶段 · 可悔棋 ${3 - s.reg} 次 · 可投降'; } else if (s is EndState) { statusText.value = '白热化阶段 · 悔棋已用完 · 可投降'; } } void onBoardTap(Offset local, double boardWidth) { if (gameOver.value) return; final cell = boardWidth / (kBoardSize - 1); final snap = _snapToGrid(local, cell); if (snap == null) return; final (gridX, gridY, px, py) = snap; if (!ai.isLegal(gridX, gridY)) return; _userContext.play(); final chess = ChessFlyweightFactory.getInstance().getChess('white'); board.add(Position( px, py, chess, gridX: gridX, gridY: gridY, chessShape: _shapeFor(useCircle.value), )); ai.addChessman(gridX, gridY, 1); paintVersion.value++; _refreshStatus(); if (ai.isWin(gridX, gridY, 1)) { gameOver.value = true; TipsDialog.show(Get.context!, '恭喜', '您打败了决策树算法').then((_) { resetGame(); }); return; } _playAi(cell); } (int, int, double, double)? _snapToGrid(Offset local, double cell) { 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(double cell) { final raw = ai.searchPosition(); if (raw.dx < 0 || raw.dy < 0) return; final gridX = raw.dx.toInt(); final gridY = raw.dy.toInt(); board.add(Position( gridX * cell, gridY * cell, raw.chess, gridX: gridX, gridY: gridY, chessShape: CircleShape(), )); ai.addChessman(gridX, gridY, -1); paintVersion.value++; if (ai.isWin(gridX, gridY, -1)) { gameOver.value = true; TipsDialog.show(Get.context!, '很遗憾', '决策树算法打败了您').then((_) { resetGame(); }); } } void undo() { if (gameOver.value) { TipsDialog.show(Get.context!, '提示', '对局已结束,请重新开局'); return; } if (!_userContext.regretChess()) { TipsDialog.show(Get.context!, '提示', '现阶段不能悔棋'); return; } board.undo(); _rebuildAiFromBoard(); paintVersion.value++; _refreshStatus(); } void _rebuildAiFromBoard() { ai.init(); for (final po in board.state) { ai.addChessman( po.gridX, po.gridY, po.chess is WhiteChess ? 1 : -1, ); } } Future surrender() async { if (!_userContext.surrender()) { await TipsDialog.show(Get.context!, '提示', '现阶段不能投降'); return; } final ok = await TipsDialog.showByChoose( Get.context!, '确认投降', '投降后将重新开局', '投降', '否', ); if (ok == true) resetGame(); } Future restart() async { final ok = await TipsDialog.showByChoose( Get.context!, '提示', '是否重新开局?', '是', '否', ); if (ok == true) resetGame(); } void resetGame() { board.clean(); _userContext.reset(); ai.init(); gameOver.value = false; paintVersion.value++; _refreshStatus(); } }