黑白棋的合法落点:一次扫描,八个方向
一个空位合法,当且仅当八个方向里至少有一个方向能夹住对方的棋子。判断与翻转用的是同一次扫描,不必先模拟再回滚。
黑白棋的规则一句话能说完:落子后,被夹住的两个自己棋子之间的对方棋子全部翻面。但把它写成代码时,最容易的走法是「先试落、扫描全盘、再回滚」。黑白棋 里的做法是另一种:判断与翻转共用一次扫描。
八个方向
从候选格出发,沿八个方向各走一遍。一个方向成立的条件是:先连续遇到对方棋子(至少一个),然后遇到自己的棋子:
const DIRECTIONS = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]];
function flipsFor(board, row, col, who, size) {
if (at(board, row, col, size) !== 0) return [];
const flipped = [];
for (const [dr, dc] of DIRECTIONS) {
const line = [];
let r = row + dr;
let c = col + dc;
while (inBounds(r, c, size) && at(board, r, c, size) === other(who)) {
line.push(index(r, c, size));
r += dr;
c += dc;
}
if (line.length && at(board, r, c, size) === who) flipped.push(...line);
}
return flipped;
}
line.length 那个判断是必须的 —— 相邻格就是自己的棋子时,中间没有夹住任何东西,不构成合法落点。
为什么不用先落子再回滚
模拟法要复制棋盘、落子、扫描、再丢弃副本,而且稍微写错就留下脏状态。这里一次遍历就同时得到「是否合法」与「要翻哪些子」,落子函数只需要把这些下标翻色:
const flipped = flipsFor(board, row, col, who, size);
if (!flipped.length) return null;
const next = board.slice();
next[index(row, col, size)] = who;
for (const i of flipped) next[i] = who;
复杂度不是问题,行动力才是
8 个方向 × 最多 6 步 = 每个空位几十次比较,8×8 的棋盘上完全可以暴力枚举:AI 的简单难度就是随机挑一个合法点。真正影响棋力的是评估函数:
| 因素 | 权重 |
|---|---|
| 角(永远翻不掉) | 120 |
| 角旁边的格(送给对方占角) | 负分 |
| 行动力(合法点数量) | 中盘最重要 |
| 翻转数量 | 收官阶段才重要 |
一个容易忽略的交互细节
双方都无合法点时游戏结束;一方无点而另一方有时,要跳过而不是结束。这件事属于流程而不属于规则,所以本站把它放在 nextTurn 里:它返回下一个该走的人和是否跳过,界面只负责显示「轮空」提示,不用自己判断规则。
规则代码越少越好:一次扫描能同时回答「合法吗」和「翻哪些」,就不要写两遍。

评论
…