鸿蒙+Flutter 跨平台开发——围棋辅助教学APP
🚀 效果展示

前言介绍
1. 项目背景
Flutter作为Google推出的UI工具包,具有"一次开发,多端运行"的优势,能够帮助开发者快速构建高性能、高保真的移动应用。而鸿蒙操作系统作为华为自主研发的分布式操作系统,具有强大的设备协同能力和安全性能,正逐渐成为移动生态的重要组成部分。
围棋 作为中国传统国粹,拥有悠久的历史和深厚的文化底蕴。然而,对于初学者来说,围棋的规则较为复杂,尤其是"气"和"吃子"的概念难以理解。因此,开发一款直观、易用的围棋辅助教学APP,对于推广围棋文化、帮助初学者快速入门具有重要意义。
2. 技术栈选择
| 技术/框架 | 版本 | 用途 |
|---|---|---|
| Flutter | 3.0.0+ | 跨平台UI框架 |
| Dart | 2.17.0+ | 开发语言 |
| HarmonyOS SDK | API Version 9+ | 鸿蒙系统支持 |
| Canvas API | - | 棋盘绘制 |
| BFS算法 | - | 连通块检测 |
游戏设计及介绍
1. 功能设计
1.1 核心功能
| 功能模块 | 功能描述 |
|---|---|
| 棋盘绘制 | 绘制19x19标准围棋棋盘,包括网格线和星位点 |
| 棋子放置 | 支持黑白棋子交替放置 |
| 气的显示 | 根据棋子颜色显示不同颜色的气 |
| 吃子逻辑 | 自动提掉没有气的棋子 |
| 禁着点检测 | 禁止在禁着点落子 |
| 打劫规则 | 实现打劫规则,防止棋局无限循环 |
| 悔棋功能 | 支持悔棋,恢复上一步状态 |
| 重置功能 | 重置棋盘,重新开始游戏 |
1.2 系统架构
2. 界面设计
2.1 主界面布局
2.2 棋子与气的显示设计
| 棋子类型 | 显示颜色 | 气的颜色 |
|---|---|---|
| 黑棋 | 纯黑色 | 半透明白色 |
| 白棋 | 纯白色 | 半透明黑色 |
3. 游戏规则设计
3.1 基本规则
- 气的定义:棋子周围相邻的空交叉点
- 吃子规则:当一方棋子或其连通块的所有气都被对方占据时,必须被提子
- 禁着点:落子后自身立即无气且无法提掉对方棋子的点,禁止落子
- 打劫规则:被提劫方不能立即反提,必须先在棋盘其他地方走一步
3.2 棋子放置及提子流程
核心代码实现
1. 棋盘初始化与绘制
1.1 棋盘初始化
/// 初始化棋盘
void _initializeBoard() {
// 创建19x19的空棋盘
_board = List.generate(
boardSize,
(_) => List.filled(boardSize, null),
);
// 初始化气的数量,所有位置初始气为4
_liberties = List.generate(
boardSize,
(_) => List.filled(boardSize, defaultLiberties),
);
// 更新边界位置的气(边界位置气较少)
_updateBorderLiberties();
// 保存初始状态到历史记录
_saveHistory();
}
1.2 棋盘绘制
/// 棋盘绘制画家
class BoardPainter extends CustomPainter {
final int boardSize;
// 星位点位置
static const List<int> starPoints = [3, 9, 15];
// 星位点半径比例
static const double starRadiusRatio = 8;
// 线条宽度
static const double lineWidth = 1.5;
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.black
..strokeWidth = lineWidth;
final cellSize = size.width / boardSize;
// 绘制横线
for (int i = 0; i < boardSize; i++) {
canvas.drawLine(
Offset(0, i * cellSize),
Offset(size.width, i * cellSize),
paint,
);
}
// 绘制竖线
for (int i = 0; i < boardSize; i++) {
canvas.drawLine(
Offset(i * cellSize, 0),
Offset(i * cellSize, size.height),
paint,
);
}
// 绘制星位点
final starPaint = Paint()
..color = Colors.black
..style = PaintingStyle.fill;
for (int x in starPoints) {
for (int y in starPoints) {
canvas.drawCircle(
Offset(x * cellSize, y * cellSize),
cellSize / starRadiusRatio,
starPaint,
);
}
}
}
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
2. 棋子放置与提子
2.1 棋子放置
/// 放置棋子
void _placeStone(int x, int y) {
// 如果该位置已有棋子,不处理
if (_board[x][y] != null) return;
// 检查是否为禁着点
if (_isForbiddenPoint(x, y, _currentPlayer)) {
return;
}
// 检查是否为打劫位置
final (int, int) pos = (x, y);
if (_koPosition == pos && _koColor == _currentPlayer) {
return;
}
setState(() {
// 放置当前玩家的棋子
_board[x][y] = _currentPlayer;
// 检查并移除没有气的棋子
_checkAndRemoveCapturedStones();
// 保存历史记录
_saveHistory();
// 更新相邻位置的气
_recalculateAllLiberties();
// 切换玩家
_currentPlayer = !_currentPlayer;
});
}
2.2 提子逻辑
/// 检查并移除没有气的棋子
void _checkAndRemoveCapturedStones() {
// 记录需要移除的棋子位置
final Set<(int, int)> stonesToRemove = {};
// 检查所有棋子
for (int i = 0; i < boardSize; i++) {
for (int j = 0; j < boardSize; j++) {
if (_board[i][j] != null && !stonesToRemove.contains((i, j))) {
// 检查该棋子所在的连通块是否有气
final result = _getGroupAndLiberties(i, j);
final group = result[0] as Set<(int, int)>;
final liberties = result[1] as int;
if (liberties == 0) {
// 没有气,添加到移除列表
stonesToRemove.addAll(group);
}
}
}
}
// 先移除所有没有气的棋子
for (final (x, y) in stonesToRemove) {
_board[x][y] = null;
}
// 检查是否形成打劫(只提掉一个棋子时)
if (stonesToRemove.length == 1) {
final capturedStone = stonesToRemove.first;
final capturedColor = !_currentPlayer; // 被提掉的是对方颜色
// 检查当前玩家的棋子是否只形成一个子的连通块,且只有一个气(即打劫)
for (int i = 0; i < boardSize; i++) {
for (int j = 0; j < boardSize; j++) {
if (_board[i][j] == _currentPlayer) {
final result = _getGroupAndLiberties(i, j);
final group = result[0] as Set<(int, int)>;
final liberties = result[1] as int;
if (group.length == 1 && liberties == 1) {
// 形成打劫,记录打劫位置
_koPosition = capturedStone;
_koColor = capturedColor;
_koStep = _history.length;
return;
}
}
}
}
}
// 不是打劫或不符合打劫条件,清除打劫状态
_koPosition = null;
_koColor = null;
}
3. 气的计算与显示
3.1 气的计算流程图
3.2 气的计算实现
/// 计算单个位置的气
int _calculateLiberties(int x, int y) {
// 如果该位置已有棋子,返回0
if (_board[x][y] != null) return 0;
int count = 0;
// 检查上下左右四个方向
final directions = [
[-1, 0], [1, 0], [0, -1], [0, 1]
];
for (var dir in directions) {
int nx = x + dir[0];
int ny = y + dir[1];
// 检查边界
if (nx >= 0 && nx < boardSize && ny >= 0 && ny < boardSize) {
// 如果相邻位置为空,气+1
if (_board[nx][ny] == null) {
count++;
}
}
}
return count;
}
3.3 气的显示实现
// 绘制棋子的气
for (int i = 0; i < boardSize; i++) {
for (int j = 0; j < boardSize; j++) {
if (board[i][j] != null) {
final bool isBlack = board[i][j]!;
// 检查上下左右四个方向,绘制气
for (var dir in [
[-1, 0], [1, 0], [0, -1], [0, 1]
]) {
int nx = i + dir[0];
int ny = j + dir[1];
final (int, int) pos = (nx, ny);
// 检查边界、是否为空以及是否已经绘制过
if (nx >= 0 && nx < boardSize && ny >= 0 && ny < boardSize) {
if (board[nx][ny] == null && !libertyPositions.contains(pos)) {
libertyPositions.add(pos);
// 根据棋子颜色调整气的颜色
Color bgColor;
Color borderColor;
if (isBlack) {
// 黑棋的气偏白
bgColor = Color.fromRGBO(255, 255, 255, 0.5);
borderColor = Color.fromRGBO(255, 255, 255, 0.8);
} else {
// 白棋的气偏黑
bgColor = Color.fromRGBO(0, 0, 0, 0.5);
borderColor = Color.fromRGBO(0, 0, 0, 0.8);
}
// 绘制气棋子
widgets.add(Positioned(
left: nx * cellSize,
top: ny * cellSize,
width: cellSize,
height: cellSize,
child: Center(
child: CircleAvatar(
backgroundColor: bgColor,
radius: cellSize / 3.5,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: borderColor,
width: 1.5,
),
),
),
),
),
));
}
}
}
}
}
}
4. 禁着点检测
/// 检查是否为禁着点
bool _isForbiddenPoint(int x, int y, bool color) {
// 临时放置棋子,检查是否合法
_board[x][y] = color;
// 检查该棋子所在连通块是否有气
final result = _getGroupAndLiberties(x, y);
final int playerLiberties = result[1] as int;
// 检查是否能提掉对方棋子
bool canCaptureOpponent = false;
for (int i = 0; i < boardSize; i++) {
for (int j = 0; j < boardSize; j++) {
if (_board[i][j] != null && _board[i][j] != color) {
final opponentResult = _getGroupAndLiberties(i, j);
final int opponentLiberties = opponentResult[1] as int;
if (opponentLiberties == 0) {
canCaptureOpponent = true;
break;
}
}
}
if (canCaptureOpponent) break;
}
// 恢复棋盘状态
_board[x][y] = null;
// 禁着点:自身无气且无法提掉对方棋子
return playerLiberties == 0 && !canCaptureOpponent;
}
5. 打劫规则实现
// 打劫相关状态
// 打劫位置,null表示无打劫
(int, int)? _koPosition;
// 打劫颜色,记录当前打劫的棋子颜色
bool? _koColor;
// 上次提劫的步数
int _koStep = 0;
// 检查是否为打劫位置
final (int, int) pos = (x, y);
if (_koPosition == pos && _koColor == _currentPlayer) {
// 打劫位置,被提劫方不能立即反提
return;
}
运行环境
1. 开发环境
| 环境 | 版本/配置 |
|---|---|
| 操作系统 | Windows 11 |
| Flutter SDK | 3.0.0+ |
| Dart SDK | 2.17.0+ |
| IDE | Android Studio / Visual Studio Code |
| 鸿蒙SDK | API Version 9+ |
| 模拟器 | 鸿蒙模拟器 / Android模拟器 / iOS模拟器 |
2. 运行环境
| 平台 | 版本要求 |
|---|---|
| 鸿蒙 | HarmonyOS 2.0+ |
| Android | Android 6.0+ |
| iOS | iOS 14.0+ |
| Web | Chrome 88+ |
3. 安装与运行
3.1 安装依赖
flutter pub get
3.2 运行在鸿蒙设备
flutter run --device-id <harmonyos_device_id>
3.3 运行在Android设备
flutter run --device-id <android_device_id>
3.4 运行在iOS设备
flutter run --device-id <ios_device_id>
问题解决
1. 气的显示方式调整
问题描述:最初设计中,气的显示方式是在每个空位显示一个红色数字,表示该位置的气数。但这种方式不够直观,初学者难以理解。
解决方案:将气的显示方式改为在棋子周围的空位显示半透明的棋子,黑棋的气显示为半透明白色,白棋的气显示为半透明黑色。这样更直观,初学者可以直接看到棋子的气在哪里。
代码调整:
// 从数字显示改为半透明棋子显示
if (stoneColor == null && liberties > 0)
Positioned(
top: libertyPositionOffset,
right: libertyPositionOffset,
child: Container(
padding: EdgeInsets.all(libertyPadding),
decoration: BoxDecoration(
color: const Color.fromRGBO(255, 0, 0, 0.8),
borderRadius: BorderRadius.circular(libertyBorderRadius),
),
child: Text(
'$liberties',
style: TextStyle(
color: Colors.white,
fontSize: cellSize / libertyTextRatio,
fontWeight: FontWeight.bold,
),
),
),
),
// 改为半透明棋子显示
CircleAvatar(
backgroundColor: bgColor,
radius: cellSize / 3.5,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: borderColor,
width: 1.5,
),
),
),
),
2. 吃子逻辑的完善
问题描述:最初的吃子逻辑只检查单个棋子的气,而没有考虑棋子的连通块。这导致有些情况下,虽然单个棋子有气,但整个连通块没有气时,棋子没有被正确提掉。
解决方案:实现了连通块检测算法,使用广度优先搜索(BFS)来查找棋子的连通块,并计算整个连通块的气。当整个连通块没有气时,将其从棋盘上移除。
代码调整:
// 从单个棋子气检查改为连通块气检查
// 检查该棋子所在的连通块是否有气
final result = _getGroupAndLiberties(i, j);
final group = result[0] as Set<(int, int)>;
final liberties = result[1] as int;
if (liberties == 0) {
// 没有气,添加到移除列表
stonesToRemove.addAll(group);
}
3. 打劫规则的实现难点
问题描述:打劫规则是围棋中比较复杂的规则,需要检测打劫状态,并防止玩家立即反提。
解决方案:通过记录打劫位置、颜色和步数,实现了打劫规则。当检测到打劫时,禁止被提劫方立即反提,必须先在棋盘其他地方走一步。
关键代码:
// 记录打劫状态
_koPosition = capturedStone;
_koColor = capturedColor;
_koStep = _history.length;
// 检查是否为打劫位置
if (_koPosition == pos && _koColor == _currentPlayer) {
return;
}
4. Flutter与鸿蒙的适配问题
问题描述:在将Flutter应用部署到鸿蒙设备上时,遇到了一些适配问题,比如屏幕尺寸、字体大小等。
解决方案:
- 使用
MediaQuery获取设备屏幕尺寸,动态调整UI元素大小 - 使用
AspectRatio确保棋盘保持正方形 - 适配鸿蒙系统的特殊要求,比如权限配置等
代码调整:
// 使用AspectRatio确保棋盘保持正方形
AspectRatio(
aspectRatio: 1,
child: GoBoardWidget(
board: _board,
liberties: _liberties,
onTap: _placeStone,
),
),
总结
1. 项目成果
本项目成功实现了一款基于Flutter和鸿蒙的围棋辅助教学APP,具有以下特点:
- 完整的围棋规则:实现了包括气、吃子、禁着点、打劫等在内的完整围棋规则。
- 直观的气显示:通过半透明棋子直观地显示棋子的气,帮助初学者理解。
- 跨平台支持:使用Flutter开发,可以同时运行在鸿蒙、Android、iOS等多个平台上。
- 良好的用户体验:界面简洁美观,操作流畅,支持悔棋和重置功能。
- 高性能:使用Canvas直接绘制棋盘,性能优异。
2. 未来展望
- AI对战功能:集成AI算法,让用户可以与电脑对战,提高学习效果。
- 棋局回放功能:支持保存和回放棋局,方便用户分析和学习。
- 教学模式:添加入门教程、基本战术等教学内容,帮助初学者快速入门。
- 多人在线对战:支持通过网络进行多人对战,增强社交性。
- 3D棋盘:实现3D棋盘效果,提高视觉体验。
- 语音交互:支持语音指令,提高操作便捷性。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)