PokerKit is the dealer, not the player.
It knows every rule of poker: whose turn it is, what raise is legal, who wins the pot, where the chips go. It never decides what to do. That part is you, or the bot you write. Everything on this page is a consequence of that one sentence.
Before you start
Read this page three times, differently each time
Read only the headings, the four jobs, and the five rules. Skip every code block. You are building the map.
Type each lab yourself, do not paste. Answer the "Predict first" box before you run it. Being wrong once is what makes it stick.
Jump to the recall cards at the bottom. Answer out loud before you open each one. Anything you miss, redo that lab.
Section 1 · The map
One library, four jobs
Picture a real card room. PokerKit ships four people you would find there. Every class you will ever import belongs to one of them.
State, NoLimitTexasHoldem
StandardHighHand
parse_range, calculate_equities
HandHistory (.phh)
Line counts from the repo: state.py 6,439 · notation.py 2,720 · games.py 2,442 · hands.py 851 · analysis.py 555. The Dealer is almost half the library, because poker rules have many corner cases (side pots, all-in runouts, stud bring-ins, draw games).
Section 2 · How you talk to the Dealer
Every program is the same four-step loop
game(stacks, players) gives you a fresh state: one hand of poker.state.actor_index says whose turn. state.can_fold() etc. say what is legal.fold(), check_or_call(), complete_bet_or_raise_to(x), deal_hole(...)state.stacks, state.board_cards, and at the end state.payoffs.Repeat steps 2 and 3 while state.status:. When status turns False, the hand is over.
What "Automation" means
A hand has many small chores. Each blue pill below is something you must always do. Each grey pill is a chore you can hand to the Dealer by listing it in automations.
Poker bot? Automate every grey pill. Want to choose the cards (to test one specific spot)? Leave HOLE_DEALING and BOARD_DEALING out, then call deal_hole('AsKs') yourself. Building a real online table? Automate nothing and animate every step.
Section 3 · The rules that trip people up
Five things to remember
Raise TO, never raise BY.
complete_bet_or_raise_to(6) means "my total bet this street becomes 6", not "add 6". One method covers complete, bet and raise.
Index = position, not seat. Heads-up flips it.
With 3+ players: 0 = small blind, 1 = big blind, last = button. With 2 players: 0 = big blind, 1 = button/small blind, so player 1 acts first preflop. Lab 2 shows this.
Every action comes in threes.
verify_folding() raises an error, can_fold() returns True/False, fold() does it. An illegal action raises ValueError and leaves the state untouched.
Cards are plain strings.
'AsKs' is two cards. Ranks 23456789TJQKA, suits cdhs. '??' is an unknown card, handy for hands where you never saw the cards.
Python counts from 0, hand histories from 1.
In code, the first player is 0. In a saved .phh file the same player is p1. Also, board_cards is a list of lists (one list per card slot) because PokerKit supports multiple boards and run-it-twice.
Section 4 · Labs
Set up a sandbox
3 minPokerKit needs Python 3.11 or newer and has no other dependencies. Use a virtual environment so nothing touches your system Python.
python3 --version # must say 3.11 or higher mkdir pokerkit-lab && cd pokerkit-lab python3 -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install pokerkit python -c "import pokerkit; print('ready')"
Every lab below is one file. Save it inside pokerkit-lab/ and run python lab1.py. All outputs shown were produced by actually running the code.
Meet the Judge
5 minStart with the smallest piece. from_game(hole, board) finds the best 5-card hand, and hands compare like numbers.
from pokerkit import StandardHighHand mine = StandardHighHand.from_game('AsKs', 'QsJsTs2c3d') yours = StandardHighHand.from_game('AhAd', 'QsJsTs2c3d') print(mine) # the best 5 cards it found print(yours) print(mine > yours) # compare like numbers
mine make, and what does yours make? Write both down.
Check yourself
A royal (reported as straight flush) against one pair of aces. The Judge picked the best 5 of 7 cards on its own.
Straight flush (AsKsQsJsTs) One pair (AhAdQsJsTs) True
StandardHighHand('7c5d4h3s2c') > StandardHighHand('AcAd9h8s2c'), then the same two hands as StandardLowHand (2-7 lowball). The first is False, the second True. In PokerKit "greater" always means "wins", even in low games.Play one hand with the Dealer
10 minHeads-up No-Limit Hold'em, blinds 1/2, 200 chips each. We leave dealing out of automations so we pick the cards.
from pokerkit import Automation, NoLimitTexasHoldem state = NoLimitTexasHoldem.create_state( ( Automation.ANTE_POSTING, Automation.BET_COLLECTION, Automation.BLIND_OR_STRADDLE_POSTING, Automation.CARD_BURNING, Automation.HOLE_CARDS_SHOWING_OR_MUCKING, Automation.HAND_KILLING, Automation.CHIPS_PUSHING, Automation.CHIPS_PULLING, ), True, # uniform antes? 0, # antes (1, 2), # blinds 2, # min-bet 200, # every stack = 200 2, # players ) state.deal_hole('AsKs') # player 0 state.deal_hole('7h7d') # player 1 print('stacks :', state.stacks) print('to act :', state.actor_index) state.complete_bet_or_raise_to(6) # player 1 raises TO 6 state.check_or_call() # player 0 calls state.deal_board('Qs Js 2c') # flop state.check_or_call() # player 0 checks state.complete_bet_or_raise_to(8) # player 1 bets 8 state.fold() # player 0 folds print('running?:', state.status) print('stacks :', state.stacks) print('payoffs :', state.payoffs)
Check yourself
[198, 199] and to act: 1. Heads-up, player 0 is the big blind and player 1 is the button, who posts the small blind and acts first preflop. That is rule 2.
stacks : [198, 199] to act : 1 running?: False stacks : [194, 206] payoffs : [-6, 6]
Notice the payoff is 6, not 14. Player 1's flop bet of 8 was never called, so the Dealer handed it back. You wrote zero lines of code for that.
state.fold() with state.check_or_call(). Now status stays True. Print state.street_index, deal a turn with state.deal_board('9d'), and keep playing to showdown.Try to break the rules
5 minGood referees say no. Ask before acting, then act illegally on purpose.
from pokerkit import Automation, NoLimitTexasHoldem state = NoLimitTexasHoldem.create_state( (Automation.ANTE_POSTING, Automation.BET_COLLECTION, Automation.BLIND_OR_STRADDLE_POSTING, Automation.CARD_BURNING, Automation.HOLE_DEALING, Automation.BOARD_DEALING, Automation.HOLE_CARDS_SHOWING_OR_MUCKING, Automation.HAND_KILLING, Automation.CHIPS_PUSHING, Automation.CHIPS_PULLING), True, 0, (1, 2), 2, 200, 2, ) print(state.can_complete_bet_or_raise_to(3)) print(state.min_completion_betting_or_raising_to_amount) try: state.complete_bet_or_raise_to(3) except ValueError as e: print('ValueError:', e) print(state.stacks)
Check yourself
Minimum raise-to is 4 (call 2, raise by at least 2). The failed raise leaves stacks exactly as before. This time the Dealer dealt random hole cards, because HOLE_DEALING is automated.
False 4 ValueError: The amount 3 is below the minimum allowed 4. [198, 199]
state.max_completion_betting_or_raising_to_amount. In no-limit it is your whole stack. Change the game to PotLimitOmahaHoldem (deal 4 hole cards) and watch the max shrink to the pot.Use the Calculator
5 minTake the flop from Lab 2. Should player 0 really have folded AsKs there?
from pokerkit import ( Card, Deck, StandardHighHand, calculate_equities, parse_range, ) print(len(parse_range('AKs')), 'combos of AKs') print(len(parse_range('AK')), 'combos of AK') print(len(parse_range('22')), 'combos of 22') eq = calculate_equities( (parse_range('AsKs'), parse_range('7h7d')), # the two hands Card.parse('QsJs2c'), # board so far 2, # hole cards per player 5, # board cards at the end Deck.STANDARD, (StandardHighHand,), sample_count=20000, ) print('AsKs vs 7h7d on QsJs2c:', eq)
Check yourself
About 63%. The fold in Lab 2 threw away a favourite. The Dealer allowed it because the Dealer never judges decisions, only legality.
4 combos of AKs 16 combos of AK 6 combos of 22 AsKs vs 7h7d on QsJs2c: [0.6321, 0.3679]
It is a Monte Carlo estimate, so your last digits will differ. 20,000 samples took about 5 seconds on one core. Pass executor=ProcessPoolExecutor() to use every core.
parse_range('AK') - parse_range('AKs') gives the 12 offsuit combos. Run AK against parse_range('22-99') preflop (board ()).1,000 hands of random bots
10 minThis is where PokerKit earns its place. Automate every chore, write a bot that picks any legal action, and let three of them play. The while state.status loop is the whole engine.
import random from pokerkit import Automation, NoLimitTexasHoldem AUTO = ( Automation.ANTE_POSTING, Automation.BET_COLLECTION, Automation.BLIND_OR_STRADDLE_POSTING, Automation.CARD_BURNING, Automation.HOLE_DEALING, Automation.BOARD_DEALING, Automation.RUNOUT_COUNT_SELECTION, Automation.HOLE_CARDS_SHOWING_OR_MUCKING, Automation.HAND_KILLING, Automation.CHIPS_PUSHING, Automation.CHIPS_PULLING, ) game = NoLimitTexasHoldem(AUTO, True, 0, (1, 2), 2) # a factory def random_bot(state): choices = [] if state.can_fold(): choices.append('fold') if state.can_check_or_call(): choices.append('call') if state.can_complete_bet_or_raise_to(): choices.append('raise') pick = random.choice(choices) if pick == 'fold': state.fold() elif pick == 'call': state.check_or_call() else: lo = state.min_completion_betting_or_raising_to_amount hi = state.max_completion_betting_or_raising_to_amount state.complete_bet_or_raise_to(random.randint(lo, hi)) random.seed(1) totals = [0, 0, 0] for _ in range(1000): state = game(100, 3) # fresh hand: 3 players, 100 chips while state.status: random_bot(state) assert sum(state.payoffs) == 0 totals = [t + p for t, p in zip(totals, state.payoffs)] print('net chips after 1000 hands:', totals) print('sum:', sum(totals))
assert ever fail? What must sum be?
Check yourself
Poker is zero-sum with no rake, so the sum is always 0. If it ever failed you would have found a bug in the Dealer. This check is the cheapest test you can put in any bot project.
net chips after 1000 hands: [692, 3739, -4431] sum: 0
1,000 full hands ran in under a second. Notice game is created once and called like a function for each new hand: game = the rules, state = one hand.
calling_station(state) that always calls when it can, give it seat 0, and keep the random bots in seats 1 and 2. Who wins over 5,000 hands? This is your first bot experiment.Kuhn poker, the game from this site's CFR pages
5 minPokerKit ships KuhnPoker: three cards (J, Q, K), ante 1, one bet of 1. It is the same toy game used in the CFR simulation, so you can use PokerKit as the rules engine for your own CFR.
from pokerkit import Automation, KuhnPoker state = KuhnPoker.create_state( ( Automation.ANTE_POSTING, Automation.BET_COLLECTION, Automation.BLIND_OR_STRADDLE_POSTING, Automation.CARD_BURNING, Automation.HOLE_CARDS_SHOWING_OR_MUCKING, Automation.HAND_KILLING, Automation.CHIPS_PUSHING, Automation.CHIPS_PULLING, ), ) state.deal_hole('Js') # player 0 gets the Jack state.deal_hole('Ks') # player 1 gets the King state.complete_bet_or_raise_to() # P0 bluffs with J state.check_or_call() # P1 calls with K print('payoffs:', state.payoffs)
Check yourself
2: the ante of 1 plus the bet of 1. In fixed-limit games complete_bet_or_raise_to() needs no amount, the size is fixed.
payoffs: [-2, 2]
Let the Scribe write it down
5 minReplay Lab 2, save it as a .phh file (Poker Hand History, a plain-text format), then load it back and step through it.
from pokerkit import Automation, HandHistory, NoLimitTexasHoldem game = NoLimitTexasHoldem( (Automation.ANTE_POSTING, Automation.BET_COLLECTION, Automation.BLIND_OR_STRADDLE_POSTING, Automation.CARD_BURNING, Automation.HOLE_CARDS_SHOWING_OR_MUCKING, Automation.HAND_KILLING, Automation.CHIPS_PUSHING, Automation.CHIPS_PULLING), True, 0, (1, 2), 2, ) state = game(200, 2) state.deal_hole('AsKs') state.deal_hole('7h7d') state.complete_bet_or_raise_to(6) state.check_or_call() state.deal_board('QsJs2c') state.check_or_call() state.complete_bet_or_raise_to(8) state.fold() hh = HandHistory.from_game_state(game, state) hh.players = ['Nut', 'Bot'] with open('my_hand.phh', 'wb') as f: hh.dump(f) print(open('my_hand.phh').read()) with open('my_hand.phh', 'rb') as f: replay = HandHistory.load(f) for i, s in enumerate(replay): # one state per action print(i, s.stacks)
variant = 'NT' ante_trimming_status = false antes = [0, 0] blinds_or_straddles = [1, 2] min_bet = 2 starting_stacks = [200, 200] actions = ['d dh p1 AsKs', 'd dh p2 7h7d', 'p2 cbr 6', 'p1 cc', 'd db QsJs2c', 'p1 cc', 'p2 cbr 8', 'p1 f'] players = ['Nut', 'Bot'] 0 [198, 199] 1 [198, 199] 2 [198, 199] 3 [198, 194] 4 [194, 194] 5 [194, 194] 6 [194, 194] 7 [194, 186] 8 [194, 206]
Read the action codes once and you can read any .phh file
| Code | Meaning | Python call |
|---|---|---|
| d dh p1 AsKs | dealer deals hole cards to player 1 | deal_hole('AsKs') |
| d db QsJs2c | dealer deals board | deal_board('QsJs2c') |
| p2 cbr 6 | player 2 completes / bets / raises to 6 | complete_bet_or_raise_to(6) |
| p1 cc | player 1 checks or calls | check_or_call() |
| p1 f | player 1 folds | fold() |
| p1 sd | stand pat / discard (draw games) | stand_pat_or_discard() |
Note p1 in the file is player 0 in Python (rule 5). The Scribe can also import PokerStars, Full Tilt, PartyPoker, iPoker, OnGame, Absolute and ACPC logs with HandHistory.from_pokerstars(...) and friends.
Section 5 · Where it fits
What PokerKit is for, and what it is not
Use it for
- The environment a poker bot plays inside
- Checking your own engine's rules against a trusted one
- Parsing and replaying hand histories
- Quick equity checks and range math
- Odd variants: 2-7 triple draw, badugi, stud, short deck, PLO
It is not
- A solver. It computes no strategy, no GTO, no EV of a decision
- Fast enough for billions of hands. Pure Python by design
- A UI. It is a library; you draw the table yourself
Speed, from the project's own benchmark (one core, i7-1255U)
| Task | PokerKit | Compare |
|---|---|---|
| Full hands simulated | 847 / s | fine for testing and replay, slow for CFR on Hold'em |
| 5-card hand evaluations | 1,016,741 / s | treys: 3,230,966 / s |
Rule of thumb: prototype with PokerKit, and when a solver needs raw speed, keep PokerKit as the referee that checks your fast engine gives the same payoffs.
Section 6 · Read 3
Recall cards: answer out loud, then open
In one sentence, what is PokerKit?
The dealer, not the player. It enforces the rules and moves the chips; it never chooses actions.
Name the four jobs and one class for each.
Dealer (NoLimitTexasHoldem / State), Judge (StandardHighHand), Calculator (calculate_equities), Scribe (HandHistory).
What four steps does every PokerKit program repeat?
Create a state, ask (actor_index, can_*), act, read (stacks, payoffs). Loop while state.status.
Blinds 1/2. Player raises to 6. What do you call?
complete_bet_or_raise_to(6). The amount is the total, not the increment.
Heads-up: who is player 0, and who acts first preflop?
Player 0 is the big blind. Player 1 is the button/small blind and acts first preflop.
What happens when you make an illegal move?
ValueError, and the state does not change. Use can_* to check first.
You want to choose the hole cards yourself. What do you change?
Remove Automation.HOLE_DEALING (and BOARD_DEALING) from the automations, then call deal_hole('AsKs').
Which one-line test catches most engine bugs?
assert sum(state.payoffs) == 0 after every hand (no rake).
What is the difference between game and state?
game holds the rules and blinds; calling game(stacks, players) makes a new state, which is one hand.
In a .phh file, what does p2 cbr 8 mean?
Player 2 (Python index 1) completes, bets or raises to 8.