Summary
In a 2-player (heads-up) game, PyPokerEngine assigns the blinds and the
betting order the same way it does for 3+ player tables. This violates the
standard rules of heads-up poker.
Standard heads-up rules
- The dealer button posts the small blind (the non-button player posts the
big blind).
- Preflop: the button/small blind acts first.
- Postflop: the button/small blind acts last (the big blind acts first).
Current (incorrect) behavior
_steal_money_from_poor_player always picks the small blind as the seat to
the left of the button (dealer_btn + 1):
search_targets = search_targets[table.dealer_btn+1:table.dealer_btn+1+len(players)]
For 3+ players this is correct, but for 2 players it makes the non-button
player the small blind — the opposite of the standard rule.
The action order in RoundManager is derived from the same assumption:
__start_street seeds the first postflop actor from sb_pos().
__preflop advances two seats from the small blind.
Both are correct for 3+ players but wrong heads-up, where the button must act
first preflop and last postflop.
Impact
- Every heads-up hand posts the blinds on the wrong players.
- Preflop and postflop betting order is inverted heads-up.
- This logic is duplicated in two places, so both code paths are affected:
pypokerengine/engine/dealer.py — used by api.game.start_poker
pypokerengine/api/emulator.py — used by the Emulator
This is significant for anyone training agents on heads-up play (e.g. CFR),
since the game tree differs from a correct heads-up game.
Steps to reproduce
from pypokerengine.api.game import setup_config, start_poker
from examples.players.fold_man import FoldMan
config = setup_config(max_round=1, initial_stack=100, small_blind_amount=10)
config.register_player("p1", FoldMan()) # seat 0, on the button (dealer_btn = 0)
config.register_player("p2", FoldMan()) # seat 1
result = start_poker(config)
# Expected (heads-up rules): p1 is the small blind and acts first preflop.
# Actual: p2 is treated as the small blind.
Expected behavior
In heads-up play the button posts the small blind, acts first preflop, and
acts last postflop. 3+ player behavior should be unchanged.
Summary
In a 2-player (heads-up) game, PyPokerEngine assigns the blinds and the
betting order the same way it does for 3+ player tables. This violates the
standard rules of heads-up poker.
Standard heads-up rules
big blind).
Current (incorrect) behavior
_steal_money_from_poor_playeralways picks the small blind as the seat tothe left of the button (
dealer_btn + 1):For 3+ players this is correct, but for 2 players it makes the non-button
player the small blind — the opposite of the standard rule.
The action order in
RoundManageris derived from the same assumption:__start_streetseeds the first postflop actor fromsb_pos().__preflopadvances two seats from the small blind.Both are correct for 3+ players but wrong heads-up, where the button must act
first preflop and last postflop.
Impact
pypokerengine/engine/dealer.py— used byapi.game.start_pokerpypokerengine/api/emulator.py— used by theEmulatorThis is significant for anyone training agents on heads-up play (e.g. CFR),
since the game tree differs from a correct heads-up game.
Steps to reproduce
Expected behavior
In heads-up play the button posts the small blind, acts first preflop, and
acts last postflop. 3+ player behavior should be unchanged.