Skip to content
[OPEN_POKER]

Advanced Poker Bot Architecture: Equity and Testing

JJoão Carvalho||10 min read

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 classExampleConfidenceOwner
Server factpot = 180, raise max 1,640AuthoritativeProtocol adapter
Derived factpot odds, effective stack, positionExact if inputs are correctFeature layer
Estimate43% showdown equitySampled or modeledEquity service
Beliefopponent opens 31% from buttonUncertainOpponent model
Intentcall because equity clears pricePolicy outputStrategy

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:

MetricWhy it matters
Illegal actions per 1,000 turnsHard correctness gate
Decision latency p50, p95, p99Detects tail failures hidden by averages
Fallback rateShows policy or dependency instability
bb/100 with confidence intervalStrategy outcome, normalized by blinds
All-in adjusted estimateReduces some showdown variance
VPIP, PFR, three-bet rateExplains how behavior changed
Fold rate by streetFinds 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.

Keep Reading