リバーシの合法手:8 方向を 1 回の走査で
空きマスが合法なのは、8 方向のうち少なくとも 1 方向で相手を挟める場合です。判定と反転は同じ走査で済み、置いて戻すシミュレーションは要りません。
リバーシのルールは 1 文で書けます。石を置くと、新しい石と自分の石に挟まれた相手の石がすべて裏返る。これをコードにすると、置いてみて盤面を走査して戻す、という手順を書きたくなります。リバーシ の実装は別のやり方で、判定と反転を 1 回の走査で兼ねます。
8 方向
候補のマスから 8 方向へそれぞれ進みます。成立する条件は、まず相手の石が 1 つ以上連続し、そのあとに自分の石が現れることです。
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 の判定は必須です。隣が自分の石なら何も挟んでいないので、そのマスは合法ではありません。
置いて戻す方式にしない理由
シミュレーションは盤面の複製、配置、走査、破棄が必要で、少し間違えると汚れた状態が残ります。ここでは 1 回の走査で「合法か」と「どれが反転するか」が同時に得られるので、着手処理はその添字を塗り替えるだけです。
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 手で 1 マスあたり数十回の比較にしかならず、8×8 なら総当たりで十分です。AI の易しい難易度は合法手からランダムに選ぶだけです。強さを決めるのは評価関数です。
| 要素 | 重み |
|---|---|
| 角(二度と裏返らない) | 120 |
| 角の隣(相手に角を渡す) | 負の点 |
| 行動力(合法手の数) | 中盤で最も重要 |
| 反転数 | 終盤だけ重要 |
見落としやすい相互作用
ゲームは双方に合法手がないときに終わります。片方に無く、もう片方にある場合は終了ではなくパスです。パスはルールではなく進行なので、このサイトでは nextTurn に置いています。次に打つ側とパスの有無を返し、UI はパスの表示だけを担当してルールを判断しません。
ルールのコードは少ないほどよい。1 回の走査で「合法か」と「何が反転するか」が分かるなら、2 回書かない。

コメント
…