五目並べの勝敗判定:置いた 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 語に分ける必要があり、斜めが語の境界をまたぐため、この規模では複雑さに見合う利得がありません。
差分で判定するのは一般原則です。全体を計算し直す前に、今回の変化が何に影響しうるかを問う。

コメント
…