相手の手を読む — ミニマックス
対戦相手は自分の損を狙ってくる。自分が一番得する手を選んでも、相手が最善で返してくれば、その得は奪われる。だから「自分が一番得する手」でなく「相手が最善で返してきても一番マシな手」を選ぶ。
ミニマックスは、二人零和(一方の得が他方の損になる)ゲームの基本定理。自分の手番では評価値を最大化し、相手の手番では最小化する、と交互に仮定して木を潜る。αβ枝刈りは、すでに見つけた手より相手が良くさせない枝を、最後まで読まずに切り捨てる。読む順番が良ければ実質的に探索の深さを倍にできる。チェスのディープ・ブルー(1997)がこの系譜に立つ。
三目並べくらい小さい盤なら、終局まで全部読める。盤の評価は単純で、先手が三つ並べば +1、後手が並べば -1、それ以外は 0。並んだ列があるかを調べるだけ。
// 盤は長さ9の配列。1=先手, -1=後手, 0=空。並べば ±1
const winner = (b) => {
for (const ln of lines) {
const s = b[ln[0]] + b[ln[1]] + b[ln[2]]
if (s === 3) return 1
if (s === -3) return -1
}
return 0
}空きマスに片っ端から石を置いてみて、その盤を相手の手番として再帰で評価する。返ってきた子の値を、自分の手番なら最大、相手の手番なら最小で集める。先手は +1 に寄せたいので最大化、後手は -1 に寄せたいので最小化。この最大化と最小化を交互に潜るのがミニマックス。
const minimax = (b, player) => {
const win = winner(b)
if (win !== 0) return win // 決着していれば即その値
// ... 盤が埋まっていれば引き分けで 0
let best = player === 1 ? -2 : 2
for (let i = 0; i < 9; i++) {
if (b[i] !== 0) continue
b[i] = player
const v = minimax(b, -player) // 相手の手番として子を評価
b[i] = 0 // 置いたのを戻す
// 自分の手番なら最大、相手の手番なら最小を取る
best = player === 1 ? Math.max(best, v) : Math.min(best, v)
}
return best
}置いては評価して戻す、を空きマスぶん繰り返す。葉(決着か満杯)まで降りた値が ±1 か 0 で、それを Math.max / Math.min で根まで持ち上げると、いま打つべき最善手が決まる。bestMove はこの一段目だけを回して、値が最良になるマスを覚える。
両者ともミニマックスで打つ三目並べの自動対局。完全読みなので、毎回引き分けに落ち着く。
塗りつぶしの先手と輪郭の後手が交互に置かれて、いつも引き分けで止まる。完全読みは「お互い間違えないなら勝てない」というゲームの結論を出す。三目並べは盤が小さいから根まで読み切れるが、手の数が広がると全部は読めない。実用では枝刈り(αβ法、明らかに劣る枝を読まずに捨てる)で枝を間引き、それでも足りない深さで止めて評価関数で打ち切る。