Skip to content

danstam/poker-equity-engine

Repository files navigation

♠️ Poker Equity Engine

Texas Hold'em equity calculator built from scratch — C core, Python API, React frontend.

Live License C Python React


What is this?

The engine answers one question:

Given a player's exact two-card hand and one or more opponents playing from a weighted distribution of possible hands, what is each player's equity across all the ways the remaining board cards can be distributed?

The project has three layers:

  • The C engine (src/, include/) — hand evaluation and Monte Carlo simulation
  • The Python bridge (src/python/) — a wrapper around the compiled library, and a FastAPI server
  • The frontend (frontend/) — a React web app that calls the API

Table of Contents


The C Engine

The engine is written in C and compiled into a shared library. It has two components: the hand evaluator, which scores any poker hand, and the Monte Carlo simulator, which uses that evaluator to calculate equity across thousands of simulated runouts.


The Hand Evaluator

The hand evaluator takes any 2–7 card hand and returns a score. A higher score always means a stronger hand, and any two scores can be compared directly to determine a winner.

⚡ Performance

Benchmarked at ~700 million hands per second — roughly 300× faster than a brute-force evaluation at runtime.

Rather than storing a list of cards, the evaluator maintains a compact summary of the hand that updates incrementally as each card is added. This tracks rank composition and suit distribution — enough to perform the flush check and route to the correct table, without iterating over cards at evaluation time.

How it works

Precomputed tables

Before the engine can be used, a standalone table-generation program (tools/generate_tables.c) runs once and produces the lookup tables compiled directly into the library. These tables never change and never need to be loaded from disk at runtime.

  • Flush table — handles any hand where five or more cards share the same suit
  • Main table — handles everything else

Together they cover all possible 7-card Texas Hold'em hands.

Evaluating a hand

  1. Check if a flush is present
  2. Route to the flush table or the main table
  3. Return the score — a single lookup, immediate result

The score encodes both hand category and rank within that category, so A♠A♥ scores higher than K♠K♥, a king-high flush beats a ten-high flush, and so on.

Score Ranges by Hand Category

Category Score Range
High card 4 096 – 8 191
Pair 8 192 – 12 287
Two pair 12 288 – 16 383
Three of a kind 16 384 – 20 479
Straight 20 480 – 24 575
Flush 24 576 – 28 671
Full house 28 672 – 32 767
Four of a kind 32 768 – 36 863
Straight flush 36 864 – 40 959

The Monte Carlo Simulator

Monte Carlo simulation is a technique for solving problems too complex to calculate exactly — instead of trying every possible outcome, it runs thousands of random trials and averages the results. Every time the game state changes — a card dealt, a range updated, anything — 100,000 fresh trials run instantly in the background. Each trial accounts for the exact state of the board: known cards are locked in and never redrawn, and each opponent's hand is sampled randomly from within their weighted range, so more likely holdings get picked more often. 100,000 of these statistically valid snapshots converge on an equity figure accurate to within ±0.3 percentage points.

What a range is

In poker, a range is the set of hands a player could plausibly be holding, along with how likely each hand is. In this engine, a range is a weight between 0.0 and 1.0 assigned to each of the 1,326 possible two-card starting hands.

Weight Meaning
1.0 Player always holds this hand when it's available
0.5 Player holds it half the time
0.0 Player never holds it
Seat types
Type Description
Exact The player's two cards are known (the hero)
Active range Still in the hand with a weighted range of possible holdings
Folded range Has folded but range is known — their cards are removed from the deck for other players
Absent The seat is empty
How a trial runs

The simulation runs 100,000 trials by default, producing results accurate to within ±0.3 percentage points at 95% confidence.

Each trial:

  1. Mark all known cards as unavailable (hero's hand, existing board cards)
  2. Deal each player with a range a hand — sampler filters unavailable cards, selects randomly according to remaining weights, marks dealt cards as unavailable before moving to the next player
  3. Draw any remaining community cards from the deck
  4. Evaluate every active player's best 5-card hand against the board
  5. Award equity — clean win gets full credit, ties are split equally

Equity is tracked as exact integers throughout — no floating-point accumulation errors. Percentages are only computed once at the very end.

If a player's range has no available hands due to card conflicts, the trial is discarded and retried.


The API

The engine is accessible over HTTP via a FastAPI server. Send a POST request to /equity and get back a full equity breakdown for the hand.

API Reference

POST /equity

Request

{
    "schema_version": "equity_request_v1",
    "request_id": "abc123",
    "trials": 100000,
    "hero_cards": ["As", "Kh"],
    "board_cards": ["2h", "7c", "Jd"],
    "seats": [
        {
            "seat_index": 1,
            "state": "ACTIVE",
            "range": {
                "encoding": "sparse_bp_v1",
                "pairs": [
                    [combo_index, weight_in_basis_points]
                ]
            }
        },
        { "seat_index": 2, "state": "ABSENT" }
    ]
}

Cards are two-character strings — rank then suit. Ranks: 2–9, T, J, Q, K, A. Suits: d, c, h, s.

Seats 1 through 8 must all be present. Each seat state is one of ACTIVE, ABSENT, PRE_FLOP_FOLD, or CUSTOM_FOLD. Non-absent seats require a range.

Range encodings

  • dense_bp_v1 — an array of exactly 1,326 integers in basis points (0–10000, where 10000 = weight 1.0), one per combo in canonical order
  • sparse_bp_v1 — an array of [combo_index, weight_bp] pairs covering only nonzero combos, sorted ascending by index

Response

{
    "schema_version": "equity_response_v2",
    "request_id": "abc123",
    "status": "ok",
    "result": {
        "hero": {
            "win_bp": 6840,
            "loss_bp": 2510,
            "tie_bp": 650,
            "equity_bp": 7165,
            "win_count": 68400,
            "loss_count": 25100,
            "tie_count": 6500
        },
        "villains": [
            {
                "seat_index": 1,
                "win_bp": 2835,
                "equity_bp": 2835
            }
        ],
        "diagnostics": {
            "requested_trials": 100000,
            "accepted_trials": 99997,
            "retry_count": 3,
            "deadlock_count": 0,
            "compute_ms": 142,
            "seed_in": "1311768467294899668",
            "seed_after": "9823456712309876543"
        },
        "table": {
            "street": "FLOP",
            "board_count": 3,
            "active_villain_count": 1,
            "folded_seat_count": 0,
            "absent_seat_count": 7
        }
    }
}

All percentages are returned in basis points (1/100th of a percent). Divide by 100 to get a percentage — 6840 bp = 68.40%.

On error, the response has "status": "error" with an error object containing a code and message.


The Frontend

A React + Vite web app (frontend/) for building hands and ranges and calling the equity API. When built, the frontend is served statically by the FastAPI server — both API and UI from a single process.


License

GPL-3.0

About

Texas Hold'em equity engine built from scratch — hand evaluator in C, Monte Carlo simulator, Python/FastAPI layer, and a React frontend

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors