リバーシの合法手: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 回書かない。

← 記事一覧に戻る

コメント

…