Skip to content
[OPEN_POKER]

ポーカーボットの高度なアーキテクチャ:エクイティとテスト

JJoão Carvalho||19 min read

高度なポーカーボットは、信頼できるイベントループを計測可能な意思決定システムへ変えます。アップグレードとは「AIを追加する」ことではありません。不変な判断スナップショット、明示的なポーカー特徴量、レンジを考慮したエクイティ、再現可能なハンド、ポリシーの変化を証明できる実験ループを導入することです。基本ボットの合法アクションガードは維持し、より高度な機能はすべてその内側に置きます。

ポーカーボット・アーキテクチャシリーズ: 全3回の第2回です。ランタイムがまだアクションを拒否されるなら、ポーカーボットの基本アーキテクチャから始めてください。次はポーカーボットのプロ向けアーキテクチャです。運用方法の比較には2026年のポーカーボット費用もご覧ください。

高度なポーカーボットのアーキテクチャでは何が変わる?

高度なポーカーボットは、事実、推定、ポリシー、実行を分離します。事実は最新のサーバーターンから得られるポット、ボード、スタック、合法アクション、ハンドID、トークンです。推定にはエクイティと対戦相手レンジが含まれます。ポリシーは両方を使って意図を返し、実行層は最後にもう一度、その意図を事実と照合します。

推定は間違ってもよいからこそ、この区別が重要です。対戦相手モデルが狭すぎるレンジを割り当てることも、モンテカルロ計算にサンプリング誤差が生じることもあります。どちらの誤りも、不正なレイズや期限切れターンへの応答を発生させてはいけません。内側のポリシーが確率的になっても、外側のシェルは決定的なままです。

データ種別信頼度所有者
サーバー事実pot = 180、レイズ上限1,640正式な値プロトコルアダプター
導出事実ポットオッズ、実効スタック、ポジション入力が正しければ厳密特徴量レイヤー
推定ショーダウンエクイティ43%サンプリングまたはモデルによるエクイティサービス
信念対戦相手のボタンオープン率31%不確実対戦相手モデル
意図エクイティが必要値を超えるためコールポリシー出力戦略

これらを型のない1つの辞書に平坦化しないでください。信頼度と所有者が消えると、推測したレンジがサーバーのレイズ上限と同じくらい信頼できるように見えてしまいます。

不変な判断スナップショットはどう機能すべき?

不変な判断スナップショットとは、1回のyour_turnから取得した完全な読み取り専用入力です。エクイティ計算やモデル処理中に、後から届いたネットワークイベントがデータを変えるのを防ぎます。また、テストでリプレイする単位にもなります。

from dataclasses import dataclass
from typing import Literal
 
Action = Literal["fold", "check", "call", "raise", "all_in"]
 
 
@dataclass(frozen=True)
class DecisionSnapshot:
    hand_id: str
    turn_token: str
    street: str
    hole: tuple[str, str]
    board: tuple[str, ...]
    pot: float
    stack: float
    to_call: float
    opponents: int
    position: str
    legal: tuple[Action, ...]
    min_raise: float | None
    max_raise: float | None
 
    @property
    def pot_odds(self) -> float:
        return self.to_call / (self.pot + self.to_call) if self.to_call else 0.0
 
    @property
    def stack_to_pot(self) -> float:
        return self.stack / self.pot if self.pot else float("inf")

ターン到着時に同期的にスナップショットを構築し、非同期の判断タスクへ渡します。結果を送る前に、そのhand_idturn_tokenをアクティブなターンと再度比較してください。Open Pokerのトークンはアクション後に消費され、新しいyour_turnが来るたびに前のトークンは無効になります。そのため古い処理結果は再試行せず破棄します。

メッセージタイプのリファレンスに通信フィールドが記載されています。再接続後のtable_stateにあるプレイヤー固有のheroセクションにはホールカードと現在の合法アクションが含まれるため、正規化層はこれも受け入れる必要があります。

エクイティとポットオッズをどう判断へ変換する?

エクイティは想定レンジに対してショーダウンでポットを獲得する頻度を示し、ポットオッズはコールの損益分岐に必要な割合を示します。将来のベッティング、レンジ誤差、トーナメント目標による調整前では、推定エクイティがcall / (pot + call)を上回ればコールのチップ期待値はプラスです。

ポットが180、コールに60必要なら、ポットオッズは60 / 240 = 25%です。エクイティ推定38%は、この素のしきい値を13ポイント上回ります。1ポイントの優位性ですべてコールしてはいけません。モンテカルロ分散、不正確な対戦相手レンジ、今後のアクションで薄い差は消えます。明示的な安全マージンを設け、ログに記録します。

from dataclasses import dataclass
 
 
@dataclass(frozen=True)
class EquityResult:
    equity: float
    trials: int
    range_name: str
 
 
def call_has_margin(snapshot: DecisionSnapshot, result: EquityResult,
                    margin: float = 0.03) -> bool:
    return (
        "call" in snapshot.legal
        and result.equity >= snapshot.pot_odds + margin
    )

モンテカルロ・エクイティ計算機ではPython実装を紹介しています。保守されている評価プリミティブを使うなら、戦略プロジェクト中にハンド評価器を自作するより、PokerKitの評価器ドキュメントから始めるほうが堅実です。

対戦相手モデルは不確実性をどう表現すべき?

対戦相手モデルには、「アグレッシブ」「フィッシュ」といったラベルではなく、回数と平滑化した比率を保存します。初期に有用な特徴量には、プリフロップ参加率、プリフロップレイズ率、3ベットの機会と実行、ポストフロップのアグレッション、ベットに対するフォールド機会、観測されたショーダウンハンドがあります。すべての比率には分母が必要です。

4回の機会で2回レイズしたプレイヤーの観測レートは50%ですが、確信度はほとんどありません。ベイズ平滑化により、少数サンプルでポリシーが大きく揺れるのを防げます。ベータ事前分布を使うと、比率は(successes + alpha) / (opportunities + alpha + beta)で推定できます。中立なBeta(2, 2)事前分布なら、4回中2回のレイズは4 / 8 = 50%となり、1回中0回は確信的なゼロではなく2 / 5 = 40%になります。

統計は戦略が変わる状況ごとに分けます。ポジションとストリートは重要です。テーブル全体の「アグレッション」では、UTGからのオープンとリバーのチェックレイズが混ざります。50個の疎なセルではなく、データを蓄積できる少数のバケットから始めてください。対戦相手モデリングガイドでは、減衰、ショーダウンの証拠、エクスプロイト上限をさらに掘り下げています。

対戦相手の特徴量を無視するベースラインポリシーも維持してください。それがなければ、適応が役立ったのか、単にボット全体が1週間好調だっただけなのか判断できません。

高度なポリシーインターフェースは何を返すべき?

高度なポリシーは、アクション意図、金額の意図、理由コード、特徴量値、モデルのメタデータを返すべきです。"call"のような単なる文字列では分析に不十分で、自由形式の長文ではコードとして曖昧すぎます。型付きレコードを使います。

from dataclasses import dataclass, field
 
 
@dataclass(frozen=True)
class DecisionIntent:
    action: Action
    amount: float | None
    reason_code: str
    confidence: float
    features: dict[str, float] = field(default_factory=dict)
    policy_version: str = "range-equity-v3"
 
 
def decide(snapshot: DecisionSnapshot, eq: EquityResult) -> DecisionIntent:
    features = {
        "equity": eq.equity,
        "pot_odds": snapshot.pot_odds,
        "spr": snapshot.stack_to_pot,
    }
    if snapshot.to_call == 0 and "check" in snapshot.legal:
        return DecisionIntent("check", None, "free_action", 1.0, features)
    if call_has_margin(snapshot, eq):
        confidence = min(1.0, (eq.equity - snapshot.pot_odds) * 4)
        return DecisionIntent("call", None, "equity_clears_price", confidence, features)
    return DecisionIntent("fold", None, "equity_below_price", 0.8, features)

範囲への丸めとフォールバックは引き続き実行ガードが所有します。アクティブなターンが変わった後にポリシーがコールを提案したら破棄します。合法リストにないレイズを提案したら、チェックまたはフォールドに置き換え、ポリシー違反メトリクスを増やします。黙った修正はバグを隠します。修正とテレメトリを組み合わせれば、テーブルを安全に保ちながら障害を可視化できます。

ハンドリプレイで判断経路全体をどうテストする?

ハンドリプレイでは、ソケットを開かず、記録済みサーバーイベントをライブと同じ正規化層とポリシーへ通します。ハンド間で漏れる状態、空席から導出されたポジション、重複イベント、算術処理へ到達するnullのamountなど、単体テストでは見逃す不具合を検出します。

到着順を保ち、1行に1つのJSONメッセージを保存します。リプレイランナーはファイルを読み、実際のハンドラーを呼び出します。乱数は固定シードへ、外部サービスは記録済み応答へ置き換えます。出力は、承認済みの挙動とテストで比較できる判断レコード列にします。

import json
import random
from pathlib import Path
 
 
def replay(path: str, engine) -> list[dict]:
    random.seed(20260812)
    decisions = []
    for line in Path(path).read_text(encoding="utf-8").splitlines():
        event = json.loads(line)
        result = engine.apply(event)
        if result is not None:
            decisions.append(result)
    return decisions
 
 
def test_replay_never_emits_illegal_action(engine):
    for item in replay("fixtures/three_hands.jsonl", engine):
        assert item["action"] in item["legal"]

プロパティベーステストはアクションガードに有効です。合法アクション集合とレイズ範囲をランダム生成し、出力が必ず提示済みで範囲内にあることを検証します。Hypothesisはこの形式のテスト向けです。ポーカー上のすべての判断にプロパティテストは不要ですが、プロトコルの不変条件には導入する価値があります。

戦略変更をどう評価する?

戦略変更は、ペアにしたポリシー、固定バージョンラベル、運用メトリクス、不確実性を明らかにできる十分なハンド数で評価します。新しいレンジをデプロイし、その後の200ハンドを先週火曜日の結果と比較してはいけません。対戦相手の構成、ポジション、カードの分散がすべて変わっています。

少なくとも以下を追跡します。

メトリクス重要な理由
1,000ターン当たりの不正アクション厳格な正しさのゲート
判断レイテンシp50、p95、p99平均値に隠れるテール障害を検出する
フォールバック率ポリシーや依存先の不安定さを示す
信頼区間付きbb/100ブラインドで正規化した戦略成果
オールイン調整推定ショーダウン分散の一部を軽減する
VPIP、PFR、3ベット率挙動がどう変わったか説明する
ストリート別フォールド率明らかな戦略上のリークを見つける

可能ならシミュレーターで共通乱数を使い、ポリシーAとBを同じディール、同じ対戦相手アクションに対して実行します。ライブプレイでは環境を完全には制御できないため、テーブル構成を記録し、より長い期間で比較します。Google DeepMindのOpenSpiel論文は、不完全情報ゲームを含むゲームの評価・研究フレームワークを説明しています。ローカルではこれが有用で、ライブのOpen Pokerアリーナではプロトコルと多様な対戦相手を検証できます。

bb/100がプラスというだけで変更を昇格させません。正しさのゲートをゼロに保ち、レイテンシを予算内に収め、許容できない別の悪化を起こさず、宣言した目標を改善する必要があります。

高度な段階でLLMをどこに配置する?

LLMはネットワーククライアントや合法性の決定権者ではなく、選択的な助言役として他と同じポリシーインターフェースの背後に置きます。簡潔なスナップショット、正確な合法アクション、計算済みポットオッズ、推定レンジ、厳密なJSONスキーマを与えます。回答を検証し、決定的なフォールバックを維持してください。

明白な判断はモデルを迂回します。無料のチェック、厳格なプリフロップチャートによる強制フォールド、決定的なバリュールールですでにサイズが決まったレイズにリモート呼び出しは不要です。選択的ルーティングはコストを下げ、レイテンシを制御しやすくします。LLMポーカーボットガイドには基本的な接続パターンがありますが、高度なランタイムではスキーマ検証、バージョン管理したプロンプト、期限によるキャンセル、リプレイフィクスチャも追加します。

自然言語の理由を評価の証拠にすることには慎重です。モデルは悪いアクションにも説得力のある説明を付けられます。結果、反実仮想テスト、安定したメトリクスに照らしてアクションを評価してください。文章はデバッグ用に残しても、信頼するのは構造化された特徴量と実験記録です。

FAQ

ポーカーボットを高度にするものは?

高度なボットは、不変なターンスナップショット、明示的な導出特徴量、レンジを考慮したエクイティ、対戦相手統計、リプレイテスト、バージョン管理された実験を使います。戦略コードが多いだけでは高度なアーキテクチャになりません。

ポーカーボットはモンテカルロ試行を何回行うべき?

判断ごとに2,000から5,000回から始め、自分のハードウェアでベンチマークしてください。推定値が判断を変える効果がレイテンシに見合う場合だけ増やします。試行回数と実行時間の両方を記録してください。

対戦相手のモデリングにどれくらいデータが必要?

最初の観測から平滑化を使いますが、各統計に意味のある分母が貯まるまでは適応を小さく保ちます。3ベットの機会はプリフロップ参加の機会よりはるかに少ないため、万能なハンド数はありません。

勝率とチップ利益のどちらを最適化すべき?

比較可能な戦略レポートには100ハンド当たりのビッグブラインドを使い、シーズンへの影響には生のチップ数も使います。不確実性と運用メトリクスを必ず公開してください。ハンド数と区間のない点推定は誤った確信を招きます。

リプレイテストでポーカー戦略が勝つことを証明できる?

いいえ。リプレイは決定的な挙動を証明し、既知の状況で回帰を検出します。シミュレーションとライブ実験が、ハンドや対戦相手の分布に対する性能を検証します。

ポリシーバージョン、ハンドリプレイ、メトリクスによってすべての変更を監査できるようになると、次の制約は運用です。ポーカーボットのプロ向けアーキテクチャでは、無人稼働するボットの再同期、期限予算、障害分離、シャドー評価、安全なリリースを扱います。

続きを読む