標準の検索アルゴリズム(ミニマックス、アルファベータ剪定、反復深化など)を使用してConnect Fourを再生するエンジンを作成しました。エンジンが別の移動順序で到達した同じ位置をすぐに評価できるように、また移動順序を可能にするために、転置テーブルを実装しました。
TTの問題は、反復深化プロセスの各ステップで、使用するバイト量が少なくとも2倍になることです。TTストア特定の位置の重要な情報を表すオブジェクト。これらの各オブジェクトは288バイトです。以下では、深さ制限は、エンジンが反復深化の各ステップで検索する距離です。
深さの制限= 1-TTサイズ= 288バイト(1つのノード/位置だけが見られたため)。
深さ制限= 2-TTサイズ= 972バイト。
深さ制限= 3-TTサイズ= 3708バイト。
深さ制限= 4-TTサイズ= 11664バイト
深度制限= 5-TTサイズ= 28476バイト。
....
深さ制限= 12-TTサイズ= 11,010,960バイト。
深さ制限= 13-TTサイズ= 22,645,728バイト。
そして、この時点で.exeファイルがクラッシュします。
この問題に対して何ができるのだろう。コネクトフォーでは、分岐係数は7です(通常、少なくともゲームの開始時に、7つの可能な動きがあるため)。 TTが各ステップで7倍に成長しない理由は、私が実装したプルーニングメソッドが原因です。
エンジンが深さ8〜9までしか検索しない場合、この問題は大した問題ではありませんが、私の目標は、最後まで(つまり、深さの制限= 42)、Connect Fourに反論することです。
あなたが根本的に持っているのは、高価な関数の計算値のためのcacheです。このようなスキームでは常にそうであるように、ハッシュテーブルのエントリに対してエビクションポリシーを決定する必要があります。どちらを選択するかは、検索の実際の構造に依存します(明らかにキャッシュヒットを最大化する必要があります)が、最近使用されていない(またはLRU)から始めるのが適切です。 FIFOそして無作為化戦略も一般的です。
LRUの全体的な考え方はその名の通りです。キャッシュ(転置テーブル)のメモリが不足すると、最も古いエントリを新しいエントリで上書きします。アルファベータ検索のコンテキストでは、最後に計算された上限のみをテーブルに格納すると、検索を効果的に維持しながら、検索を効率的にプルーニングし、ストレージと再計算のバランスを保つことができます。
また、ボードのより良い表現が存在します。 49ビット(7バイトパック)でそれを行うことができます。方法は次のとおりです。列の高さは6セルです。各列をsevenビットで表し、最初のビットが列の上のスペースになるようにします。最初の空のスペースを1でマークし、その前のスペースを0にします。残りのビットは、黒または赤(それぞれ)に対して0または1です。したがって、7つの列のそれぞれを7ビットで明確に表すことができ、ボード全体(7列)を49ビットで表すことができます。
以下に示す10バイトのソリューションとは異なり、これらのビットは64ビットCPUのレジスタに収まります。したがって、平等とハッシュ演算は簡単で非常に安価に計算できます。
状態
ボードを見ると、288バイトはものすごく大きいです。
標準的なボードは7x6タイルで、各タイルは3つの状態(空、赤、黄色)のいずれかにあります。
単純なビットパッキングアプローチを採用すると、各タイルを2ビットで表すことができます。42のタイルがあり、ゲームの状態を84ビットまたは10バイトで表すことができます。
不正なゲームは、各タイルを4番目の状態に設定することで表すことができます(2ビットは状態00
、01
、10
、11
)。私が選ぶ:
enum {
empty = 0x00,
yellow = 0x01,
red = 0x02,
error = 0x03
}
ビット比較により、ゲームの不正状態をすばやくチェックできます。
byte[8] game_state;
for (int i = 0; i != 10; ++i)
if (0 != ((game_state[i] & 0x55) & ((game_state[i] & 0xAA) >> 1))
return error; //one of the tiles in the game is illegal...
return good;
同様に、プレーヤーのターンは、答えが0の場合は黄色のプレーヤータイルモジュロ2(xor各ビット)の数を数えることで推定できます。答えが0の場合は黄色のターン、それ以外の場合は赤です。
byte[10] game_state;
byte bcount = (game_state[0] & 0x55)
^ (game_state[1] & 0x55)
^ (game_state[2] & 0x55)
^ (game_state[3] & 0x55)
^ (game_state[4] & 0x55)
^ (game_state[5] & 0x55)
^ (game_state[6] & 0x55)
^ (game_state[7] & 0x55)
^ (game_state[8] & 0x55)
^ (game_state[9] & 0x55)
;
bcount = ((bcount & 0x50) >> 4) ^ (bcount & 0x05));
return ((bcount & 0x04) >> 2) == (bcount & 0x01))
? yellow_turn
: red_turn
;
各状態には7つの動きがあり、法的(タイルはエラーではない)と違法(タイルはエラーです)の間で混在しているため、決定木のノードは8つのゲーム状態で表すことができます。最初の状態は開始条件であり、他の7つはi番目の列にトークンを配置するための移動を表し、10バイトの各状態は次の決定ノードのキーです(エラーがない限り)。これにより、決定ノードごとに合計80バイトが得られます。
スペース
ゲームの状態空間は巨大です。
勝利によるゲームの終了を無視すると、ゲームの状態空間は(Ex=0..6(2^x))^7
。つまり、532875860165503であり、決定ノードのサイズが80バイトの場合、最大で38.771 PetaBytes。広範囲にわたるフィルタリングを使用しても、対処するのは困難です。
ゲームについての知識を深めることで、おそらくその上限に君臨することができます。重要なのは、メインメモリまたはメモリ期間さえも利用できるコンピュータシステムがないことです- 私たちのほとんどは、現在、死すべき者が現在アクセスできる です。
ページング
ハードドライブのスペースがある場合は、B-TreeまたはB * -Treeが役に立ちます。できれば、ファイルページノードを提供するサードパーティライブラリを見つけて、必要な場合にのみ作成してください。
重要なのは、決定ノードのベースとなる10バイトのゲーム状態です。バイナリソートで十分です。
Bツリーが純粋なマップとしてスタイル設定されている場合は、開始状態(キー)とその状態の結果を格納する必要がない場合があります。
このデータ構造を使用すると、多くのゲーム状態をキャッシュできるはずです。残念ながらこれはおそらくあなたのニーズには不十分です。
状態パターンと圧縮
ゲームには単調なボードがありますが、移動を行う場合、唯一の興味深い状態は勝利ラインを作るための候補であるタイルです。 error
コードの名前をignored
に変更して、興味のないタイルにマークを付けることができます。すべてのタイルがignored
とマークされているゲームは、定義では勝てません。これは、ゲームの状態空間をさらに圧縮するように機能し、ゲームの状態のオーバーラップを増やして、それ以上の計算を減らすという副次的な利点があります。
経験則
ヒューリスティックを検討して、パスを勝ち取る可能性が最も高いものを貪欲に選択してみてください。勝利に直接寄与する動き、または対抗敗北を最初に検討する必要があります。不合理に行動する他のプレイヤーに依存する動きは遅らせるべきです。これにより、幅と深さの両方の調査が促進されます。
また、制限された、忘れっぽい優先キューについても検討してください。最も興味深い次のゲームの状態のみがキューに残ります。キューが枯渇した場合、または状態の品質が忘れられた最良の状態を下回った場合は、Bツリーウォークから再構成することを検討してください。簡単にするために、キューを更新するために簡単に再処理できるログに未処理のゲーム状態を忘れてください。
最後に、完全なディシジョンツリーを格納するのをやめます。ルックアサイドキャッシュを使用して、忘れられた状態を再生成します。