闘獣棋エンジンで巣レースの評価項を試す
要点
- Mistboardの闘獣棋エンジンは巣レースという概念を持たないので、三つの方法で教えようとした。三つすべてが無効か悪化だった。
- 巣レースが勝敗を決めるのは出荷時の強さでは全対局の4.7%だけ(弱い探索でのテストでは22%と出た)。そして12倍深く探索しても負けを見抜くのは2手早くなるだけだ。
- 巣の近くでの探索延長は、ノード予算を固定するまでは+45 Eloに見えた。固定するとわずかにマイナスだった。専用の巣レース評価項は無効だった。
- 予算を固定しなければ、測っているのは予算だ。同じ対局から得た二つの定数、引き分けカウンタと引き分けコンテンプトが、代わりに決着局の割合を倍にした。
Mistboardの闘獣棋エンジンには巣レースという概念がない。局面の評価は駒の損得に、相手の巣へ前進した駒ごとのボーナスを加えたもので、自分の巣へ向かって走る駒が先に着くかどうかは一度も問わない。気づくのは探索が巣に到達したとき、およそ12手先だ。
これは明らかな穴に見えた。ボットが鼠を行ったり来たりさせている間に相手の鼠が巣に入った対局のあと、課題として登録した。埋めようと一日を費やした。三つの経路、すべて無効か悪化。以下は測定が示したことと、そこから残しておきたいことだ。
エンジンがすでにできていること
分けて述べる価値がある。これがその後のすべての枠組みを決めたからだ。エンジンは勝利条件を完全に理解している。巣への到達は詰みとして評価され、候補手の先頭に並べられ、静止探索でも探索される。欠けているのは先読みであって、理解ではない。
それでも勝てるのは、闘獣棋が駒の損得に支配されるゲームで、評価関数が損得を正確に読むからだ。巣に入る瞬間、勝者は中央値で20点リード、猫一匹ぶんほどだ。劣勢の側が勝つ対局は全体の4.7%しかない。
概念なしでどう指すか
スタディの第10局は4手で終わり、下のどの表よりもこの問題にうまく答えている。
闘獣棋の中央は三段ぶんの川だ。d列だけがそれを渡る唯一の陸橋になる。これらの対局が指された当時のMistboardのルールでは、虎は川を縦方向にのみ跳び越え、横方向には跳べなかったので、この回廊に立った虎はほとんど行き場がなかった。(2026年9月以降、中国のルールに合わせて虎も獅子と同じく横に跳ぶようになり、回廊はもはや虎にとって罠ではない。以下の手順は旧ルールの手順だ。)
20手目、青の手番。赤いマスは直前の手を示す。赤の虎がd6の狼を取り、いま回廊に立っている。青の象はその一つ上にいて、虎より格上だ。 20手目、d5の赤の虎には合法手が二つあった。前か後ろ、どちらも回廊の中だ。虎は前に出て狼を取り、青に40点の損を与え、虎は青の象の隣に居座った。
青の象は虎より格上なので、これは贈り物のように見える。青はすぐには取らなかった。その手の説明を求められたのだ。エンジンの理由は、虎が逃げられなかったことにある。象がd7にいれば虎の唯一の手はd5で、d5からの唯一の手はd4、自分を破る駒に一車線の回廊を追い下ろされる。取るのは、すでに手中にあるものを回収するために手番を一つ費やすことだ。青は得になる自由な手を指し、二手後にd6ではなくd5で虎を取った。どちらの手順でも駒の損得は同じになる。
対局を決めた4手:
41 Blue elephant d7 -> d6 steps into the corridor after the tiger
42 Red lion c3 -> c7 jumps into the square the elephant just left
43 Blue elephant d6 -> d5 takes the tiger, up a piece
Red's lion is now three moves from a den Blue cannot defend
虎は餌だった。犠打ではない。どうせほぼ捕まっていたし、狼を取る手は+10、ほぼすべての代替手が+6で、差は4点であって計画ではない。盤から消える途中でこの駒がやることは、獅子より格上の唯一の駒をd列に引き下ろすことだ。20手目の時点で獅子の跳躍は-195と評価される。c7がd7の青の象の隣にあるからだ。二手後には同じ跳躍が勝ちになる。象が去ったからだ。
青はこの手順の終わりに13点リードし、そして負ける。
計画を振り返ると、エンジンが巣レースを理解していたように聞こえる。していない。この評価関数のどこにも巣レースの項はない。このプロジェクトの前も後もだ。500万ノードを探索して、この手順が出てきた。それが結論のすべてだ。課題が組み込みたかった振る舞いはすでにそこにあり、知識ではなく深さが生み出していた。
基礎率:全対局の4.7%、しかも出荷時の強さで測れ
本番の予算である1手500万ノードでの自己対局172局。
| 終局の種類 | 割合 |
|---|---|
| 千日手による引き分け | 52.9% |
| 巣への侵入 | 26.2% |
| 進展なしによる引き分け | 20.9% |
| 真のレース(勝者が10手前の時点で互角か劣勢) | 4.7% |
同じハーネスを20万ノードで走らせるとレースは22%と出る。弱い指し手がこの数字をおよそ5倍に膨らませる。安い測定は手元にあったが、より多くの作業を正当化する方向へ5倍間違っていたことになる。
伸びしろ:12倍深い探索で稼げるのは2手
各レースについて、敗者の対局を二分探索し、それぞれの予算が必敗を認識する最も早い手数を求め、その差を取った。
| 差 | レース数 |
|---|---|
| 0手 | 4 |
| 2手 | 3 |
| 4手 | 1 |
500万ノードに対して6000万ノード、それで深い探索が負けを見抜くのは中央値で2手早いだけだ。その地平線より前では両者はほぼ一致し、最後の1、2手でようやく分かれる。
これがプロジェクトを終わらせるべき数字だった。探索を12倍にして取り戻せるのが2手なら、その情報は賢い評価関数が掘り出すのを待ってそこに眠っているわけではない。
経路A:探索延長、無料版と有料版
実装せずに探った。ハーネス側の方針として、相手の巣からR マス以内に駒がいるときは一方に大きなノード予算を与える。これはあらゆる現実の延長に対する厳密な上限になる。
Eloを測る前の最初の発見:発動条件は疎ではない。
| 半径 | 発動する手の割合 |
|---|---|
| 2 | 23% |
| 3 | 43% |
| 4 | 72% |
闘獣棋の盤は小さく、巣は最終段に置かれている。レースを早く見るために必要な半径R=4では、延長は単に「どこでも12倍探索する」ことになり、外科的な施策という枠組みは最初から間違っていた。
R=2、各条件につき先後を入れ替えた300ペア:
| 条件 | Elo | 対照に対するノード数 |
|---|---|---|
| 巣の近くで12倍の深さ、無料で付与 | +45 | 3.3倍 |
| 予算中立、基本370万+巣の近くで1000万 | -15 | 1.04倍 |
| 予算中立、基本110万+巣の近くで2000万 | -12 | 0.98倍 |
同じ着想、同じコード、符号が逆。エンジンの500万ノードはプレイヤーが待てる時間で固定されているので、現実の延長は深さを生み出せない。探索の残りの部分から奪うのだ。無料の条件が測っていたのは追加の計算量だった。巣とは何の関係もない。
予算中立の条件を走らせたのは、+45が良すぎて見えたからだ。そこで止めていれば、ゼロよりわずかに価値の低い機能のために一週間を買っていたことになる。
どちらの条件も半日で済んだ。どちらもエンジンに手を入れる必要がなかったからだ。延長は、盤面の条件に応じてノード予算を変えるハーネス側の方針だった。「Xについてもっと賢くしろ」という形の提案には、まず完璧なXを無料で与えてみるのが、Xがそもそも重要かどうかを知る最も安い方法だ。
経路B:評価項そのもの
それでも作った。延長が失敗した理由は評価項には当てはまらないからだ。延長はノードを食うが、評価項は葉ごとにほとんど何も食わない。
当時の評価関数はこうだった。盤を一度ループし、各駒が独立に寄与する:
for idx in 0..N {
let v = VAL[role_of(code) as usize];
let friendly = color_of(code) == me;
s += if friendly { 2 * v } else { -2 * v };
// Advancement toward the relevant den: the enemy's for our pieces, ours for theirs.
let target = if friendly { enemy_den } else { own_den };
let dist = manhattan(idx as u8, target);
let advance = if dist <= 1 { 400 } else { (16 - dist) * 3 };
s += if friendly { advance } else { -advance };
}
この前進項こそ評価関数がレースに盲目な理由で、原因は対称性だ。自分の走者が相手の巣から3マスなら+39。相手の走者が自分の巣から3マスなら-39。両者が全力で走ればゼロ近くの数字になり、最も勝敗が決まっているまさにその瞬間に局面は静かだと読まれる。
だからこの項は、駒ごとのもう一つのボーナスにはできない。一度だけ計算し、二人の走者を互いに比較する、片側につき一つの判定でなければならない:
for side in [me, 1 - me] {
// Nearest enemy runner to this side's den, and this side's nearest defender
// to its own trap ring.
let (mut att, mut def) = (99, 99);
for idx in 0..N { /* ...scan once... */ }
if att > RACE_RADIUS { continue; } // no race here: contribute nothing
// The side to move gets one free step in the race for its own den.
let margin = att - def + i32::from(side == me);
let pen = if margin <= 0 { RACE_LOST }
else if margin == 1 { RACE_TIGHT }
else { 0 };
score += if side == me { -pen } else { pen };
}
守る側は巣ではなく罠のリングまでの距離で測る。それが正しい迎撃地帯なのは、巣の三つの隣接マスがそのまま三つの罠であり、罠に立った駒は格に関係なくどの守備駒にも取られるからだ。自分の巣に踏み込む象は、自分の鼠に取られるマスを越えなければならない。
この項は、想定された局面の17%で選ぶ手を変え、同ノード数で実時間を約17%増やす。各重みにつき先後を入れ替えた300ペア、このコストを差し引く前の結果:
| 重み | Elo |
|---|---|
| 10 | -3 |
| 25 | +9 |
無効、しかも甘めに見た方向で。
残しておきたいこと
予算を一定に保て。さもなければ測っているのは予算だ。実験の無料版は作るのが手軽で、自信に満ちた正の数字を渡してくる。その変更が何かから奪わねばならないなら、実際に奪って、結果が生き残るかどうかを見ろ。
この罠には同じ日に二度かかった。同じ午後のコンテンプト値の走査では決着局が32%から50%に上がったが、これは構造上コンテンプトを上げれば起きることだ。自己対局の走査では両者が同じ設定を受け取るからだ。それが何かを犠牲にしているかを言えるのは、二つの値を向かい合わせたときだけだった。犠牲はなかったので、これは出荷した。
評価関数の穴は実在し、診断も正しかった。ただ、その後ろに何もなかった。同じ対局群から見つかった二つの定数、対局を早く終わらせていた無捕獲の引き分けカウンタと、ほぼゼロの引き分けコンテンプトが、闘獣棋の勝負決着率を4分の1から約半分に引き上げた。どちらもエンジンに何も教えていない。課題が求めたのは理解だった。理解は最初から制約ではなかった。
対局は公開されているし、上の局面も公開されている。解析盤は同じエンジンで動くので、青は虎を取るべきだったと思うなら、その手順を実際に指させられる。