五目並べの勝敗判定:置いた 1 点だけ見れば足りる

着手ごとに盤面全体を走査するのは無駄です。勝敗は最新の 1 手でしか変わりません。置いた点から 4 方向へ伸ばして数えれば、盤面の大きさに依りません。

五目並べ の盤面を書き終えて最初に思いつく判定は全体走査です。各マスについて 4 方向に 5 連を探す。動きますが、着手のたびに走らせると O(盤面) のコストを最も熱い経路に置くことになります。

勝敗は最後の 1 手でしか変わらない

その手の前の盤面に 5 連はありません。あればゲームは終わっています。したがって新しい 5 連は必ず今置いた石を含みます。確認すべきはその 4 本の線だけです。

const DIRECTIONS = [[0, 1], [1, 0], [1, 1], [1, -1]];

function checkWin(board, row, col, size) {
  const who = at(board, row, col, size);
  if (!who) return null;
  for (const [dr, dc] of DIRECTIONS) {
    const cells = [index(row, col, size)];
    for (const sign of [1, -1]) {
      let r = row + dr * sign;
      let c = col + dc * sign;
      while (inBounds(r, c, size) && at(board, r, c, size) === who) {
        cells.push(index(r, c, size));
        r += dr * sign;
        c += dc * sign;
      }
    }
    if (cells.length >= 5) return { who, cells };
  }
  return null;
}

cells が連の座標も集めるので、UI で勝利の 5 石を強調するときに探し直す必要はありません。

コストは盤面の大きさに依らない

4 方向で、5 個目が見つかれば止まるので各方向およそ 4 手以内です。つまり定数時間。15×15 でも 19×19 でもループ回数はほぼ同じです。

書き方 1 手あたりのコスト
全体走査 O(size²)、15×15 で 225 マスから
置いた点だけ 4 方向それぞれ O(1)、盤面の大きさに依らない

端とルールの 2 つの判断

  • 盤外は「同じ色ではない」とみなす:別の判定を書かず、inBounds と色の比較を 1 つの条件にまとめれば、端が自然に伸長を止めます。
  • 長連を勝ちとするか:ルール次第で、禁じ手系では 6 連が負けになることもあります。このサイトは 5 個以上を勝ちとし、cells.length >= 5 に表現しています。ルールが 1 つの式になっていれば、変更も 1 か所です。

ビットボード版

15×15 の盤面を 64 ビット整数(1 行に数ビット)に詰めれば、判定はシフトと AND になります。盤面をある方向へ 1 つずらして元と AND する操作を 4 回繰り返し、残ったビットが 4 連です。popcount の記事 と同じ系統の考え方です。このサイトでは採用していません。225 マスは 64 ビットを 4 語に分ける必要があり、斜めが語の境界をまたぐため、この規模では複雑さに見合う利得がありません。

差分で判定するのは一般原則です。全体を計算し直す前に、今回の変化が何に影響しうるかを問う。

← 記事一覧に戻る

コメント

…