Poker Bot Position Ranges: Stop Opening Every Seat
Your poker bot should not play the same hands from every seat. In 6-max No-Limit Hold'em, the button acts last after the flop and the blinds act first. That one fact changes which hands print chips and which hands quietly bleed your stack.
Part of: The Complete Guide to Building an AI Poker Bot in 2026 - the full pillar covering frameworks, decision logic, equity, testing, and where to compete.
Key Takeaways
- Position ranges are a cheap edge because they fix preflop mistakes before the hand gets expensive.
- Early seats need tight opens. Late seats can steal more often because fewer players remain.
- Implement ranges as hand-code sets first. Add opponent modeling only after the baseline works.
Why does position matter so much?
Position matters because late players make decisions with more information. Open Poker's own strategy docs call out early position, late position, and blinds as separate decision contexts (actions and strategy docs). In a 6-max game, each orbit has six seats and 30 forced chips in blinds at the 10/20 structure.
A bot that opens KTo under the gun has five players left to punish it. A bot that opens the same hand on the button only has two blinds left to act. Same cards. Different risk.
This is where beginner bots lose quietly. They build a single hand_strength() number, then use it from every seat. That works for obvious hands like aces. It fails on the hands that decide most seasons: suited connectors, weak aces, small pairs, and broadway trash.
What are the six useful seat buckets?
Use six buckets: UTG, HJ, CO, BTN, SB, and BB. Open Poker sends dealer_seat in hand_start; your bot knows its own seat from table_joined and can compute relative position from those two numbers (WebSocket docs).
POSITIONS_6MAX = ["SB", "BB", "UTG", "HJ", "CO", "BTN"]
def position_name(hero_seat, dealer_seat, active_seats):
ordered = sorted(active_seats)
dealer_index = ordered.index(dealer_seat)
relative = ordered[(dealer_index + 1) % len(ordered):] + ordered[:dealer_index + 1]
# relative[0] is small blind, relative[1] is big blind in a full hand.
seat_to_position = {
seat: POSITIONS_6MAX[i]
for i, seat in enumerate(relative[: len(POSITIONS_6MAX)])
}
return seat_to_position[hero_seat]Real tables can be short-handed. If only three bots are seated, collapse toward BTN, SB, and BB logic. Don't pretend there is an under-the-gun seat when only three players are active.
What opening ranges should a first bot use?
Start tighter than you think. A practical beginner baseline is 12% from UTG, 16% from HJ, 22% from CO, 35% from BTN, and a cautious small blind range. These are not solver-perfect. They are safe enough to stop the worst leaks while your postflop logic catches up.
| Position | Open target | Example hands |
|---|---|---|
| UTG | 12% | 77+, AJs+, KQs, AQo+ |
| HJ | 16% | 66+, ATs+, KJs+, QJs, AJo+ |
| CO | 22% | 55+, A8s+, KTs+, QTs+, JTs, ATo+, KQo |
| BTN | 35% | 22+, most suited aces, broadways, suited connectors |
| SB | 28% | Raise hands that play well heads-up, fold junk |
| BB | Defend by price | Call based on pot odds and hand playability |
The exact percentages matter less than the shape. Early tight. Button wide. Blinds weird. That shape fixes more beginner bots than another postflop heuristic.
For stack depth, combine these ranges with poker bot stack management. A 50bb bot and a 250bb bot should not treat small pairs the same way.
How do you encode ranges in Python?
Encode hands as short strings: AA, AKs, AQo. Pairs have two letters. Suited hands end in s; offsuit hands end in o. This is compact enough for hand-written ranges and easy to debug in logs.
RANK_ORDER = "23456789TJQKA"
def hand_code(cards):
r1, s1 = cards[0][0], cards[0][1]
r2, s2 = cards[1][0], cards[1][1]
if r1 == r2:
return r1 + r2
high, low = sorted([r1, r2], key=RANK_ORDER.index, reverse=True)
suited = "s" if s1 == s2 else "o"
return high + low + suited
OPEN_RANGES = {
"UTG": {"77", "88", "99", "TT", "JJ", "QQ", "KK", "AA", "AJs", "AQs", "AKs", "KQs", "AQo", "AKo"},
"HJ": {"66", "77", "88", "99", "TT", "JJ", "QQ", "KK", "AA", "ATs", "AJs", "AQs", "AKs", "KJs", "KQs", "QJs", "AJo", "AQo", "AKo"},
"CO": {"55", "66", "77", "88", "99", "TT", "JJ", "QQ", "KK", "AA", "A8s", "A9s", "ATs", "AJs", "AQs", "AKs", "KTs", "KJs", "KQs", "QTs", "QJs", "JTs", "ATo", "AJo", "AQo", "AKo", "KQo"},
"BTN": {"22", "33", "44", "55", "66", "77", "88", "99", "TT", "JJ", "QQ", "KK", "AA", "A2s", "A3s", "A4s", "A5s", "A6s", "A7s", "A8s", "A9s", "ATs", "AJs", "AQs", "AKs", "K9s", "KTs", "KJs", "KQs", "Q9s", "QTs", "QJs", "J9s", "JTs", "T9s", "98s", "87s", "A9o", "ATo", "AJo", "AQo", "AKo", "KTo", "KJo", "KQo", "QJo"},
}
def should_open(position, cards):
return hand_code(cards) in OPEN_RANGES.get(position, set())This looks crude, which is a feature. When your bot punts a stack, you can print position=CO hand=KTo open=False and immediately understand the decision.
How do you use ranges with valid actions?
Only open when there is no outstanding bet and a raise is legal. Open Poker tells you exactly which actions are valid, including raise with minimum and maximum amounts. The raise amount is raise-to, not increment, which is a common bot bug (actions docs).
def preflop_decision(msg, hole_cards, position):
actions = {a["action"]: a for a in msg["valid_actions"]}
# Free option in the big blind.
if "check" in actions:
return "check", None
if "raise" in actions and should_open(position, hole_cards):
return "raise", actions["raise"]["min"]
# Facing a raise: use a tighter continue range for now.
if "call" in actions and hand_code(hole_cards) in {"TT", "JJ", "QQ", "KK", "AA", "AQs", "AKs", "AKo"}:
return "call", None
return "fold", NoneThat calling range is intentionally narrow. Calling too wide is one of the fastest ways to turn a decent preflop bot into a losing postflop bot. Once you have Monte Carlo equity, widen calls based on price and opponent tendencies.
When should ranges change?
Ranges should change when stack depth, table size, or opponents change. Open Poker buy-ins can range from 1,000 to 5,000 chips, while every season starts with 5,000 chips and fixed 10/20 blinds (season docs). That means the same bot can play 50bb, 100bb, or 250bb poker.
Use three adjustments:
| Context | Adjustment |
|---|---|
| Short stack under 60bb | Remove speculative suited connectors and small pairs |
| Deep stack over 150bb | Add suited connectors and pairs in late position |
| Loose blinds | Tighten steals, value-bet harder |
| Tight blinds | Widen button and cutoff opens |
Don't add all four on day one. First prove the baseline range beats your old single-score strategy. Then let opponent modeling adjust the button and blind spots.
How do you test a new range?
Test ranges with one change at a time. Preflop edits look small, but they alter every later street because your bot reaches the flop with a different mix of hands. If you widen the button, loosen big-blind defense, and change bet sizing in one deploy, you won't know which change helped.
Use this testing loop:
- Pick one position, usually BTN or BB.
- Change 5 to 10 hands in that range.
- Run at least 300 hands.
- Compare chip trend, rebuys, and big losing hands.
- Keep the change only if the log explains the result.
The fastest signal is not win rate. It is "hands that entered preflop and lost more than 20bb." Pull those hands first. If the new button range adds five marginal offsuit hands and those hands keep losing bloated pots, revert them. If they steal blinds and rarely see showdowns, keep testing.
Range work rewards boring discipline. Good bots do not need heroic preflop creativity. They need fewer dominated hands in early position and more controlled pressure in late position.
FAQ
Should a poker bot use solver-perfect ranges?
Not at first. Solver ranges assume a specific game tree and opponent response. A simple 6-max range table is easier to test, easier to log, and usually better than a copied chart your bot can't play postflop.
How does the big blind defend?
The big blind should defend by price. If calling 20 into a 90-chip pot, you need much less equity than if calling 200 into 300. Start with pot odds, then filter out hands that play badly postflop.
What range should I use for a no-code bot?
Start with The Shark template. The Bot Builder docs list it as tight-aggressive with 0.30 tightness and 0.75 aggression, which is a better baseline than loose templates for most new users.
Can position ranges replace hand evaluation?
No. Ranges decide whether to enter the hand preflop. Hand evaluation and equity decide what to do after the board appears. Use both.