Flutter 框架跨平台鸿蒙开发 - 开发双人对战五子棋游戏
算法难度特点随机落子⭐最简单,随机选择空位评分函数⭐⭐对每个位置打分,选最高分极小化极大⭐⭐⭐博弈树搜索,考虑对手反应Alpha-Beta剪枝⭐⭐⭐⭐优化搜索,减少计算量蒙特卡洛树搜索⭐⭐⭐⭐⭐随机模拟,适合复杂局面棋盘绘制:CustomPainter绘制网格和星位胜负判定:四方向搜索算法,O(1)复杂度状态管理:棋盘数据、玩家切换、历史记录交互设计:落子、悔棋、重开、高亮显示视觉效果:渐变棋子、
⚫⚪ Flutter + HarmonyOS 实战:开发双人对战五子棋游戏
运行效果预览图

📋 文章导读
| 章节 | 内容概要 | 预计阅读 |
|---|---|---|
| 一 | 五子棋规则与游戏设计 | 3分钟 |
| 二 | 棋盘数据结构与渲染 | 8分钟 |
| 三 | 胜负判定算法详解 | 10分钟 |
| 四 | 游戏交互与状态管理 | 8分钟 |
| 五 | UI美化与动画效果 | 5分钟 |
| 六 | 完整源码与运行 | 3分钟 |
💡 写在前面:五子棋是一款老少皆宜的经典棋类游戏,规则简单却变化无穷。本文将带你用Flutter实现一款支持双人对战的五子棋游戏,重点讲解棋盘绘制、胜负判定算法、以及如何打造流畅的游戏体验。无论你是想学习游戏开发,还是想和朋友来一局,这篇文章都能满足你。
一、游戏设计
1.1 五子棋规则
五子棋的规则可以用一句话概括:先在横、竖、斜任意方向连成五子者获胜。
-
棋盘规格
- 标准棋盘为15×15,共225个交叉点
1.2 功能设计
1.3 界面布局
| 区域 | 内容 | 说明 |
|---|---|---|
| 顶部AppBar | 标题、悔棋、重开按钮 | 游戏控制 |
| 玩家指示器 | 黑棋/白棋卡片 | 显示当前执子方 |
| 棋盘区域 | 15×15网格 + 棋子 | 游戏主体 |
| 底部信息 | 棋盘规格、步数、状态 | 游戏统计 |
二、棋盘数据结构与渲染
2.1 数据模型设计
/// 棋子类型枚举
enum ChessType {
none, // 空位
black, // 黑棋
white // 白棋
}
/// 棋盘数据:15×15的二维数组
late List<List<ChessType>> board;
/// 初始化棋盘
void _initGame() {
board = List.generate(
15,
(_) => List.generate(15, (_) => ChessType.none),
);
}
2.2 棋盘坐标系统
棋盘采用标准的二维坐标系,左上角为原点:
0 1 2 3 4 ... 14
┌───┬───┬───┬───┬───┬───┬───┐
0 │ │ │ │ │ │ │ │
├───┼───┼───┼───┼───┼───┼───┤
1 │ │ │ │ │ │ │ │
├───┼───┼───┼───┼───┼───┼───┤
2 │ │ │ │ ● │ │ │ │ ← board[2][3] = black
├───┼───┼───┼───┼───┼───┼───┤
3 │ │ │ │ │ ○ │ │ │ ← board[3][4] = white
...
2.3 棋盘绘制
使用 CustomPainter 绘制棋盘网格和星位:
class BoardPainter extends CustomPainter {
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.black
..strokeWidth = 1;
final cellSize = size.width / 15;
final offset = cellSize / 2;
// 绘制15条横线和15条竖线
for (int i = 0; i < 15; i++) {
// 横线
canvas.drawLine(
Offset(offset, offset + i * cellSize),
Offset(size.width - offset, offset + i * cellSize),
paint,
);
// 竖线
canvas.drawLine(
Offset(offset + i * cellSize, offset),
Offset(offset + i * cellSize, size.height - offset),
paint,
);
}
// 绘制星位(5个黑点)
final starPositions = [
[7, 7], // 天元(中心)
[3, 3], [3, 11], [11, 3], [11, 11], // 四角星
];
for (var pos in starPositions) {
canvas.drawCircle(
Offset(offset + pos[0] * cellSize, offset + pos[1] * cellSize),
4,
Paint()..color = Colors.black,
);
}
}
}
2.4 星位说明
标准五子棋棋盘有5个星位,用于辅助定位:
| 星位名称 | 坐标 | 位置说明 |
|---|---|---|
| 天元 | (7, 7) | 棋盘正中心 |
| 左上星 | (3, 3) | 左上角区域 |
| 右上星 | (11, 3) | 右上角区域 |
| 左下星 | (3, 11) | 左下角区域 |
| 右下星 | (11, 11) | 右下角区域 |
三、胜负判定算法
3.1 算法思路
每次落子后,只需检查以该点为中心的四个方向是否形成五连:
3.2 四个检查方向
| 方向 | 向量 (dx, dy) | 说明 |
|---|---|---|
| 水平 → | (1, 0) | 左右方向 |
| 垂直 ↓ | (0, 1) | 上下方向 |
| 主对角线 ↘ | (1, 1) | 左上到右下 |
| 副对角线 ↙ | (1, -1) | 右上到左下 |
3.3 核心算法实现
/// 检查是否获胜
/// [x], [y] 最后落子的坐标
bool _checkWin(int x, int y) {
// 四个方向的向量
final directions = [
[1, 0], // 水平
[0, 1], // 垂直
[1, 1], // 主对角线
[1, -1], // 副对角线
];
for (var dir in directions) {
List<List<int>> pieces = [[x, y]]; // 记录连续棋子位置
int count = 1;
// 正方向搜索
for (int i = 1; i < 5; i++) {
int nx = x + dir[0] * i;
int ny = y + dir[1] * i;
if (_isValidPos(nx, ny) && board[ny][nx] == currentPlayer) {
count++;
pieces.add([nx, ny]);
} else {
break; // 遇到边界或不同棋子,停止搜索
}
}
// 反方向搜索
for (int i = 1; i < 5; i++) {
int nx = x - dir[0] * i;
int ny = y - dir[1] * i;
if (_isValidPos(nx, ny) && board[ny][nx] == currentPlayer) {
count++;
pieces.add([nx, ny]);
} else {
break;
}
}
// 判断是否达到5个
if (count >= 5) {
winningPieces = pieces; // 保存获胜棋子位置
return true;
}
}
return false;
}
3.4 算法复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(1)O(1)O(1) | 每次最多检查 4×8=32 个位置 |
| 空间复杂度 | O(1)O(1)O(1) | 只需常数级额外空间 |
由于每次只检查落子点周围的有限范围,算法效率非常高。
3.5 边界检查
/// 检查坐标是否在棋盘范围内
bool _isValidPos(int x, int y) {
return x >= 0 && x < 15 && y >= 0 && y < 15;
}
四、游戏交互与状态管理
4.1 状态变量
// 当前执子方
ChessType currentPlayer = ChessType.black;
// 游戏是否结束
bool gameOver = false;
// 获胜者
ChessType? winner;
// 最后落子位置(用于高亮)
int? lastX, lastY;
// 获胜的五个棋子位置(用于标记)
List<List<int>> winningPieces = [];
// 落子历史(用于悔棋)
List<List<int>> history = [];
4.2 落子流程
4.3 落子函数实现
void _placePiece(int x, int y) {
// 前置检查
if (gameOver || board[y][x] != ChessType.none) return;
setState(() {
// 1. 放置棋子
board[y][x] = currentPlayer;
lastX = x;
lastY = y;
history.add([x, y]);
// 2. 检查胜利
if (_checkWin(x, y)) {
gameOver = true;
winner = currentPlayer;
_showWinDialog();
}
// 3. 检查平局
else if (_isBoardFull()) {
gameOver = true;
_showDrawDialog();
}
// 4. 切换玩家
else {
currentPlayer = currentPlayer == ChessType.black
? ChessType.white
: ChessType.black;
}
});
}
4.4 悔棋功能
void _undoMove() {
if (history.isEmpty || gameOver) return;
setState(() {
// 取出最后一步
final last = history.removeLast();
// 清除该位置的棋子
board[last[1]][last[0]] = ChessType.none;
// 切换回上一个玩家
currentPlayer = currentPlayer == ChessType.black
? ChessType.white
: ChessType.black;
// 更新最后落子位置
if (history.isNotEmpty) {
lastX = history.last[0];
lastY = history.last[1];
} else {
lastX = null;
lastY = null;
}
});
}
五、UI美化与动画效果
5.1 棋子渲染
使用渐变色让棋子更有立体感:
Widget _buildPiece(ChessType type, bool isLast, bool isWinning) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
shape: BoxShape.circle,
// 渐变效果,模拟光照
gradient: type == ChessType.black
? RadialGradient(
center: const Alignment(-0.3, -0.3), // 光源位置
colors: [Colors.grey.shade700, Colors.black],
)
: RadialGradient(
center: const Alignment(-0.3, -0.3),
colors: [Colors.white, Colors.grey.shade300],
),
// 边框:获胜棋子红色,最后落子金色
border: Border.all(
color: isWinning ? Colors.red : isLast ? Colors.amber : Colors.grey,
width: isWinning || isLast ? 3 : 1,
),
// 阴影效果
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.4),
blurRadius: 4,
offset: const Offset(2, 2),
),
],
),
);
}
5.2 视觉效果对照表
| 状态 | 边框颜色 | 边框宽度 | 特殊标记 |
|---|---|---|---|
| 普通棋子 | 灰色 | 1px | 无 |
| 最后落子 | 金色 | 3px | 中心红点 |
| 获胜棋子 | 红色 | 3px | 无 |
5.3 玩家指示器动画
AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
// 当前玩家高亮
color: isActive ? Colors.brown.shade600 : Colors.white,
borderRadius: BorderRadius.circular(16),
// 获胜者金色边框
border: isWinner
? Border.all(color: Colors.amber, width: 3)
: Border.all(color: Colors.brown.shade300),
// 当前玩家添加阴影
boxShadow: isActive ? [BoxShadow(...)] : null,
),
)
六、完整源码与运行
6.1 项目结构
flutter_gomoku/
├── lib/
│ └── main.dart # 五子棋游戏代码(约350行)
├── ohos/ # 鸿蒙平台配置
├── pubspec.yaml # 依赖配置
└── README.md # 项目说明
6.2 运行命令
# 获取依赖
flutter pub get
# 运行游戏
flutter run
# 运行到鸿蒙设备
flutter run -d ohos
6.3 功能清单
| 功能 | 状态 | 说明 |
|---|---|---|
| 15×15标准棋盘 | ✅ | CustomPainter绘制 |
| 双人对战 | ✅ | 黑白交替落子 |
| 胜负判定 | ✅ | 四方向五连检测 |
| 悔棋功能 | ✅ | 支持多步悔棋 |
| 重新开始 | ✅ | 一键重置 |
| 最后落子高亮 | ✅ | 金色边框+红点 |
| 获胜棋子标记 | ✅ | 红色边框 |
| 玩家指示器 | ✅ | 动画切换 |
| 步数统计 | ✅ | 实时显示 |
七、扩展方向
7.1 功能扩展
7.2 AI算法简介
如果想添加人机对战,可以使用以下算法:
| 算法 | 难度 | 特点 |
|---|---|---|
| 随机落子 | ⭐ | 最简单,随机选择空位 |
| 评分函数 | ⭐⭐ | 对每个位置打分,选最高分 |
| 极小化极大 | ⭐⭐⭐ | 博弈树搜索,考虑对手反应 |
| Alpha-Beta剪枝 | ⭐⭐⭐⭐ | 优化搜索,减少计算量 |
| 蒙特卡洛树搜索 | ⭐⭐⭐⭐⭐ | 随机模拟,适合复杂局面 |
7.3 简单AI示例
/// 简单AI:评分法
List<int>? findBestMove() {
int bestScore = -1;
List<int>? bestMove;
for (int y = 0; y < 15; y++) {
for (int x = 0; x < 15; x++) {
if (board[y][x] == ChessType.none) {
int score = _evaluatePosition(x, y);
if (score > bestScore) {
bestScore = score;
bestMove = [x, y];
}
}
}
}
return bestMove;
}
/// 评估某个位置的分数
int _evaluatePosition(int x, int y) {
int score = 0;
// 检查周围是否有己方棋子(进攻)
// 检查周围是否有对方棋子(防守)
// 优先选择中心位置
// ...
return score;
}
八、常见问题
Q1: 为什么用枚举而不是数字表示棋子?使用枚举(enum ChessType)比数字(0, 1, 2)有以下优势:
- 可读性:
ChessType.black比1更直观 - 类型安全:编译器会检查类型错误
- IDE支持:自动补全和重构更方便
因为只有最后落下的棋子才可能形成新的五连。之前的棋子如果能形成五连,游戏早就结束了。这样可以将检查范围从整个棋盘(225个点)缩小到1个点的周围,大大提高效率。
Q3: 如何实现禁手规则?禁手是专业五子棋的规则,限制黑棋的某些走法:
bool isForbidden(int x, int y) {
if (currentPlayer != ChessType.black) return false;
// 检查三三禁手:同时形成两个活三
// 检查四四禁手:同时形成两个四
// 检查长连禁手:形成六子或以上
return false; // 具体实现较复杂
}
九、总结
本文实现了一款完整的双人对战五子棋游戏,核心技术点包括:
- 棋盘绘制:CustomPainter绘制网格和星位
- 胜负判定:四方向搜索算法,O(1)复杂度
- 状态管理:棋盘数据、玩家切换、历史记录
- 交互设计:落子、悔棋、重开、高亮显示
- 视觉效果:渐变棋子、动画切换、阴影效果
五子棋虽然规则简单,但要下好却需要深厚的功力。希望这个项目能帮你理解游戏开发的基本思路,也欢迎和朋友来一局!
⚫⚪ 完整源码已上传,欢迎Star支持!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)