黑白棋的合法落点:一次扫描,八个方向

一个空位合法,当且仅当八个方向里至少有一个方向能夹住对方的棋子。判断与翻转用的是同一次扫描,不必先模拟再回滚。

黑白棋的规则一句话能说完:落子后,被夹住的两个自己棋子之间的对方棋子全部翻面。但把它写成代码时,最容易的走法是「先试落、扫描全盘、再回滚」。黑白棋 里的做法是另一种:判断与翻转共用一次扫描。

八个方向

从候选格出发,沿八个方向各走一遍。一个方向成立的条件是:先连续遇到对方棋子(至少一个),然后遇到自己的棋子:

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 里:它返回下一个该走的人和是否跳过,界面只负责显示「轮空」提示,不用自己判断规则。

规则代码越少越好:一次扫描能同时回答「合法吗」和「翻哪些」,就不要写两遍。

← 返回文章列表

评论

…