
2025/07/08 投稿
今回、私は20x20までのすべての長方形盤面について、先手必勝となる初手をすべて列挙するChompソルバーを開発しました。本記事では、効率的なメモ化と盤面のビット表現について解説します。
Chompの盤面状態は、左と上を揃えたフェラーズ図形として表現できます。ゲームの探索は、この盤面状態を頂点とするグラフ上の探索問題とみなせます。しかし、盤面のサイズが大きくなるにつれて状態の数は組み合わせ的に増加します。
探索の基本戦略として、メモ化を利用したNegamax法(αβ枝刈り込み) を採用しました。一度探索した局面の結果を保存し、再利用することで計算量を削減します。しかし、前述の通り、20x20盤面の状態をすべて記録するには、単純な方法ではメモリが不足します。この問題を解決したのが、本プログラムの核心であるメモ化の実装方法です。
20x20盤面の探索を実現するため、メモリ使用量を極限まで削減するメモ化テーブルを設計しました。キーとなるアイデアは「盤面状態の完全ハッシュ化」と「状態の2ビット化」です。
// src/ferrers_key.cpp より
uint128_t make_key_sub(const uint8_t *rows, uint8_t height)
{
// (抜粋)
const int L = N_GRID_SIZE + M_GRID_SIZE; // 40
int remT = L;
int remR = M_GRID_SIZE; // 20
uint128_t rank = 0;
// ... 盤面の形状に応じて二項係数を足し合わせ、ランクを計算 ...
return rank;
}
各局面の状態は「勝ち (Win)」「負け (Lose)」「未探索 (Unexplored)」の3種類です。これらを表現するには2ビットあれば十分です(例: 00: 未探索, 01: 勝ち, 10: 負け)。
// src/memo_table.cpp より
// キーに対応する2ビットの状態を取得
uint8_t get_state(uint128_t key)
{
uint128_t bit = key * STATE_BITS; // STATE_BITS = 2
uint128_t idx = bit >> 3; // 8で割ってバイト位置を計算
uint8_t off = bit & 7; // 8の剰余でビットオフセットを計算
return (memo[idx] >> off) & 0x3; // 該当する2ビットを読み出す
}
// キーに対応する2ビットの状態を設定
void set_state(uint128_t key, uint8_t v)
{
uint128_t bit = key * STATE_BITS;
uint128_t idx = bit >> 3;
uint8_t off = bit & 7;
memo[idx] &= ~(0x3u << off); // 対象の2ビットをクリア
memo[idx] |= ((v & 0x3u) << off); // 新しい値を書き込む
}
本プログラムを用いて20x20までの長方形盤面を解析した結果、この範囲にも先手必勝手が高々2つの初期盤面しか存在しないという事実が確認できました。多くの盤面で必勝手はただ1つでした。
本プロジェクトは、Chompという古典的なゲームの20x20盤面の探索を行いました。
ソースコードはGitHubで公開しています。個人の開発によるものであり、正確性は保証できません。誤りを発見された場合はぜひご報告ください。