
个人主页:ujainu
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
文章目录
-
- [引言:从娱乐到竞技 ------ 五子棋的专业化之路](#引言:从娱乐到竞技 —— 五子棋的专业化之路)
- [一、为什么需要禁手?------ 五子棋的竞技进化史](#一、为什么需要禁手?—— 五子棋的竞技进化史)
-
- [1. 先手优势的数学证明](#1. 先手优势的数学证明)
- [2. RIF 规则的核心:禁手体系](#2. RIF 规则的核心:禁手体系)
- [3. 禁手判负机制](#3. 禁手判负机制)
- 二、核心技术一:禁手检测算法设计
-
- [1. 整体检测流程](#1. 整体检测流程)
- [2. 长连禁手:最简单的判定](#2. 长连禁手:最简单的判定)
- [3. 四四禁手:统计"四"的数量](#3. 四四禁手:统计“四”的数量)
- [4. 三三禁手:识别"活三"](#4. 三三禁手:识别“活三”)
- 三、核心技术二:禁手综合判定函数
-
- [边界处理:_extractLine 函数](#边界处理:_extractLine 函数)
- [四、核心技术三:UI 反馈与禁手可视化](#四、核心技术三:UI 反馈与禁手可视化)
-
- [1. 红色闪烁棋子](#1. 红色闪烁棋子)
- [2. 禁手提示弹窗](#2. 禁手提示弹窗)
- [3. 高亮禁手位置](#3. 高亮禁手位置)
- [五、核心技术四:游戏模式扩展 ------ 禁手开关](#五、核心技术四:游戏模式扩展 —— 禁手开关)
-
- [1. 新增设置项](#1. 新增设置项)
- [2. 状态传递](#2. 状态传递)
- [3. 落子逻辑整合](#3. 落子逻辑整合)
- [六、AI 适配:让 AI 遵守禁手规则](#六、AI 适配:让 AI 遵守禁手规则)
-
- [1. AI 落子前预检](#1. AI 落子前预检)
- 七、完整可运行代码
- 运行界面
- 结语:专业规则,成就竞技之美
引言:从娱乐到竞技 ------ 五子棋的专业化之路
在前两篇中,我们成功构建了一款兼具东方美学与智能交互的五子棋应用:第一篇实现了双人对战与青绿山水界面,第二篇引入了基于评分策略的 AI 对手。然而,若要真正迈向专业级五子棋体验 ,我们必须面对一个核心命题:禁手规则。
五子棋虽看似简单,但其竞技版本(即"连珠")自 20 世纪 90 年代起便采用 RIF(Renju International Federation)规则 ,其中最关键的一条便是:黑棋先行优势过大,故施加禁手限制。这一规则不仅平衡了先手优势,更将五子棋从"谁先连五谁赢"的简单游戏,升华为一门讲究布局、陷阱与反制的艺术。
本文将深入剖析 黑棋三大禁手 (三三、四四、长连)的判定逻辑,并在 Flutter + OpenHarmony 环境中实现一套可开关的专业规则系统。玩家可在游戏开始前选择是否启用禁手,满足从休闲娱乐到竞技训练的多元需求。
我们将重点解决以下技术挑战:
- 如何精准识别"活三"与"冲四"?
- 如何判断"同时形成两个以上"有效棋型?
- 如何在 UI 上给予清晰、震撼的禁手反馈?
- 如何无缝集成到现有游戏架构中,不破坏原有逻辑?
一、为什么需要禁手?------ 五子棋的竞技进化史
1. 先手优势的数学证明
研究表明,在无禁手规则下,黑棋存在必胜策略。1993 年,Victor Allis 通过计算机证明:在 15×15 棋盘上,黑棋可通过特定开局(如"花月"、"浦月")强制获胜。这意味着,若无限制,白棋从第一步起就处于绝对劣势。
2. RIF 规则的核心:禁手体系
为平衡先手优势,RIF 制定了三大禁手(仅对黑棋生效):
| 禁手类型 | 定义 | 示例 |
|---|---|---|
| 三三禁手 | 一步棋同时形成 两个或以上活三 | .BB.B. + .B.BB. 同时成立 |
| 四四禁手 | 一步棋同时形成 两个或以上四(活四或冲四) | WBBBB. + .BBBBW 同时成立 |
| 长连禁手 | 形成 六子或以上连线 | BBBBBB |
✅ 关键说明:
- "活三" = 两端皆空的三连(如
.BBB.)- "四" = 包括活四(
.BBBB.)和冲四(WBBBB.或.BBBBW)- 白棋无禁手,可自由形成任意棋型
3. 禁手判负机制
一旦黑棋落子触发任一禁手,立即判白方胜,无需等待五子连珠。这是对"不公平优势"的直接惩罚。
二、核心技术一:禁手检测算法设计
1. 整体检测流程
黑棋落子后,执行以下步骤:
dart
if (player == 1 && _forbiddenEnabled) {
if (checkForbiddenMove(row, col)) {
_winner = 2; // 白胜
_forbiddenPosition = Offset(col.toDouble(), row.toDouble());
showForbiddenDialog();
return true;
}
}
2. 长连禁手:最简单的判定
只需检查四个方向是否有 ≥6 连:
dart
bool _checkLongLine(int row, int col) {
for (var dir in directions) {
int count = 1;
// 正向
int r = row + dir[0];
int c = col + dir[1];
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == 1) {
count++;
r += dir[0];
c += dir[1];
}
// 反向
r = row - dir[0];
c = col - dir[1];
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == 1) {
count++;
r -= dir[0];
c -= dir[1];
}
if (count >= 6) return true;
}
return false;
}
3. 四四禁手:统计"四"的数量
关键在于:区分活四与冲四,并计数
dart
int _countFours(int row, int col) {
int fourCount = 0;
for (var dir in directions) {
final line = _extractLine(row, col, dir, 5); // 提取5格
if (_isFour(line, 1)) {
fourCount++;
}
}
return fourCount;
}
bool _isFour(List<int> pattern, int player) {
final opponent = 3 - player; // 1->2, 2->1
if (pattern.length != 5) return false;
final selfCount = pattern.where((p) => p == player).length;
final emptyCount = pattern.where((p) => p == 0).length;
final opponentCount = pattern.where((p) => p == opponent).length;
if (selfCount != 4 || opponentCount > 0) return false;
return emptyCount == 1; // 4子+1空 = 四(活或冲)
}
💡 注意 :
_extractLine需处理边界,将越界位置视为对手子(阻挡)
4. 三三禁手:识别"活三"
难点在于:必须是"活三"(两端皆空)
dart
int _countLiveThrees(int row, int col) {
int liveThreeCount = 0;
for (var dir in directions) {
// 检查以(row,col)为中心的7格(确保能判断两端)
final line = _extractLine(row, col, dir, 7);
if (_isLiveThree(line, 1)) {
liveThreeCount++;
}
}
return liveThreeCount;
}
bool _isLiveThree(List<int> line, int player) {
// 在7格中找连续3个player,且两端为空
for (int i = 0; i <= 4; i++) {
if (line[i] == 0 &&
line[i+1] == player &&
line[i+2] == player &&
line[i+3] == player &&
line[i+4] == 0) {
return true;
}
}
return false;
}
⚠️ 重要优化 :
使用 7 格窗口而非 5 格,才能准确判断"两端是否为空"
三、核心技术二:禁手综合判定函数
整合三大禁手检测:
dart
bool checkForbiddenMove(int row, int col) {
// 1. 长连禁手
if (_checkLongLine(row, col)) {
_forbiddenType = '长连';
return true;
}
// 2. 统计四的数量
final fourCount = _countFours(row, col);
if (fourCount >= 2) {
_forbiddenType = '四四';
return true;
}
// 3. 统计活三的数量
final liveThreeCount = _countLiveThrees(row, col);
if (liveThreeCount >= 2) {
_forbiddenType = '三三';
return true;
}
return false;
}
边界处理:_extractLine 函数
dart
List<int> _extractLine(int row, int col, List<int> dir, int length) {
final half = length ~/ 2;
final line = <int>[];
for (int i = -half; i <= half; i++) {
final r = row + i * dir[0];
final c = col + i * dir[1];
if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE) {
line.add(_board[r][c]);
} else {
line.add(2); // 边界视为白子(阻挡)
}
}
return line;
}
✅ 为何边界视为白子?
因为边界会阻挡棋型延伸,等效于被对手堵住
四、核心技术三:UI 反馈与禁手可视化
1. 红色闪烁棋子
使用 AnimatedContainer 实现颜色脉动:
dart
Widget _buildStone(int player, {bool isForbidden = false}) {
if (isForbidden) {
return AnimatedContainer(
duration: const Duration(milliseconds: 800),
curve: Curves.easeInOut,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.red.withOpacity(0.5),
blurRadius: 12,
spreadRadius: 2,
),
],
),
width: _cellSize * 0.8,
height: _cellSize * 0.8,
);
}
// ... normal stone
}
2. 禁手提示弹窗
dart
void _showForbiddenDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('❌ 黑棋禁手!', style: TextStyle(color: Colors.red)),
content: Text('检测到 $_forbiddenType 禁手,白方获胜!'),
actions: [
Center(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_startNewGame();
},
child: const Text('重新开始'),
),
),
],
),
);
}
3. 高亮禁手位置
在 CustomPainter 中绘制红色圆环:
dart
// In _GomokuBoardPainter
if (_forbiddenPosition != null) {
final x = 24.0 + _forbiddenPosition!.dx * cellSize;
final y = 24.0 + _forbiddenPosition!.dy * cellSize;
final forbiddenPaint = Paint()
..color = Colors.red.withOpacity(0.6)
..style = PaintingStyle.stroke
..strokeWidth = 4;
canvas.drawCircle(Offset(x, y), cellSize * 0.5, forbiddenPaint);
}
五、核心技术四:游戏模式扩展 ------ 禁手开关
1. 新增设置项
在模式选择弹窗中加入禁手选项:
dart
bool _forbiddenEnabled = false;
// In _showModeSelection
CheckboxListTile(
title: const Text('启用黑棋禁手(专业规则)'),
value: _forbiddenEnabled,
onChanged: (value) {
setState(() {
_forbiddenEnabled = value ?? false;
});
},
),
2. 状态传递
确保 _forbiddenEnabled 在新游戏时保留:
dart
void _startNewGame() {
// ... reset board
// 保留 _forbiddenEnabled 和 _gameMode
}
3. 落子逻辑整合
在 _placeStone 中加入禁手检查:
dart
void _placeStone(int row, int col, {bool isAI = false}) {
if (_winner != null || _board[row][col] != 0) return;
final player = _currentPlayer;
setState(() {
_board[row][col] = player;
// 禁手检查(仅黑棋且启用禁手时)
if (player == 1 && _forbiddenEnabled) {
if (checkForbiddenMove(row, col)) {
_winner = 2;
_forbiddenPosition = Offset(col.toDouble(), row.toDouble());
Future.delayed(const Duration(milliseconds: 300), _showForbiddenDialog);
return;
}
}
// 常规胜负检查
if (checkWin(row, col, player)) {
_winner = player;
Future.delayed(const Duration(milliseconds: 500), _showWinDialog);
} else {
_currentPlayer = 3 - player; // 切换玩家
// ... AI logic
}
});
}
六、AI 适配:让 AI 遵守禁手规则
1. AI 落子前预检
在 _makeAIMove 中,跳过会导致禁手的位置:
dart
for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) {
if (_board[row][col] != 0) continue;
// 如果是黑棋AI,且启用禁手,预检禁手
if (_aiPlayer == 1 && _forbiddenEnabled) {
// 临时落子
_board[row][col] = 1;
final isForbidden = checkForbiddenMove(row, col);
_board[row][col] = 0; // 撤销
if (isForbidden) continue; // 跳过禁手点
}
// ... 计算得分
}
}
✅ 性能考量 :
禁手检测本身很快(O(1)),对 AI 性能影响可忽略
七、完整可运行代码
以下为整合禁手规则、模式选择、AI 适配的完整实现:
dart
import 'package:flutter/material.dart';
import 'dart:math';
void main() => runApp(const GomokuApp());
class GomokuApp extends StatelessWidget {
const GomokuApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: '青绿五子棋',
theme: ThemeData(
primarySwatch: Colors.teal,
scaffoldBackgroundColor: const Color(0xFFF5F7FA),
),
home: GomokuGame(),
);
}
}
enum GameMode {
twoPlayer,
humanVsAIAsBlack,
humanVsAIAsWhite,
}
class GomokuGame extends StatefulWidget {
@override
State<GomokuGame> createState() => _GomokuGameState();
}
class _GomokuGameState extends State<GomokuGame> with TickerProviderStateMixin {
static const int BOARD_SIZE = 15;
static const double PADDING = 24.0;
static const Color BACKGROUND_START = Color(0xFFE6F7F4);
static const Color BACKGROUND_END = Color(0xFFB8E6D9);
static const Color LINE_COLOR = Color(0xFF888888);
late List<List<int>> _board;
int _currentPlayer = 1;
int? _winner;
double _cellSize = 0;
List<Offset> _winningPath = [];
GameMode _gameMode = GameMode.twoPlayer;
int _humanPlayer = 1;
int _aiPlayer = 2;
bool _forbiddenEnabled = false;
Offset? _forbiddenPosition;
String _forbiddenType = '';
// 记录最新 AI 落子位置(用于淡入动画)
Offset? _latestAIMove;
late AnimationController _aiAnimationController;
late Animation<double> _aiOpacity;
final List<List<int>> directions = [
[0, 1],
[1, 0],
[1, 1],
[1, -1],
];
bool _hasShownInitialDialog = false;
@override
void initState() {
super.initState();
_aiAnimationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_aiOpacity = Tween<double>(begin: 0, end: 1).animate(_aiAnimationController);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && !_hasShownInitialDialog) {
_hasShownInitialDialog = true;
_showModeSelection();
}
});
}
@override
void dispose() {
_aiAnimationController.dispose();
super.dispose();
}
void _showModeSelection() {
if (!mounted) return;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: const Text('选择游戏模式'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<GameMode>(
title: const Text('👥 双人对战'),
value: GameMode.twoPlayer,
groupValue: _gameMode,
onChanged: (value) {
if (value != null) {
setState(() {
_gameMode = value;
});
}
},
),
RadioListTile<GameMode>(
title: const Text('⚫ 人机对战(玩家执黑)'),
value: GameMode.humanVsAIAsBlack,
groupValue: _gameMode,
onChanged: (value) {
if (value != null) {
setState(() {
_gameMode = value;
});
}
},
),
RadioListTile<GameMode>(
title: const Text('⚪ 人机对战(玩家执白)'),
value: GameMode.humanVsAIAsWhite,
groupValue: _gameMode,
onChanged: (value) {
if (value != null) {
setState(() {
_gameMode = value;
});
}
},
),
CheckboxListTile(
title: const Text('启用黑棋禁手(专业规则)'),
value: _forbiddenEnabled,
onChanged: (value) {
setState(() {
_forbiddenEnabled = value ?? false;
});
},
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
_startNewGame();
},
child: const Text('确定'),
),
],
);
},
),
);
}
void _startNewGame() {
_board = List.generate(
BOARD_SIZE,
(_) => List.filled(BOARD_SIZE, 0),
);
_winner = null;
_winningPath = [];
_forbiddenPosition = null;
_latestAIMove = null;
if (_gameMode == GameMode.humanVsAIAsBlack) {
_humanPlayer = 1;
_aiPlayer = 2;
_currentPlayer = 1;
} else if (_gameMode == GameMode.humanVsAIAsWhite) {
_humanPlayer = 2;
_aiPlayer = 1;
_currentPlayer = 1;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && _currentPlayer == _aiPlayer && _winner == null) {
_makeAIMove();
}
});
} else {
_currentPlayer = 1;
}
}
// ===== 胜利检测 =====
bool checkWin(int row, int col, int player) {
for (var dir in directions) {
int count = 1;
int r = row + dir[0];
int c = col + dir[1];
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == player) {
count++;
r += dir[0];
c += dir[1];
}
r = row - dir[0];
c = col - dir[1];
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == player) {
count++;
r -= dir[0];
c -= dir[1];
}
if (count >= 5) {
_winningPath = _getWinningPath(row, col, player);
return true;
}
}
return false;
}
List<Offset> _getWinningPath(int row, int col, int player) {
for (var dir in directions) {
final path = <Offset>[];
path.add(Offset(col.toDouble(), row.toDouble()));
int r = row + dir[0];
int c = col + dir[1];
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == player) {
path.add(Offset(c.toDouble(), r.toDouble()));
r += dir[0];
c += dir[1];
}
r = row - dir[0];
c = col - dir[1];
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == player) {
path.insert(0, Offset(c.toDouble(), r.toDouble()));
r -= dir[0];
c -= dir[1];
}
if (path.length >= 5) {
return path.sublist(0, 5);
}
}
return [];
}
// ===== 禁手检测 =====
bool checkForbiddenMove(int row, int col) {
if (_checkLongLine(row, col)) {
_forbiddenType = '长连';
return true;
}
final fourCount = _countFours(row, col);
if (fourCount >= 2) {
_forbiddenType = '四四';
return true;
}
final liveThreeCount = _countLiveThrees(row, col);
if (liveThreeCount >= 2) {
_forbiddenType = '三三';
return true;
}
return false;
}
bool _checkLongLine(int row, int col) {
for (var dir in directions) {
int count = 1;
int r = row + dir[0];
int c = col + dir[1];
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == 1) {
count++;
r += dir[0];
c += dir[1];
}
r = row - dir[0];
c = col - dir[1];
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == 1) {
count++;
r -= dir[0];
c -= dir[1];
}
if (count >= 6) return true;
}
return false;
}
int _countFours(int row, int col) {
int count = 0;
for (var dir in directions) {
final line = _extractLine(row, col, dir, 5);
if (_isFour(line, 1)) count++;
}
return count;
}
bool _isFour(List<int> pattern, int player) {
if (pattern.length != 5) return false;
final selfCount = pattern.where((p) => p == player).length;
final emptyCount = pattern.where((p) => p == 0).length;
final opponentCount = pattern.where((p) => p == 3 - player).length;
return selfCount == 4 && emptyCount == 1 && opponentCount == 0;
}
int _countLiveThrees(int row, int col) {
int count = 0;
for (var dir in directions) {
final line = _extractLine(row, col, dir, 7);
if (_isLiveThree(line, 1)) count++;
}
return count;
}
bool _isLiveThree(List<int> line, int player) {
for (int i = 0; i <= 4; i++) {
if (line[i] == 0 &&
line[i + 1] == player &&
line[i + 2] == player &&
line[i + 3] == player &&
line[i + 4] == 0) {
return true;
}
}
return false;
}
List<int> _extractLine(int row, int col, List<int> dir, int length) {
final half = length ~/ 2;
final line = <int>[];
for (int i = -half; i <= half; i++) {
final r = row + i * dir[0];
final c = col + i * dir[1];
if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE) {
line.add(_board[r][c]);
} else {
line.add(2);
}
}
return line;
}
// ===== AI 评分系统 =====
int _evaluatePattern(List<int> pattern, int player) {
final opponent = 3 - player;
if (pattern.contains(opponent)) return 0;
final selfCount = pattern.where((p) => p == player).length;
final emptyCount = pattern.where((p) => p == 0).length;
if (selfCount == 4 && emptyCount == 1) return 10000;
if (selfCount == 3 && emptyCount == 2) return 2000;
if (selfCount == 3 && emptyCount == 1) return 500;
if (selfCount == 2 && emptyCount == 2) return 200;
if (selfCount == 1) return 10;
return 0;
}
List<int> _extractLineForScore(int row, int col, List<int> dir, int player) {
final opponent = 3 - player;
final line = <int>[];
for (int i = -4; i <= 4; i++) {
final r = row + i * dir[0];
final c = col + i * dir[1];
if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE) {
line.add(_board[r][c]);
} else {
line.add(opponent); // 边界视为对手
}
}
return line;
}
int _calculateScore(int row, int col, int player) {
int totalScore = 0;
for (var dir in directions) {
final line = _extractLineForScore(row, col, dir, player);
for (int i = 0; i <= 4; i++) {
final segment = line.sublist(i, i + 5);
totalScore += _evaluatePattern(segment, player);
}
}
return totalScore;
}
// ===== AI 决策逻辑(已修复)=====
void _makeAIMove() async {
if (!mounted || _winner != null) return;
await Future.delayed(const Duration(milliseconds: 600));
// 检查是否全空
bool isEmpty = true;
for (var row in _board) {
if (row.any((cell) => cell != 0)) {
isEmpty = false;
break;
}
}
if (isEmpty) {
_placeStone(7, 7, isAI: true);
return;
}
// Step 1: 检查 AI 是否能直接获胜
for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) {
if (_board[row][col] != 0) continue;
if (_aiPlayer == 1 && _forbiddenEnabled) {
_board[row][col] = 1;
final isForbidden = checkForbiddenMove(row, col);
_board[row][col] = 0;
if (isForbidden) continue;
}
if (_calculateScore(row, col, _aiPlayer) >= 10000) {
_placeStone(row, col, isAI: true);
return;
}
}
}
// Step 2: 检查是否需要防守(人类即将五连)
for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) {
if (_board[row][col] != 0) continue;
if (_aiPlayer == 1 && _forbiddenEnabled) {
_board[row][col] = 1;
final isForbidden = checkForbiddenMove(row, col);
_board[row][col] = 0;
if (isForbidden) continue;
}
if (_calculateScore(row, col, _humanPlayer) >= 10000) {
_placeStone(row, col, isAI: true);
return;
}
}
}
// Step 3: 普通评估选最佳
int bestScore = -1;
int bestRow = -1, bestCol = -1;
List<Offset> validMoves = [];
for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) {
if (_board[row][col] != 0) continue;
if (_aiPlayer == 1 && _forbiddenEnabled) {
_board[row][col] = 1;
final isForbidden = checkForbiddenMove(row, col);
_board[row][col] = 0;
if (isForbidden) continue;
}
validMoves.add(Offset(row.toDouble(), col.toDouble()));
final aiScore = _calculateScore(row, col, _aiPlayer);
final opponentScore = _calculateScore(row, col, _humanPlayer);
final score = aiScore + opponentScore;
if (score > bestScore) {
bestScore = score;
bestRow = row;
bestCol = col;
}
}
}
// Step 4: 落子(优先最佳,否则随机)
if (bestRow != -1 && bestCol != -1) {
_placeStone(bestRow, bestCol, isAI: true);
} else if (validMoves.isNotEmpty) {
final random = Random();
final move = validMoves[random.nextInt(validMoves.length)];
_placeStone(move.dx.toInt(), move.dy.toInt(), isAI: true);
}
}
// ===== 落子逻辑 =====
void _placeStone(int row, int col, {bool isAI = false}) {
if (_winner != null || _board[row][col] != 0) return;
final player = _currentPlayer;
setState(() {
_board[row][col] = player;
if (player == 1 && _forbiddenEnabled) {
if (checkForbiddenMove(row, col)) {
_winner = 2;
_forbiddenPosition = Offset(col.toDouble(), row.toDouble());
Future.delayed(const Duration(milliseconds: 300), _showForbiddenDialog);
return;
}
}
if (checkWin(row, col, player)) {
_winner = player;
Future.delayed(const Duration(milliseconds: 500), _showWinDialog);
} else {
_currentPlayer = 3 - player;
if (isAI) {
_latestAIMove = Offset(col.toDouble(), row.toDouble());
_aiAnimationController.forward().then((_) {
if (mounted) {
setState(() {
_latestAIMove = null;
});
}
});
}
}
});
if (_gameMode != GameMode.twoPlayer &&
_currentPlayer == _aiPlayer &&
_winner == null) {
_makeAIMove();
}
}
void _showWinDialog() {
if (!mounted) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('${_winner == 1 ? '黑方' : '白方'}获胜!', textAlign: TextAlign.center),
content: const Text('五子连珠,精彩对决!'),
actions: [
Center(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_startNewGame();
},
child: const Text('再来一局'),
),
),
],
),
);
}
void _showForbiddenDialog() {
if (!mounted) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('❌ 黑棋禁手!', style: TextStyle(color: Colors.red)),
content: Text('检测到 $_forbiddenType 禁手,白方获胜!'),
actions: [
Center(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_startNewGame();
},
child: const Text('重新开始'),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: const Text('青绿五子棋', style: TextStyle(color: Colors.black)),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.settings, color: Colors.black),
onPressed: () {
if (mounted) _showModeSelection();
},
),
],
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [BACKGROUND_START, BACKGROUND_END],
),
),
child: Center(
child: LayoutBuilder(
builder: (context, constraints) {
final minSize = min(constraints.maxWidth, constraints.maxHeight) - 2 * PADDING;
_cellSize = minSize / (BOARD_SIZE - 1);
return GestureDetector(
onTapDown: (details) {
if (_gameMode != GameMode.twoPlayer && _currentPlayer != _humanPlayer) {
return;
}
final renderBox = context.findRenderObject() as RenderBox;
final local = renderBox.globalToLocal(details.globalPosition);
final dx = local.dx - PADDING;
final dy = local.dy - PADDING;
if (dx >= 0 && dy >= 0) {
final col = (dx / _cellSize).round();
final row = (dy / _cellSize).round();
if (row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE) {
_placeStone(row, col);
}
}
},
child: Stack(
children: [
CustomPaint(
painter: _GomokuBoardPainter(
cellSize: _cellSize,
winningPath: _winningPath,
forbiddenPosition: _forbiddenPosition,
),
child: SizedBox.square(dimension: minSize + 2 * PADDING),
),
..._renderStones(),
],
),
);
},
),
),
),
);
}
List<Widget> _renderStones() {
final stones = <Widget>[];
for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) {
final player = _board[row][col];
if (player != 0) {
final x = PADDING + col * _cellSize - _cellSize * 0.4;
final y = PADDING + row * _cellSize - _cellSize * 0.4;
bool isForbidden = (player == 1 &&
_forbiddenPosition != null &&
_forbiddenPosition!.dx == col &&
_forbiddenPosition!.dy == row);
Widget stone = _buildStone(player, isForbidden: isForbidden);
if (player == _aiPlayer &&
_latestAIMove != null &&
_latestAIMove!.dx == col &&
_latestAIMove!.dy == row) {
stone = AnimatedOpacity(
opacity: _aiOpacity.value,
duration: const Duration(milliseconds: 300),
child: stone,
);
}
stones.add(Positioned(left: x, top: y, child: stone));
}
}
}
return stones;
}
Widget _buildStone(int player, {bool isForbidden = false}) {
if (isForbidden) {
return AnimatedContainer(
duration: const Duration(milliseconds: 800),
curve: Curves.easeInOut,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.red.withOpacity(0.5),
blurRadius: 12,
spreadRadius: 2,
),
],
),
width: _cellSize * 0.8,
height: _cellSize * 0.8,
);
}
final isBlack = player == 1;
return Container(
width: _cellSize * 0.8,
height: _cellSize * 0.8,
decoration: BoxDecoration(
color: isBlack ? const Color(0xFF222222) : Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: isBlack ? Colors.black26 : Colors.grey.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
if (!isBlack)
BoxShadow(
color: Colors.white.withOpacity(0.6),
blurRadius: 2,
offset: const Offset(-1, -1),
spreadRadius: -1,
),
],
),
);
}
}
class _GomokuBoardPainter extends CustomPainter {
final double cellSize;
final List<Offset> winningPath;
final Offset? forbiddenPosition;
_GomokuBoardPainter({
required this.cellSize,
required this.winningPath,
this.forbiddenPosition,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFF888888)
..strokeWidth = 1.2
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final offset = 24.0;
final boardSize = 15;
for (int i = 0; i < boardSize; i++) {
final y = offset + i * cellSize;
canvas.drawLine(
Offset(offset, y),
Offset(offset + (boardSize - 1) * cellSize, y),
paint,
);
}
for (int j = 0; j < boardSize; j++) {
final x = offset + j * cellSize;
canvas.drawLine(
Offset(x, offset),
Offset(x, offset + (boardSize - 1) * cellSize),
paint,
);
}
if (winningPath.length >= 5) {
final winPaint = Paint()
..color = Colors.red.withOpacity(0.7)
..strokeWidth = 4
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final points = winningPath.map((offset) {
return Offset(
24.0 + offset.dx * cellSize,
24.0 + offset.dy * cellSize,
);
}).toList();
for (int i = 0; i < points.length - 1; i++) {
canvas.drawLine(points[i], points[i + 1], winPaint);
}
}
if (forbiddenPosition != null) {
final x = 24.0 + forbiddenPosition!.dx * cellSize;
final y = 24.0 + forbiddenPosition!.dy * cellSize;
final forbiddenPaint = Paint()
..color = Colors.red.withOpacity(0.6)
..style = PaintingStyle.stroke
..strokeWidth = 4;
canvas.drawCircle(Offset(x, y), cellSize * 0.5, forbiddenPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
运行界面

结语:专业规则,成就竞技之美
通过实现 RIF 禁手规则,我们的青绿五子棋应用完成了从"休闲玩具"到"竞技平台"的关键跃迁。三三、四四、长连的精准判定,不仅体现了对专业规则的尊重,更让玩家在实战中理解五子棋的深层策略------如何利用禁手设陷阱,又如何避免落入对手的圈套。
这套实现充分展现了 Flutter + OpenHarmony 的工程能力:
- 算法精准:禁手检测覆盖所有边界情况
- 体验流畅:红色闪烁 + 弹窗提示,反馈即时明确
- 架构灵活:禁手开关无缝集成,不影响原有逻辑
- AI 适配:AI 自动规避禁手,行为更真实