Skip to content
PokerEnginePro
Game engine

Every hand, deterministic.
Every chip, accounted for.

A pure engine decides every hand on the server — the betting, the side pots, the rake, the house rules — and writes a log that replays to the decision. Clients send intentions; the engine sends back what happened.

0

chip-conservation violations

every hand, every run — checked after every state transition

100,000

hands in the conservation soak

random rake, antes and micro-stacks, on every build, in about two seconds

2,598,960

five-card hands verified

every one, plus 12 million random hands against two independent oracles

43,000

bot hands per second

single-threaded, chips checked after every hand

Server authority

Clients ask.
The engine decides.

Clients send intentions, the server decides. Every action is checked against the legal-action set; anything else gets a typed rejection and the engine never throws.
  • A pure engine with no clock, I/O, threads or randomness of its own: the same deck and the same actions always produce the same hand, enforced by architecture tests.
  • Chip conservation is checked after every state transition, and a 100,000-hand soak with random rake, antes and micro-stacks runs on every build.
  • Any hand can be voided at any point: every stack is restored and the void is written into the hand's own log.

Every rejection is typed

  • NoDecisionPending
  • WrongSeat
  • IllegalActionType
  • AmountBelowMinimum
  • AmountAboveMaximum
  • TooManyDiscards
  • CardNotHeld
  • RunsNotAllowed
The engine's trust boundaryA player client sends an intention, raise to 100. The engine checks it against the pending decision for seat 3: legal actions fold, call or raise, call 2, raise to between 4 and 100. A legal action is applied and becomes an ActionTaken event broadcast to every viewer through per-viewer projections. An illegal one is rejected with a typed reason, such as AmountAboveMaximum, and nothing changes.Player clientintentionRaiseTo(100)PendingDecision · seat 3legal Fold · Call · RaisecallAmount 2minRaiseTo 4 · maxRaiseTo 100the whole trust boundarylegalillegalAppliedActionTakenseq 16 · all-incommitted 100RejectedAmountAboveMaximumno mutation,no throwEvery viewer,projectedBack to thatseat only
The decision is real: seat 3's turn in a golden hand, before it moves all in.
engine API · driving a hand
var hand = new HandStateMachine(config, rules, seats, deck);
var step = hand.Advance();                          // runs until a decision is needed or the hand settles
while (step.Pending is { } decision)
{
    PlayerAction action = /* from the seat's client, validated against decision */;
    var result = hand.Submit(decision.Seat, action); // typed rejection, no mutation, on illegal input
    step = new AdvanceResult(result.Phase, result.Pending);
}
HandSettlement settlement = hand.Settlement!;       // per-seat deltas, pots, rake
IReadOnlyList<HandEvent> log = hand.Events;         // canonical record; serialise with HandEventSerializer
The engine's API reference, quoted as written: the whole loop a room runs. Randomness enters only through the deck; the clock, I/O and threads stay outside the engine.

The hand

One state machine.
Every game composed on it.

Forced bets, the deal, betting, the run-out, showdown and settlement as explicit phases of one machine — with bring-in and draw phases for stud and draw games built in.

HandPhase

  1. NotStarted
  2. ForcedBets
  3. Dealing
  4. BringInstud
  5. Drawingdraw
  6. Betting
  7. RunOut
  8. Showdown
  9. Settled
  10. Voided

Dealing and betting repeat for every street; a hand that is voided at any point restores every stack and says so in its log. Rejections, phases and settlements are data — never exceptions.

Seven seams

IDeckSpec
52 cards, or 36 for Short Deck
IDealingSchedule
streets, hole and community cards, burns, draw rounds
IForcedBets
blinds, antes, entry posts, straddles, bring-ins
IBettingStructure
no-limit, pot-limit, fixed-limit — raise ranges and caps
IActionOrder
who acts first, and next, on every street
IShowdownRules
high, low, Omaha's two-plus-three, show order, odd chip
IDrawRules
draw rounds, discards, stand pat, reshuffle

Games as compositions

Every game is composed from seams — deck, dealing schedule, forced bets, betting structure, action order, showdown and draw rules — so a new variant is a composition, not a fork.

Equity calculator

Each hand's share of the pot — exact with one or two cards to come, a seeded sample otherwise — so the same hand always shows the same numbers.

Side pots

Three all-ins.
Three pots, three winners.

Pots are built by contribution level — as many as the hand needs. This is a real hand from the engine's golden tests: four players all in before the flop for 20, 50, 100 and 100.
  • Side pots by contribution level, uncalled-bet return, configurable odd-chip rules and hi/lo split settlement.
  • Each pot is contested only by the seats that paid into it, and settled on its own.
  • Every seat's result is recorded at settlement: stack before and after, contributed, won, shown.
sixmax-three-allins-side-pots.json · event 27
{
  "event": "StreetCompleted",
  "street": "Pre-flop",
  "potTotal": 270,
  "pots": [
    { "index": 0, "amount": 80,
      "eligibleSeats": [0, 1, 2, 3], "level": 20 },
    { "index": 1, "amount": 90,
      "eligibleSeats": [0, 2, 3], "level": 50 },
    { "index": 2, "amount": 100,
      "eligibleSeats": [0, 3], "level": 100 }
  ],
  "seq": 27,
  "streetIndex": 0
}
engine/Poker.Engine.Tests/Golden — the engine's own log of the pots after the pre-flop betting (whitespace condensed).

What each seat put in

Three all-ins, three potsSeat 1 is all in for 20, seat 2 for 50, seats 0 and 3 for 100. Every seat's first 20 chips make the main pot of 80; the next 30 from seats 2, 0 and 3 make side pot 1 of 90; the last 50 from seats 0 and 3 make side pot 2 of 100.205010020Seat 12♣ 2♦50Seat 2A♣ A♦100Seat 09♠ T♠100Seat 3K♣ K♦

Board

  1. Main pot

    20 × 4 = 80

    eligible: seats 0 · 1 · 2 · 3

    Seat 1 wins with three twos

  2. Side pot 1

    30 × 3 = 90

    eligible: seats 0 · 2 · 3

    Seat 2 wins with a pair of aces

  3. Side pot 2

    50 × 2 = 100

    eligible: seats 0 · 3

    Seat 3 wins with a pair of kings

Seats 4 and 5 folded and paid nothing. The twos win only what they could cover, the aces take the middle pot, and the kings win back the top one — exactly what they put in.

Rake & house rules

Your rake.
Your rules.

Basis points, caps, caps by players dealt, no-flop-no-drop and minimum pots, split proportionally across side pots — scheduled by game, structure, currency and stakes.
Basis points
A percentage in hundredths — 500 is 5%.
Cap
A maximum per hand; zero leaves it uncapped.
Cap by players dealt
A different cap heads-up, short-handed and full.
No flop, no drop
No rake when the hand ends on the first street.
Minimum pot
Pots below it are not raked.
Across side pots
The rake split proportionally between the pots.
A schedule
Lines by game, structure, currency and stakes — the most specific line applies.
RakeRule.cs · Compute
public long Compute(long potTotal, int playersDealt, bool reachedSecondStreet)
{
    if (BasisPoints <= 0 || potTotal <= 0 || potTotal < MinimumPot)
    {
        return 0;
    }

    if (NoFlopNoDrop && !reachedSecondStreet)
    {
        return 0;
    }

    var rake = potTotal * BasisPoints / 10_000;
    var cap = EffectiveCap(playersDealt);
    if (cap > 0 && rake > cap)
    {
        rake = cap;
    }

    return Math.Max(0, Math.Min(rake, potTotal));
}
engine/Poker.Engine/Core/RakeRule.cs — the room supplies the rule; the engine only applies it, in minor units, and the rake is part of the chip-conservation check.

House rules, as settings

TDA and Robert's Rules defaults, big-blind and per-player antes, new-player posting, show and muck order, all-in run-out reveal and dead-button rules. Under-the-gun, button and Mississippi straddles, re-straddle chains and short-stack all-in straddles — or none, by house rule.

Straddle

Straddles

  • None
  • UnderTheGun
  • Button
  • Mississippi

MaxStraddles for re-straddle chains

AnteMode

Antes

  • None
  • PerPlayer
  • BigBlindAnte

a short big blind posts the blind first

NewPlayerPosting

New players

  • WaitForBigBlind
  • PostBigBlind
  • PostNothing

OddChip

Odd chips

  • FirstLeftOfButton
  • ToHigh
  • ToBestSuitedHigh

RunItMultiple

Running it more than once

  • Never
  • ByUnanimousConsent
  • Always

RunItTimes: 2 or 3

FixedLimitRaiseCap

Limit games

  • 4 bets a street

a bet and three raises; HeadsUpUnlimitedRaises lifts the cap heads-up

Commit-reveal shuffle

Committed before the deal.
Verifiable after it.

A CSPRNG Fisher–Yates shuffle with SHA-256 commit-reveal: the deck is committed before the hand and revealed after it, so players and auditors can verify every deal.
  1. 1

    Seed

    32 bytes from a cryptographic random number generator, and a 16-byte nonce.

  2. 2

    Commit

    SHA-256 of seed and nonce is fixed before a single card is dealt, tagged with the algorithm, PEP-SHUFFLE-1.

  3. 3

    Shuffle

    Fisher–Yates, driven by an HMAC-SHA256 counter stream keyed by the seed; rejection sampling leaves no modulo bias.

  4. 4

    Deal

    The hand is dealt from that order. Changing a card would break the commitment.

  5. 5

    Reveal

    After settlement — never before — the seed and nonce are revealed.

  6. 6

    Verify

    Anyone recomputes the hash, compared in constant time, and the deck order, card by card.

SecureShuffle.cs
public const string AlgorithmId = "PEP-SHUFFLE-1";

public static ShuffleCommitment Commit(ReadOnlySpan<byte> seed, ReadOnlySpan<byte> nonce)
{
    Span<byte> material = stackalloc byte[SeedBytes + NonceBytes];
    seed.CopyTo(material);
    nonce.CopyTo(material[SeedBytes..]);
    var hash = SHA256.HashData(material);
    return new ShuffleCommitment(AlgorithmId, ImmutableArray.Create(hash));
}

internal static void FisherYates(List<Card> list, int from, HmacStream stream)
{
    for (var i = list.Count - 1; i > from; i--)
    {
        var j = from + stream.NextInt(i - from + 1);
        (list[i], list[j]) = (list[j], list[i]);
    }
}
engine/Poker.Engine/Shuffle/SecureShuffle.cs — the commitment and the shuffle, as shipped.

The committed shuffle at every table

Every table, play money included, deals from the committed cryptographic shuffle — 25 to 35 microseconds of CPU a hand — and real-money tables also log every shuffle for the laboratory when the log is switched on.

Lab-certified RNG

RNG output logging for test laboratories and certification of the random number generator.

Evaluator & bots

Proven on every hand.
Then on millions more.

An allocation-free evaluator verified on all 2,598,960 five-card hands and 12 million random hands against two independent oracles, with ace-to-five, deuce-to-seven, Badugi and Short Deck rankings.

12M

random hands against two oracles

10 million seven-card hands against the legacy evaluator, 2 million against the reference — identical keys

50,000

Omaha deals against the best five of nine

an Omaha hand is never better than the best five of its nine cards

The production evaluator counts ranks and suits instead of enumerating five-card subsets, and allocates nothing. It ranks by profile, so Short Deck's order — a flush over a full house — is a table, not a second evaluator; Omaha's two-from-the-hand, three-from-the-board rule is a constraint on the same code.

Bots for development and play-money tables

Rule-based bots that play every variant, a 43,000 hands/s runner for soak testing, and table fillers locked to play money.

  • SmartBot
  • RandomBot
  • AlwaysCallBot
  • AlwaysFoldBot
  • AlwaysRaiseBot
  • AlwaysAllInBot

43,000

bot hands per second

single-threaded, chips checked after every hand

Poker.Engine.Bots · 100,000 hands
$ dotnet run --project engine/Poker.Engine.Bots -c Release \
    -- --hands 100000 --reset-stacks
Running 100,000 hands of No-Limit Hold'em with 6 bots (seed 1, stack 200)...
Hands: 100,000  Showdowns: 67,972  Actions: 1,519,237  Rake: 0
Elapsed: 2.28s  (43,908 hands/s)
  seat 0 Smart#0    net  845,359  rebuys    0  hands won 19,645
  seat 1 Random#1   net -444,323  rebuys    0  hands won 15,788
  seat 2 Random#2   net -366,518  rebuys    0  hands won 16,010
  seat 3 Smart#3    net  764,085  rebuys    0  hands won 19,614
  seat 4 Random#4   net -372,877  rebuys    0  hands won 16,037
  seat 5 Random#5   net -425,726  rebuys    0  hands won 15,600
Chip conservation held for every hand.
The soak test, run on a development machine (the progress line is cut). Seeded, so the same command deals the same 100,000 hands.

Replay & hand histories

Replayed to the decision.
Exported, ready to read.

A canonical event log per hand, per-viewer projections that never leak a hidden card, and golden replays checked at every decision.

1,500

random hands replayed at every decision

the replay equals the live machine at each decision and at settlement

14

golden hands, byte for byte

side pots, straddles, odd chips, limit caps, Omaha and run it twice

one event, two viewers
// the full log
{
  "event": "CardsDealt",
  "seat": 3,
  "cards": [{ "card": "Kc", "faceUp": false }],
  "seq": 5,
  "streetIndex": 0
}
// what a spectator receives
{
  "event": "HiddenCardsDealt",
  "seat": 3,
  "count": 1,
  "seq": 5,
  "streetIndex": 0
}
Event 5 of the side-pot hand, and the same event projected for a spectator by HandEventProjection — a hidden card never leaves the server (whitespace condensed).
hand 1 · PokerStars-compatible text
PokerEnginePro Hand #1: Hold'em No Limit (1/2 PLAY) - 2026/09/23 12:00:00 UTC
Table 'Golden table' 6-max Seat #1 is the button
// … six seats, the blinds, seat 3's hole cards
Player3: raises to 100 and is all-in
Player4: folds
Player5: folds
Player0: calls 100 and is all-in
Player1: calls 20 and is all-in
Player2: calls 50 and is all-in
// … the hands, shown for the all-in run-out
*** FLOP *** [Qh Jd 5s]
*** TURN *** [Qh Jd 5s 2s]
*** RIVER *** [Qh Jd 5s 2s 6d]
*** SHOW DOWN ***
Player3: shows [Kc Kd] (Pair [King King Queen Jack Six])
Player0: shows [9s Ts] (HighCard [Queen Jack Ten Nine Six])
Player1: shows [2c 2d] (ThreeOfAKind [Two Two Two Queen Jack])
Player2: shows [Ac Ad] (Pair [Ace Ace Queen Jack Six])
Player1 collected 80 from pot
Player2 collected 90 from side pot-1
Player3 collected 100 from side pot-2
*** SUMMARY ***
Total pot 270 | Rake 0
Board [Qh Jd 5s 2s 6d]
HandHistoryExport.ToPokerStarsText over the same golden hand, from seat 3 — cut where marked. Both exports come from the one event log the table sent.

Hand histories players already know

Every hand stored at settlement, exportable as canonical JSON or PokerStars-compatible text, and replayable in the client.

A replayer that cannot disagree

Every stored hand replays through the same reducer that drew it live, so a replay never shows what the table would not have.

Read the log.
Then replay it yourself.

A walkthrough of the engine with its golden hands, its soak and its API reference — every number on this page, run in front of you.