Advanced Poker Bot Architecture: Equity and Testing
An advanced poker bot turns a reliable event loop into a measurable decision system. The upgrade isn't “add AI.” It's immutable decision snapshots, explicit poker features, range-aware equity, replayable hands, and an experiment loop that can prove a policy changed. Keep the legal-action guard from the basic bot. Everything smarter sits behind it.
Poker bot architecture series: Part 2 of 3. Start with Basic Poker Bot Architecture if your runtime still rejects actions. Continue with Pro Poker Bot Architecture. Compare operating options in Poker Bot Cost in 2026.
What changes in an advanced poker bot architecture?
An advanced poker bot separates facts, estimates, policy, and execution. Facts come from the latest server turn: pot, board, stacks, legal actions, hand ID, and token. Estimates include equity and opponent range. Policy uses both to return an intent. Execution checks that intent against facts one last time.
This distinction matters because estimates are allowed to be wrong. An opponent model can assign a range that's too tight. A Monte Carlo run can have sampling error. Neither error should produce an invalid raise or a response for an expired turn. The outer shell remains deterministic even when the inner policy becomes probabilistic.
| Data class | Example | Confidence | Owner |
|---|---|---|---|
| Server fact | pot = 180, raise max 1,640 | Authoritative | Protocol adapter |
| Derived fact | pot odds, effective stack, position | Exact if inputs are correct | Feature layer |
| Estimate | 43% showdown equity | Sampled or modeled | Equity service |
| Belief | opponent opens 31% from button | Uncertain | Opponent model |
| Intent | call because equity clears price | Policy output | Strategy |
Never flatten those into one untyped dictionary. Once confidence and ownership disappear, a guessed range starts looking as trustworthy as the server's raise bound.
How should immutable decision snapshots work?
An immutable decision snapshot is a complete, read-only input captured from one your_turn. It prevents late network events from changing data while equity or model work is running. It also becomes the unit you replay in tests.
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")Build the snapshot synchronously when the turn arrives. Then pass it to an async decision task. Before sending the result, compare its hand_id and turn_token with the active turn again. Open Poker tokens are consumed after an action and each new your_turn invalidates the prior one, so stale work must be discarded, not retried.
The message types reference documents the wire fields. Your normalizer should also accept table_state after reconnect because its player-specific hero section contains hole cards and current legal actions.
How do equity and pot odds become a decision?
Equity answers how often a hand receives the pot at showdown against an assumed range; pot odds answer the break-even share required for a call. Calling has positive chip expectation when estimated equity exceeds call / (pot + call), before adjustments for future betting, range error, and tournament objectives.
If the pot is 180 and calling costs 60, pot odds are 60 / 240 = 25%. A 38% equity estimate clears that raw threshold by 13 percentage points. Don't call every spot with a one-point edge. Monte Carlo variance, an inaccurate opponent range, and future action can erase a thin margin. We use an explicit safety margin and log it.
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
)The Monte Carlo equity calculator shows a Python implementation. For maintained evaluation primitives, PokerKit's evaluator documentation is a stronger starting point than writing a hand ranker during a strategy project.
How should an opponent model represent uncertainty?
An opponent model should store counts and smoothed rates, not labels such as “aggressive” or “fish.” Useful early features include voluntary preflop participation, preflop raise rate, three-bet opportunities and actions, postflop aggression, fold-to-bet opportunities, and observed showdown hands. Every rate needs its denominator.
A player who raised twice in four opportunities has a 50% observed rate and almost no certainty. Bayesian smoothing keeps tiny samples from swinging the policy. With a Beta prior, a rate can be estimated as (successes + alpha) / (opportunities + alpha + beta). A neutral Beta(2, 2) prior turns two raises in four chances into 4 / 8 = 50%, while zero raises in one chance becomes 2 / 5 = 40%, not a confident zero.
Segment statistics by situations that change strategy. Position and street matter. Table-wide “aggression” throws together an under-the-gun open and a river check-raise. Start with a few buckets you can fill, not fifty sparse cells. Our opponent modeling guide goes deeper into decay, showdown evidence, and exploit caps.
Keep a baseline policy that ignores opponent features. Without it, you can't tell whether adaptation helps or whether the whole bot simply ran well for a week.
What should the advanced policy interface return?
The advanced policy should return action intent, amount intent, reason codes, feature values, and model metadata. A plain string such as "call" is too small for analysis. A free-form essay is too loose for code. Use a typed record.
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)The execution guard still owns clamping and fallback. If this policy proposes a call after the active turn changes, discard it. If it proposes a raise absent from the legal list, replace it with check or fold and increment a policy-violation metric. Silent correction hides bugs; correction plus telemetry keeps the table safe and the failure visible.
How do hand replays test the whole decision path?
Hand replays feed recorded server events through the same normalizer and policy used live, without opening a socket. They catch bugs that isolated unit tests miss: state that leaks between hands, position derived from an empty seat, a duplicated event, or a null amount that reaches arithmetic.
Store one JSON message per line, preserving arrival order. A replay runner can load the file and call the real handlers. Replace randomness with a fixed seed and external services with recorded responses. The output should be a sequence of decision records that a test can compare with approved behavior.
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"]Property-based tests are useful for action guards. Generate random legal-action sets and raise bounds, then assert that output is always offered and always within bounds. Hypothesis is built for this style of testing. You don't need property tests for every poker opinion, but protocol invariants deserve them.
How should you evaluate a strategy change?
Evaluate a strategy change with paired policies, fixed version labels, operational metrics, and enough hands to expose uncertainty. Don't deploy a new range and compare its next 200 hands with last Tuesday's result. Opponent mix, positions, and card variance changed too.
Track at least these metrics:
| Metric | Why it matters |
|---|---|
| Illegal actions per 1,000 turns | Hard correctness gate |
| Decision latency p50, p95, p99 | Detects tail failures hidden by averages |
| Fallback rate | Shows policy or dependency instability |
| bb/100 with confidence interval | Strategy outcome, normalized by blinds |
| All-in adjusted estimate | Reduces some showdown variance |
| VPIP, PFR, three-bet rate | Explains how behavior changed |
| Fold rate by street | Finds obvious strategic leaks |
Use common random numbers in a simulator when possible: run policy A and B against the same deals and opponent actions. Live play can't fully control the environment, so record table composition and compare longer windows. Google DeepMind's OpenSpiel paper describes an evaluation and research framework for games, including imperfect-information games. It is useful locally, while the live Open Poker arena tests protocol and opponent diversity.
A change isn't promoted merely because bb/100 is positive. It must keep correctness gates at zero, keep latency within budget, and improve a declared target without causing an unacceptable regression elsewhere.
Where does an LLM fit at the advanced level?
An LLM fits behind the same policy interface as a selective adviser, not as the network client or legality authority. Give it a compact snapshot, exact legal actions, calculated pot odds, an estimated range, and a strict JSON schema. Validate its answer and keep a deterministic fallback.
Route obvious decisions around the model. A free check, a forced fold from a strict preflop chart, or a raise size already chosen by a deterministic value rule doesn't need a remote call. Selective routing lowers cost and makes latency easier to control. The LLM poker bot guide shows the basic connection pattern, but an advanced runtime should add schema validation, versioned prompts, deadline cancellation, and replay fixtures.
We're skeptical of natural-language reasons as evaluation evidence. A model can produce a persuasive explanation for a poor action. Judge the action against outcomes, counterfactual tests, and stable metrics. Keep the text for debugging, but trust the structured features and experiment record.
FAQ
What makes a poker bot advanced?
An advanced bot uses immutable turn snapshots, explicit derived features, range-aware equity, opponent statistics, replay tests, and versioned experiments. More strategy code alone doesn't make the architecture advanced.
How many Monte Carlo trials should a poker bot run?
Start with 2,000 to 5,000 trials per decision and benchmark on your hardware. Use more only when the estimate changes decisions enough to justify the latency. Log both trial count and runtime.
How much data do I need for opponent modeling?
Use smoothing from the first observation, but keep adaptation small until each statistic has a meaningful denominator. There isn't one magic hand count because three-bet opportunities arrive far less often than preflop participation opportunities.
Should I optimize for win rate or chip profit?
Use big blinds per 100 hands for comparable strategy reporting, plus raw chips for season impact. Always publish uncertainty and operational metrics. A point estimate without hand count and interval invites false confidence.
Can replay tests prove a poker strategy wins?
No. Replays prove deterministic behavior and catch regressions on known situations. Simulations and live experiments test performance against distributions of hands and opponents.
When policy versions, hand replays, and metrics make every change auditable, the next constraint is operational. Pro Poker Bot Architecture covers resynchronization, deadline budgets, failure isolation, shadow evaluation, and safe releases for a bot that runs unattended.