通过网盘分享的文件:game_flutter_openharmony.zip
链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip
前言
黑白棋(又叫翻转棋、奥赛罗)是一个策略游戏。落子后,夹住的对方棋子会翻转成自己的颜色。
翻转是黑白棋的核心机制,这篇来聊聊怎么实现。这个机制看起来简单,但实现起来需要考虑八个方向的搜索、夹住的判断、以及各种边界情况。
我在实现这个功能的时候,最开始只考虑了水平和垂直方向,忘了对角线,结果游戏玩起来很奇怪。后来加上了八个方向的搜索,游戏才正常了。

棋盘数据
dart
static const int size = 8;
late List<List<int>> board; // 0:empty, 1:black, 2:white
8x8的棋盘,每个格子存储:
- 0: 空
- 1: 黑棋
- 2: 白棋
用数字而不是枚举,是因为数字可以做简单的数学运算(比如3-player得到对手),代码更简洁。
初始布局
dart
void _initGame() {
board = List.generate(size, (_) => List.filled(size, 0));
board[3][3] = board[4][4] = 2;
board[3][4] = board[4][3] = 1;
中间四格交叉放置黑白棋,这是标准开局。
3 4
3 ○ ●
4 ● ○
List.generate创建8行,每行用List.filled创建8个0。然后在中间四格放置初始棋子。
这个初始布局是黑白棋的标准规则,所有正式比赛都是这样开局的。
获取可翻转的棋子
dart
List<List<int>> _getFlips(int row, int col, int player) {
if (board[row][col] != 0) return [];
List<List<int>> allFlips = [];
int opponent = 3 - player;
这是黑白棋最核心的方法,计算在某个位置落子后能翻转哪些棋子。返回一个坐标列表,每个元素是[row, col]。
前置检查
dart
if (board[row][col] != 0) return [];
只能落在空格子上。如果格子已经有棋子,返回空列表,表示不能落子。
对手计算
dart
int opponent = 3 - player;
巧妙的计算:player是1时opponent是2,player是2时opponent是1。
这个技巧利用了1+2=3的特性。比用if-else判断更简洁,而且不容易出错。
八个方向
dart
for (int dr = -1; dr <= 1; dr++) {
for (int dc = -1; dc <= 1; dc++) {
if (dr == 0 && dc == 0) continue;
遍历8个方向:上、下、左、右、四个对角线。
dr和dc是方向向量,取值-1、0、1的组合,排除(0,0)。
具体来说:
- (-1, -1): 左上
- (-1, 0): 上
- (-1, 1): 右上
- (0, -1): 左
- (0, 1): 右
- (1, -1): 左下
- (1, 0): 下
- (1, 1): 右下
用两层循环生成这8个方向,比手动列出来更简洁。
沿方向搜索
dart
List<List<int>> flips = [];
int r = row + dr, c = col + dc;
while (r >= 0 && r < size && c >= 0 && c < size && board[r][c] == opponent) {
flips.add([r, c]);
r += dr; c += dc;
}
从落子位置出发,沿着方向走,遇到对手的棋子就记录下来。
while循环的条件:
- r和c在棋盘范围内
- 当前位置是对手的棋子
每次循环把当前位置加入flips列表,然后继续沿方向走(r += dr, c += dc)。
验证夹住
dart
if (flips.isNotEmpty && r >= 0 && r < size && c >= 0 && c < size && board[r][c] == player) {
allFlips.addAll(flips);
}
}
}
return allFlips;
}
如果:
- 记录了对手的棋子(flips不为空)
- 最后遇到的是自己的棋子(在棋盘范围内且是player)
说明这些对手棋子被夹住了,可以翻转。把flips加入allFlips。
如果flips为空,说明这个方向没有对手棋子,不能翻转。
如果最后遇到的是空格或出界,说明没有形成夹击,也不能翻转。
落子逻辑
dart
void _placePiece(int row, int col) {
if (gameOver) return;
List<List<int>> flips = _getFlips(row, col, currentPlayer);
if (flips.isEmpty) return;
setState(() {
board[row][col] = currentPlayer;
for (var f in flips) board[f[0]][f[1]] = currentPlayer;
这个方法处理玩家的落子操作,是游戏交互的核心。
游戏结束检查
dart
if (gameOver) return;
游戏已经结束,不能再落子。
检查合法性
dart
List<List<int>> flips = _getFlips(row, col, currentPlayer);
if (flips.isEmpty) return;
如果没有可翻转的棋子,这步棋不合法,不能落子。
黑白棋的规则是:每一步必须翻转至少一个对手的棋子。如果某个位置不能翻转任何棋子,就不能在那里落子。
放置棋子
dart
board[row][col] = currentPlayer;
在落子位置放上自己的棋子。currentPlayer是1(黑)或2(白)。
翻转棋子
dart
for (var f in flips) board[f[0]][f[1]] = currentPlayer;
把所有被夹住的棋子翻转成自己的颜色。
flips是一个坐标列表,每个元素是[row, col]。遍历这个列表,把每个位置的棋子改成currentPlayer。
这就是"翻转"的实现------把对手的棋子变成自己的。
切换玩家
dart
int next = 3 - currentPlayer;
if (_hasValidMove(next)) {
currentPlayer = next;
} else if (!_hasValidMove(currentPlayer)) {
gameOver = true;
_showResult();
}
});
}
落子后需要切换玩家,但黑白棋有特殊规则:如果对手没有合法落子位置,就跳过对手,当前玩家继续下。
计算下一个玩家
dart
int next = 3 - currentPlayer;
同样用3-player的技巧计算对手。
正常切换
dart
if (_hasValidMove(next)) {
currentPlayer = next;
}
如果对手有合法落子位置,切换到对手。
跳过
如果对手没有合法位置(_hasValidMove返回false),不切换,当前玩家继续下。
这是黑白棋的重要规则。有时候一方会连续下好几步,因为另一方一直没有合法位置。
游戏结束
dart
else if (!_hasValidMove(currentPlayer)) {
gameOver = true;
如果对手没有合法位置,而且当前玩家也没有合法位置,游戏结束。
这意味着双方都不能再下了,通常是棋盘满了或者剩余的空位都不能形成夹击。
检查是否有合法落子
dart
bool _hasValidMove(int player) {
for (int r = 0; r < size; r++) {
for (int c = 0; c < size; c++) {
if (_getFlips(r, c, player).isNotEmpty) return true;
}
}
return false;
}
遍历所有格子,只要有一个位置能翻转棋子,就有合法落子。
这个方法用于判断是否需要跳过玩家,以及游戏是否结束。
遍历64个格子,每个格子调用_getFlips检查。虽然看起来效率不高,但实际上很快,因为大部分格子要么已经有棋子,要么很快就能判断出不能翻转。
可落子位置提示
dart
bool canPlace = _getFlips(r, c, currentPlayer).isNotEmpty;
...
Widget _buildPiece(int value, bool canPlace) {
if (value == 0) {
return canPlace ? Container(width: 12, height: 12, decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.white30)) : const SizedBox();
}
空格子如果可以落子,显示一个半透明的小圆点提示。
这个提示很重要,黑白棋规则复杂,玩家不容易看出哪里能下。有了提示,玩家可以专注于策略,而不是花时间找合法位置。
提示点的样式
dart
Container(width: 12, height: 12, decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.white30))
12x12像素的小圆点,白色30%透明度。足够明显让玩家看到,又不会太抢眼影响棋盘的整体观感。
棋子渲染
dart
return Container(
width: 32, height: 32,
decoration: BoxDecoration(shape: BoxShape.circle, color: value == 1 ? Colors.black : Colors.white,
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 2, offset: const Offset(1, 1))]),
);
黑棋黑色,白棋白色,加阴影增加立体感。
尺寸
dart
width: 32, height: 32,
32x32像素的棋子,在8x8的棋盘上大小适中。
形状
dart
shape: BoxShape.circle,
圆形棋子,这是黑白棋的标准样式。
颜色
dart
color: value == 1 ? Colors.black : Colors.white,
value是1显示黑色,value是2显示白色。
阴影
dart
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 2, offset: const Offset(1, 1))]
阴影让棋子看起来像是浮在棋盘上,有立体感。blurRadius是模糊半径,offset是阴影偏移。
棋盘背景
dart
Container(
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(color: Colors.green[700], border: Border.all(color: Colors.black, width: 2)),
绿色背景,模拟经典黑白棋棋盘。
颜色选择
dart
color: Colors.green[700],
深绿色,这是黑白棋棋盘的经典颜色。和黑白棋子形成鲜明对比,看起来很舒服。
边框
dart
border: Border.all(color: Colors.black, width: 2),
2像素的黑色边框,让棋盘更有质感。
边距
dart
margin: const EdgeInsets.all(8),
8像素的外边距,让棋盘不会贴着屏幕边缘。
格子渲染
dart
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 8),
itemCount: 64,
itemBuilder: (_, index) {
int r = index ~/ 8, c = index % 8;
bool canPlace = _getFlips(r, c, currentPlayer).isNotEmpty;
return GestureDetector(
onTap: () => _placePiece(r, c),
child: Container(
decoration: BoxDecoration(border: Border.all(color: Colors.black26)),
child: Center(child: _buildPiece(board[r][c], canPlace)),
),
);
},
)
用GridView.builder构建8x8的网格。
坐标计算
dart
int r = index ~/ 8, c = index % 8;
index是0-63的一维索引,转换成二维坐标。~/是整除,%是取余。
点击处理
dart
onTap: () => _placePiece(r, c),
点击格子时调用_placePiece落子。
小结
这篇讲了黑白棋的落子翻转,核心知识点:
- 八方向搜索: dr和dc的组合遍历8个方向,覆盖所有可能的夹击方向
- 夹住判断: 连续对手棋子后遇到自己的棋子,形成夹击
- 翻转操作: 把被夹住的棋子改成自己的颜色
- 合法性检查: 必须能翻转至少一个棋子才能落子
- 跳过规则: 没有合法位置时跳过,对手继续
- 游戏结束: 双方都没有合法位置时结束
- 落子提示: 显示可落子位置,帮助玩家找到合法位置
- 3-player技巧: 用3减去当前玩家得到对手
翻转是黑白棋的核心,理解了这个,游戏的主要逻辑就清楚了。黑白棋的策略很深,角落和边缘的位置特别重要,因为它们不容易被翻转。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net