ボードゲームのコンピューター相手を作る:合法手・評価・先読み入門
遊びをつくる執筆にClaudeを使用しています。

「AIが生成した盤面に、一人で対戦できる相手を追加したい」という要望は自然です。しかし生成AIに「次の手は?」と聞くのとは根本的に違う仕組みが必要です。コンピューター対戦相手として機能するエンジンは、盤面を数値で評価し、候補手を先読みしてスコアを比べる——この三段階に尽きます。この記事では、オセロ(リバーシ)を具体例にしながら、合法手の列挙・評価関数・ミニマックス探索・アルファベータ枝刈りを順に整理します。考え方はチェスや将棋の簡易版など二人零和完全情報ゲーム全般に応用できます。本文はClaudeが執筆し、アソバース編集部が内容を確認しています。人間による取材やプレイテストは行っていません。
この記事の目次
「会話するAI」と「読むAI」はまったく別の仕組み
ChatGPTやClaudeのような生成AIは、大量のテキストから学習した確率的な言語モデルです。盤面を文字列で渡して次の手を尋ねることはできますが、その応答は「それらしい手」であり、全合法手を確実に列挙して最善を選んでいるわけではありません。対局の途中で非合法手を提案したり、同じ盤面で異なる答えを返すこともあります。
コンピューター対戦相手として機能するエンジンは、決定論的なアルゴリズムです。盤面の状態(どこに何の石が置かれているか)を入力として受け取り、ルールに基づいて合法手をすべて列挙し、それぞれの手を指した後の局面を評価し、先読みによって最もスコアが高い手を選んで返します。この三段階はどれを省いても「ちゃんとゲームになる相手」にはなりません。
生成AIをアシスタントとして使うことはできます。評価関数の重みの初期値を提案させたり、コードの骨格を一緒に書いたりする使い方は有効です。ただしエンジンの中核ロジック——合法手の生成とミニマックス探索——は、提案されたコードを自分で理解して検証するまで信用しないでください。
合法手を列挙する:エンジンの出発点
エンジンが最初に行う仕事は、「今この局面で、現在の手番のプレイヤーが置ける場所をすべて列挙する」ことです。オセロのルールでは、空のマスに石を置いたとき、上下左右斜め八方向のいずれかで相手の石を一枚以上はさんで自分の石で止まる手だけが合法手です。この条件を満たさないマスには置けません。
実装の定番は、各マスについて八方向を順にたどる単純なループです。以下は考え方を示す説明用コードです。実際の動作は自分のゲーム環境で検証してください。
// 説明用:一方向だけチェックする関数の骨格(動作保証なし)
function isLegalInDirection(board, row, col, dr, dc, myColor) {
const opp = myColor === 'B' ? 'W' : 'B';
let r = row + dr, c = col + dc;
let found = false;
while (inBounds(r, c) && board[r][c] === opp) {
found = true;
r += dr; c += dc;
}
return found && inBounds(r, c) && board[r][c] === myColor;
}
// 八方向すべてでこれを呼び、一つでも true なら合法手合法手がゼロの場合は「パス」です。両者ともパスになれば終局です。この終局判定も合法手列挙と同じ仕組みで実装できます。合法手の列挙が正しくなければ、評価も先読みもすべて無意味になるため、既知の局面(例:序盤の定石局面や終盤の単純局面)でテストして一致を確認しておくことを強くすすめます。
盤面の内部表現として「ビットボード」を使う実装もあります(8×8の盤面を64ビット整数二枚(黒用・白用)で表し、ビット演算で高速に合法手を計算する手法です)。小規模な実装では配列の二重ループで十分ですが、探索を深くしたい場合の最適化手段として知っておくと役立ちます。
評価関数:局面に点数をつける
評価関数は「今の局面がどちらにとってどれくらい有利か」を数値で返します。自分が有利なほど正の大きい値、相手が有利なほど負の大きい値を返す設計が一般的です。ゲームが終局していれば石数差や勝敗で正確に評価できますが、探索の途中では近似値を使います。
オセロの評価関数でよく使われる要素を三つ挙げます。
- コーナー:四隅は一度取れば絶対に返されません。評価値への寄与を大きく設定するのが定石です。
- 着手可能数(モビリティ):自分の合法手数から相手の合法手数を引いた差です。選択肢が多い方が有利で、序盤から中盤にかけて石数より重要なことが多いです。
- 確定石:今後絶対に返されない石(コーナーに隣接する辺の連続した石など)の差です。長期的な優位を反映します。
単純に石数の差だけを評価関数にすると、序盤に石を増やそうとして相手に選択肢を与え続ける弱いエンジンができやすいです。少なくとも序盤〜中盤はコーナーとモビリティを重視し、終盤(残り数十手程度)から石数差の比重を上げるのが実用的な出発点です。
各要素に掛ける重みはゲームによって異なります。初期値を決めたら、単純な相手と対戦させて明らかに弱い点を観察し、重みを調整するという繰り返しで育てていきます。「最適な重み」は理論だけでは導けないため、試行と観察が欠かせません。
ミニマックス探索:先読みを実装する
評価関数が用意できたら、先読みを実装します。ミニマックスアルゴリズムは「自分は評価値を最大化し、相手は評価値を最小化するように動くと仮定して、指定した深さまでゲーム木を展開し、最善手を見つける」手法です。二人零和完全情報ゲームのAI研究における古典的な基盤で、計算機科学の黎明期から知られています。
再帰的な構造を持ちます。現在の深さが0(探索の限界)に達したら評価関数を呼びます。それ以外は全合法手を試し、次の局面を再帰的に評価して、自分の手番なら最大値を、相手の手番なら最小値を返します。以下は骨格を示す説明用コードです。
// 説明用:ミニマックスの骨格(動作保証なし)
function minimax(board, depth, isMyTurn) {
if (depth === 0 || isTerminal(board)) {
return evaluate(board);
}
const moves = getLegalMoves(board, isMyTurn ? MY_COLOR : OPP_COLOR);
if (moves.length === 0) {
// パス:手番を交代して同じ深さで続ける
return minimax(board, depth, !isMyTurn);
}
let best = isMyTurn ? -Infinity : Infinity;
for (const move of moves) {
const next = applyMove(board, move, isMyTurn ? MY_COLOR : OPP_COLOR);
const score = minimax(next, depth - 1, !isMyTurn);
best = isMyTurn ? Math.max(best, score) : Math.min(best, score);
}
return best;
}この関数を深さ1〜3で動かすと、それぞれ一手・二手・三手先まで読むエンジンになります。深さが増えるほど強くなりますが、計算量は合法手数の深さ乗に比例して増加します。オセロの中盤では合法手が平均およそ10前後あるため、深さ4なら約10,000局面、深さ6なら約100万局面を評価することになります。
ネガマックス(Negamax)はミニマックスの変形で、「現在の手番プレイヤーから見た評価値を常に最大化する」という単一の視点で書けるため、コードが短くなります。max(a, b) = −min(−a, −b) という性質を利用したもので、動作はミニマックスと同等です。
アルファベータ枝刈り:同じ深さをより速く
アルファベータ枝刈りはミニマックスの探索量を削減する最適化です。結果には影響しません。「どのみち選ばれない手」の先を読むのをやめるだけで、同じ結果を出します。
仕組みはシンプルです。探索中に「自分の手番でのこれまでの最高値(α)」と「相手の手番でのこれまでの最低値(β)」を持ち歩きます。ある手を評価した結果が β 以上なら、相手はその手を選ばないためそれ以上その枝を深く読む必要はありません。逆に α 以下の場合も自分がその手を選ばないため読み飛ばせます。
枝刈りの効果は手の並び順に大きく左右されます。理論的には、最良の順序で手を試した場合に実効的な分岐因子を大幅に削減できることが知られており、同じ計算時間でより深い探索が可能になります。逆にランダムな順序では効果が小さくなります。コーナーや高評価マスへの手を優先的に並べるだけでも探索効率が改善します。これを「手順付け(move ordering)」と呼び、アルファベータ探索をさらに強化する実用的な追加施策です。
// 説明用:アルファベータ枝刈りを加えた骨格(動作保証なし)
function alphaBeta(board, depth, alpha, beta, isMyTurn) {
if (depth === 0 || isTerminal(board)) return evaluate(board);
const moves = getLegalMoves(board, isMyTurn ? MY_COLOR : OPP_COLOR);
if (moves.length === 0) return alphaBeta(board, depth, alpha, beta, !isMyTurn);
for (const move of moves) {
const next = applyMove(board, move, isMyTurn ? MY_COLOR : OPP_COLOR);
const score = alphaBeta(next, depth - 1, alpha, beta, !isMyTurn);
if (isMyTurn) {
alpha = Math.max(alpha, score);
} else {
beta = Math.min(beta, score);
}
if (beta <= alpha) break; // 枝刈り
}
return isMyTurn ? alpha : beta;
}
// 初期呼び出し: alphaBeta(board, depth, -Infinity, Infinity, true)ミニマックスで動作を確認してからアルファベータ枝刈りを追加し、同じ局面で同じ結果が出ることを確認してください。枝刈りのバグは結果に影響するため、既知の正解がある局面での検証が特に重要です。
難易度の設計と、動くエンジンで観察すること
探索の深さは難易度の主要な調整軸です。深さ1〜2は初心者向け、深さ3〜4は中級者が手ごたえを感じる目安です。ただし深さを増やすだけでは「強くなるほど遅くなる」問題が生じます。実際の難易度設計では次の手段を組み合わせます。
- 探索深さの段階:難易度ごとに深さを変える。最も直接的で理解しやすい。
- 確率的な選択:最善手ではなく上位n手からランダムに選ぶ。「ミスをする相手」を表現できる。
- 評価関数の簡略化:低難易度では石数差だけを評価するなど、意図的に弱い評価を使う。
エンジンを作ったら、既知の「正解がある局面」で動作を確認するのが近道です。オセロには終盤を完全読みできる局面(残り数十手以下など)があります。そこでエンジンの選択と期待される結果が一致するか確認することで、合法手生成と探索のバグを早期に発見できます。
プレイヤーとして体験しながらエンジンの判断を観察するのも有効な検証方法です。アソバースの潮目のオセロでは三段階の難易度を切り替えて一人対局ができます。ヒント機能を使いながら、自分が考えた手とエンジンが選ぶ手を比べると、評価関数の設計方針の違いが直感的に見えてきます。自分のエンジンを作る前に、動いている対戦相手の「選択の傾向」を観察しておくと設計の参考になります。
実装の優先順位
- 合法手の列挙と石の返し処理を正しく実装し、既知局面で検証する
- 深さ1のミニマックスで評価関数を単体で確認する
- 深さを3〜4まで増やし、レスポンス時間を計測する
- アルファベータ枝刈りを加え、同じ結果が出ることを確認してから性能を測定する
- 評価関数の重みを調整しながら難易度を段階化する
コンピューター対戦相手の実装は、ゲームのルールを精確に記述する作業でもあります。「置ける場所」「返る石」「終局条件」をプログラムに落とし込む過程で、自分のゲームのルールにあいまいな点が残っていないかが見えてきます。生成AIが作った盤面の見た目を、遊べる体験に変える核心的な一歩がここにあります。



