オセロAIの作り方|HTML1枚で遊べるコードと、読む深さ・採点で強さがどう変わるか

オセロのAIは、先の手を何手か読んで、盤面に点をつけ、いちばん点の高い手を選ぶ形で作れます。この記事のコードは、HTML1枚で遊べるオセロで、αβ法で読む手を絞り、場所ごとの重みで盤面を採点します。人が黒、AIが白です。
コードはNodeで関数をそのまま呼んで測りました。読む深さごとの調べる局面の数、でたらめな相手との対戦、深さや評価のしかたを変えたAI同士の対戦を、すべて実際に数えた結果です。
この記事で分かること
- オセロAIのコード全体(合法手・αβ法の探索・場所の重みの採点)
- 深さ1〜6で、調べる局面がどう増えるか(実測)
- 深く読むAIが、浅いAIにどれだけ勝つか(100局ずつ)
- 採点を「場所の重み」にした効果(石の数だけで採点するAIとの対戦)
- 確かめられていないこと
まるばつのように、盤面が小さいゲームのAIは、AIと対戦するゲームの作り方で扱っています。オセロは盤面が大きく、すべての手を終局まで読めないので、先を読む深さを区切ります。
この記事の目次
オセロAIが考える3つの部品

コード全体
次のコードを、index.htmlという名前で保存してブラウザで開くと、そのまま遊べます。サーバーは要りません。黒の置ける場所には、小さな点が出ます。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>オセロ(AIと対戦)</title>
<style>
body{font:17px/1.6 sans-serif;text-align:center}
#board{display:grid;grid-template-columns:repeat(8,44px);gap:2px;justify-content:center;background:#063;padding:4px;width:fit-content;margin:12px auto}
#board button{width:44px;height:44px;border:0;background:#2a7;font-size:28px;line-height:1;padding:0}
</style>
</head>
<body>
<h1>オセロ</h1>
<p>あなたは黒(●)、AIは白(○)です。</p>
<div id="board"></div>
<p id="msg"></p>
<script>
// 盤面は64個の配列。1=黒、-1=白、0=空き
const DIRS = [-9, -8, -7, -1, 1, 7, 8, 9];
const WEIGHT = [ // 角は取りたい(高い点)。角の隣は取りたくない(低い点)
100, -20, 10, 5, 5, 10, -20, 100,
-20, -50, -2, -2, -2, -2, -50, -20,
10, -2, 1, 1, 1, 1, -2, 10,
5, -2, 1, 0, 0, 1, -2, 5,
5, -2, 1, 0, 0, 1, -2, 5,
10, -2, 1, 1, 1, 1, -2, 10,
-20, -50, -2, -2, -2, -2, -50, -20,
100, -20, 10, 5, 5, 10, -20, 100];
function newBoard() { const b = Array(64).fill(0); b[27] = b[36] = -1; b[28] = b[35] = 1; return b; }
// その手で裏返せる石の位置を返す(置けないときは空の配列)
function flips(b, pos, me) {
if (b[pos] !== 0) return [];
const x = pos % 8, out = [];
for (const d of DIRS) {
const line = [];
let p = pos + d, px = x;
for (;;) {
const nx = p % 8;
if (p < 0 || p > 63 || Math.abs(nx - px) > 1) break; // 盤の端を越えない
if (b[p] === -me) { line.push(p); px = nx; p += d; continue; }
if (b[p] === me && line.length) out.push(...line);
break;
}
}
return out;
}
function moves(b, me) { const m = []; for (let i = 0; i < 64; i++) if (flips(b, i, me).length) m.push(i); return m; }
function play(b, pos, me) { const n = b.slice(); n[pos] = me; for (const f of flips(b, pos, me)) n[f] = me; return n; }
// 評価: 場所の重みの合計(石の数だけで評価する版も比べるため、切り替えられる)
function evaluate(b, me, mode) {
let s = 0;
for (let i = 0; i < 64; i++) s += b[i] * (mode === 'count' ? 1 : WEIGHT[i]);
return s * me;
}
let nodes = 0;
// αβ法つきの探索。depth手先まで読んで、meにとっての評価を返す
function search(b, me, depth, alpha, beta, mode) {
nodes++;
const mv = moves(b, me);
if (depth === 0) return evaluate(b, me, mode);
if (!mv.length) {
if (!moves(b, -me).length) return evaluate(b, me, mode) * 100; // 両者とも置けない=終局
return -search(b, -me, depth - 1, -beta, -alpha, mode); // パス
}
for (const p of mv) {
const s = -search(play(b, p, me), -me, depth - 1, -beta, -alpha, mode);
if (s > alpha) alpha = s;
if (alpha >= beta) break;
}
return alpha;
}
function bestMove(b, me, depth, mode = 'weight') {
let best = -Infinity, pick = null;
for (const p of moves(b, me)) {
const s = -search(play(b, p, me), -me, depth - 1, -Infinity, Infinity, mode);
if (s > best) { best = s; pick = p; }
}
return pick;
}
if (typeof document !== 'undefined') {
let board = newBoard();
const el = document.getElementById('board'), msg = document.getElementById('msg');
function draw() {
el.innerHTML = '';
const legal = new Set(moves(board, 1));
board.forEach((v, i) => {
const b = document.createElement('button');
b.textContent = v === 1 ? '●' : v === -1 ? '○' : legal.has(i) ? '·' : '';
b.style.color = v === 1 ? '#000' : '#fff';
b.onclick = () => humanMove(i);
el.appendChild(b);
});
const c = board.filter((v) => v === 1).length, w = board.filter((v) => v === -1).length;
msg.textContent = `黒 ${c} - 白 ${w}`;
}
function aiTurn() {
while (true) {
const m = moves(board, -1);
if (!m.length) break;
board = play(board, bestMove(board, -1, 4), -1);
if (moves(board, 1).length) break; // 人が置けるなら交代
}
draw();
}
function humanMove(i) {
if (!flips(board, i, 1).length) return;
board = play(board, i, 1);
setTimeout(aiTurn, 50);
draw();
}
draw();
}
</script>
</body>
</html>

画面は、人が黒で1手打ち、白のAIが1手返した直後です。動作の確認として、初期配置からの黒の合法手が4つ(19・26・37・44)になること、60手で盤面が64個の石で埋まることも確かめました。
先を読む深さと、調べる局面の数
探索は、αβ法で枝を省くので、読む深さを1つ増やしたときの局面の増え方が、実際にどれくらいかを測りました。最初の局面と、でたらめに20手進めた中盤の局面で、1手を決めるまでに調べた局面を数えています。

中盤では、深さ4で11,436局面(約54ミリ秒)、深さ6で395,059局面(約1.5秒)でした。最初の局面は、置ける場所が少ないので、深さ6でも2,685局面(約14ミリ秒)です。中盤の1.5秒は、1手ごとなので、深さ6では、遊んでいて待たされる長さになります。この記事のコードは、深さ4(約54ミリ秒)にしています。
深く読むと、強くなるか
AI同士や、でたらめに打つ相手と、100局ずつ戦わせました。AIは、黒と白を半分ずつ受け持ちます。AI同士の対戦では、最初の6手をでたらめにして、同じ対局が繰り返されないようにしました。

でたらめな相手には、深さの差が出なかった
でたらめな相手との対戦は、深さ1で86勝3分11敗、深さ4で87勝4分9敗でした。深さ2と3も、82〜83勝です。読む深さを増やしても、でたらめな相手に対しては、勝率がほとんど変わりませんでした。理由は確かめていません。
AI同士では、深い側が勝った
深さが違うAI同士では、深い側が勝ち越しました。深さ2対深さ1は83勝2分15敗、深さ4対深さ1は95勝2分3敗、深さ4対深さ2は82勝0分18敗でした。でたらめな相手では見えなかった強さの差が、AI同士の対戦では出ています。
場所の重みの効果
同じ深さ3で、盤面の採点だけを変えました。場所の重みで採点するAIと、石の数だけで採点するAIが100局戦って、場所の重み版は78勝4分18敗でした。角を取りたい、角の隣は取りたくない、という場所の重みが、石の数だけの採点より大きく効いています。
確かめられていないこと
- 人が遊んだときの強さと面白さ勝敗は、AIとでたらめな相手、AI同士の対戦で測っています。人との対戦は、確かめていません。
- でたらめな相手に深さの差が出なかった理由
- 終盤の読み切り(残りの手が少ないとき、終局まで読む方法)
- 場所の重みの数字の最適化(この記事の表は、1つの例です)
- スマートフォンでの表示と操作(盤面は幅44pxのマスで作っています)
- 深さ6以上の対戦の強さ(時間がかかるため、調べる局面の数だけ測りました)
確かめていないものは、動かして確かめてから、別の記事で扱います。
うまくいかないとき
| 症状 | 確認すること |
|---|---|
| 置けない場所に置けてしまう | 合法手の判定(flips)で、盤の端を越えて数えていないかを確かめます。左右の端では、横の位置が2つ以上飛ぶと、別の行へ回り込みます |
| AIの考えが遅い | 読む深さ(bestMoveの第3引数)を下げます。この記事の測定では、中盤の深さ6で約1.5秒でした |
| パスができない | 置ける場所がなくなったとき、相手に手番を渡す処理があるかを確かめます。コードでは、aiTurnのwhileがそれを担当します |
| AIが角の隣ばかり打つ | 場所の重みの符号を確かめます。角は大きなプラス、角の隣はマイナスです |
関連記事AIと対戦するゲームの作り方|負けないまるばつAI(ミニマックス法)と弱くする方法
よくある疑問
AIの強さを調整するには、どうしますか
読む深さを変えるのが簡単です。深さ1と深さ4では、AI同士で95勝2分3敗の差がありました。人が遊んだときの感じ方は、確かめていません。
ChatGPTのようなAIに、オセロを打たせることはできますか
この記事では扱っていません。この記事のAIは、読む深さと点数で手を選ぶ仕組みです。
将棋や囲碁にも使えますか
考え方は同じですが、手の数が非常に多いので、この記事のコードのままでは動かせません。確かめていません。
この記事のまとめ
この記事のまとめ
- オセロAIは、合法手・先読み(αβ法)・場所の重みの採点で作れる
- 調べる局面は、中盤の深さ4で約1.1万、深さ6で約39.5万。深さ4は約54ミリ秒だった
- 深さの違うAI同士では、深い側が勝ち越した(深さ4対深さ1で95勝2分3敗)
- 場所の重みの採点は、石の数だけの採点に、100局で78勝4分18敗だった
- 人が遊んだときの強さと、でたらめな相手に差が出なかった理由は、確かめていない












