The 100 problems, solved / Games / 071 · Design an object-oriented poker game

Design an object-oriented poker game

Hands compared deterministically, betting rounds and a table of players.

Concepts this design leans on

01The design, in the six-step framework

1. Requirements and use cases

A reported Google onsite prompt asked for an object-oriented poker game with clear class responsibilities, deterministic hand comparison, and an extensible, testable design. An older report asks for a deck class, a shuffle and poker-hand scoring. The hard core is therefore the comparison, and the rest is a state machine around it.

  • A table seats 2 to 9 players with chip stacks, for Texas Hold'em.
  • Each hand, the dealer deals two private cards and, over the streets, five community cards.
  • In each betting round a player folds, checks or calls, or raises.
  • When one player remains or the river betting ends, the hand finishes.
  • At showdown the system finds the best five-card hand from each player's seven cards, compares them, and splits the pot on an exact tie.

Out of scope: side pots for all-in players, blinds rotation and tournaments, all named so the interviewer can pick one. Constraints: comparison is deterministic and total, so two evaluations of the same cards always agree; variants (Omaha, seven-card stud) reuse the evaluator; randomness is injected so a test can fix the deck.

2. Core entities and relationships

  • Card, Rank, Suit: as in Blackjack; here aces rank high, and also low in one straight.
  • Category: an ordered enum from high card to straight flush.
  • HandValue: an immutable, comparable pair of a category and tiebreak ranks.
  • Evaluator: turns five cards into a HandValue, and picks the best five of seven.
  • Seat: a player's stack, hole cards, street bet and status.
  • Street: preflop, flop, turn, river, showdown.
  • HandRound: one deal: the street, the pot, whose turn it is, and the betting rules.
  • HoleRule: which card combinations are allowed, the strategy that distinguishes variants.
RoundRulesValues2..9returnsHandRoundseats: list[Seat]board: list[Card]street: Streetpot: intcurrent_bet: intto_act: deque[Seat]act(seat, action, amount)advance()showdown() -> dictSeatnamestackhole: list[Card]street_betfolded: boolEvaluatorrank_five(cards) -> HandValuebest(cards, rule) -> HandValueHoleRulecombos(hole, board) -> IterableHandValuecategory: Categorytiebreak: tuple[int, ...]StreetPREFLOPFLOPTURNRIVERSHOWDOWN

The round composes its seats for the life of one hand. The evaluator is stateless, so it is merely used; that keeps it free of table details and trivially testable, which is the point of the separation.

3. Interfaces and key methods

import itertools, random
from collections import Counter, deque
from dataclasses import dataclass, field
from enum import Enum, IntEnum
from typing import Iterable, Protocol, Tuple

class Suit(Enum):
    CLUBS = "c"; DIAMONDS = "d"; HEARTS = "h"; SPADES = "s"

@dataclass(frozen=True)
class Card:
    rank: int          # 2..14, ace is 14
    suit: Suit

class Category(IntEnum):
    HIGH_CARD = 0; PAIR = 1; TWO_PAIR = 2; TRIPS = 3; STRAIGHT = 4
    FLUSH = 5; FULL_HOUSE = 6; QUADS = 7; STRAIGHT_FLUSH = 8

@dataclass(frozen=True, order=True)
class HandValue:
    category: Category
    tiebreak: Tuple[int, ...]

class HoleRule(Protocol):
    def combos(self, hole: list, board: list) -> Iterable:
        """Every legal five-card combination for this variant."""

class Street(Enum):
    PREFLOP = 0; FLOP = 1; TURN = 2; RIVER = 3; SHOWDOWN = 4

class Action(Enum):
    FOLD = "fold"; CALL = "call"; RAISE = "raise"

Invariants: a HandValue depends only on ranks and suits, never on card order; the dataclass ordering compares category first and then the tiebreak, so the comparison is total; the pot equals the sum of all chips moved in; a betting round ends only when every live seat has acted and matched the current bet.

4. Design patterns and the choices behind them

A hand's strength as a comparable value

The force: "who won" has a dozen special cases if written as nested ifs. Reduce it to one idea: every hand maps to a tuple, and tuples compare left to right. A full house of kings over twos becomes (FULL_HOUSE, (13, 2)); two pair becomes (TWO_PAIR, (high pair, low pair, kicker)). Ordering by count then by rank builds the tiebreak in a single line. All the rules of poker's ordering reduce to that encoding, and max then finds the winner.

That is a value object doing exactly what value objects are for. It also settles an identity and equality question: two hands with different suits can be equal in value and split a pot, while the cards themselves remain distinct. So equality of strength belongs on HandValue, not on a Hand of cards.

Variants as a strategy

Hold'em lets any five of seven cards play; Omaha requires exactly two hole cards and three from the board. That is the only difference for evaluation, so HoleRule.combos is a strategy yielding allowed combinations, and best takes the maximum. Enumerating is simple: 21 combinations for Hold'em, and a lookup table is an optimisation worth mentioning rather than building.

The hand as a state machine

The force: betting has strict order, and a raise reopens action. Street moves in one direction only, which is the state idea, and the to_act queue encodes who still must respond. A raise refills the queue with every other live seat, and that one line captures the reopening rule without a has_acted flag per seat. The alternative, looping around the table testing conditions, is where the bugs in this problem usually live.

5. Core flow and code

  1. HandRound receives a shuffled deck, deals two cards per seat and fills to_act.
  2. Each act validates that it is that seat's turn, moves chips, and on a raise refills the queue.
  3. When the queue empties, advance resets street bets and deals the next community cards.
  4. If only one seat is left unfolded, it wins the pot with no showdown.
  5. Otherwise showdown evaluates each live seat and divides the pot among those with the maximum HandValue.
class Evaluator:
    @staticmethod
    def _straight_high(ranks: list) -> int:
        distinct = sorted(set(ranks), reverse=True)
        if len(distinct) == 5:
            if distinct[0] - distinct[4] == 4:
                return distinct[0]
            if distinct == [14, 5, 4, 3, 2]:   # the wheel: the ace plays low
                return 5
        return 0

    @classmethod
    def rank_five(cls, cards: tuple) -> HandValue:
        ranks = [c.rank for c in cards]
        counts = Counter(ranks)
        ordered = sorted(counts, key=lambda r: (counts[r], r), reverse=True)
        shape = sorted(counts.values(), reverse=True)
        flush = len({c.suit for c in cards}) == 1
        high = cls._straight_high(ranks)
        if flush and high:
            return HandValue(Category.STRAIGHT_FLUSH, (high,))
        if shape == [4, 1]:
            return HandValue(Category.QUADS, tuple(ordered))
        if shape == [3, 2]:
            return HandValue(Category.FULL_HOUSE, tuple(ordered))
        if flush:
            return HandValue(Category.FLUSH, tuple(ordered))
        if high:
            return HandValue(Category.STRAIGHT, (high,))
        if shape == [3, 1, 1]:
            return HandValue(Category.TRIPS, tuple(ordered))
        if shape == [2, 2, 1]:
            return HandValue(Category.TWO_PAIR, tuple(ordered))
        if shape == [2, 1, 1, 1]:
            return HandValue(Category.PAIR, tuple(ordered))
        return HandValue(Category.HIGH_CARD, tuple(ordered))

    @classmethod
    def best(cls, hole: list, board: list, rule: HoleRule) -> HandValue:
        return max(cls.rank_five(combo) for combo in rule.combos(hole, board))

class HoldemRule:
    def combos(self, hole: list, board: list) -> Iterable:
        return itertools.combinations(hole + board, 5)

class OmahaRule:
    def combos(self, hole: list, board: list) -> Iterable:
        for h in itertools.combinations(hole, 2):
            for b in itertools.combinations(board, 3):
                yield h + b

@dataclass
class Seat:
    name: str
    stack: int
    hole: list = field(default_factory=list)
    street_bet: int = 0
    folded: bool = False

class HandRound:
    def __init__(self, seats: list, deck: list, rule: HoleRule) -> None:
        self.seats, self.deck, self.rule = seats, deck, rule
        self.board: list = []
        self.street = Street.PREFLOP
        self.pot = 0
        self.current_bet = 0
        for s in seats:
            s.hole = [self.deck.pop(), self.deck.pop()]
        self.to_act = deque(seats)

    def live(self) -> list:
        return [s for s in self.seats if not s.folded]

    def act(self, seat: Seat, action: Action, amount: int = 0) -> None:
        if not self.to_act or self.to_act[0] is not seat:
            raise ValueError("not your turn")
        self.to_act.popleft()
        if action is Action.FOLD:
            seat.folded = True
        else:
            target = self.current_bet + (amount if action is Action.RAISE else 0)
            pay = min(target - seat.street_bet, seat.stack)
            seat.stack -= pay
            seat.street_bet += pay
            self.pot += pay
            if action is Action.RAISE:
                self.current_bet = target
                i = self.seats.index(seat)      # everyone else must respond, in seat order
                order = self.seats[i + 1:] + self.seats[:i]
                self.to_act = deque(s for s in order if not s.folded)
        if not self.to_act or len(self.live()) == 1:
            self.advance()

    def advance(self) -> None:
        if len(self.live()) == 1:
            self.street = Street.SHOWDOWN
            return
        self.street = Street(self.street.value + 1)
        for s in self.seats:
            s.street_bet = 0
        self.current_bet = 0
        if self.street is not Street.SHOWDOWN:
            count = 3 if self.street is Street.FLOP else 1
            self.board += [self.deck.pop() for _ in range(count)]
            self.to_act = deque(self.live())

    def showdown(self) -> dict:
        live = self.live()
        if len(live) == 1:
            return {live[0].name: self.pot}
        values = {s.name: Evaluator.best(s.hole, self.board, self.rule) for s in live}
        top = max(values.values())
        winners = sorted(n for n, v in values.items() if v == top)
        share, odd = divmod(self.pot, len(winners))
        return {n: share + (1 if i < odd else 0) for i, n in enumerate(winners)}

if __name__ == "__main__":
    def hand(text: str) -> tuple:
        ranks = {"T": 10, "J": 11, "Q": 12, "K": 13, "A": 14}
        return tuple(Card(ranks.get(t[0]) or int(t[0]), Suit(t[1])) for t in text.split())
    rank = Evaluator.rank_five
    assert rank(hand("Ah 2h 3h 4h 5h")).category is Category.STRAIGHT_FLUSH
    assert rank(hand("Kh Kd Kc 2s 2d")) > rank(hand("Qh Qd Qc As Ad"))
    assert rank(hand("Ah Ad 9c 8s 2d")) > rank(hand("Ks Kd Qc Js Td"))
    assert rank(hand("2h 2d 3c 3s 9d")) == rank(hand("2c 2s 3h 3d 9s"))

6. Extensions, concurrency and what interviewers probe

  • "Make the comparison deterministic." It is: HandValue depends only on the multiset of cards, and the last assert shows two suits tying exactly. Odd chips go to winners in sorted name order, which is a stated rule, not an accident of dictionary order.
  • "Side pots." When a seat is all-in for less, split the pot into layers by contribution, and evaluate each layer among the seats eligible for it. It belongs in a Pot class that replaces the single integer.
  • "Speed up evaluation." Enumerating 21 combinations is fine for a table; a simulator would precompute a lookup keyed by rank primes. Say it, then keep the simple form.
  • "Add Omaha." Pass OmahaRule and deal four hole cards; the evaluator is untouched.
  • Concurrency: one table is a single stream of actions, so each table runs on one event loop; many tables are independent, and the stateless evaluator is safe to share.
  • Common mistake: forgetting the wheel (A-2-3-4-5, whose high card is the five), comparing kickers in the wrong order, and failing to reopen action after a raise, so a player who already acted never gets to respond.

02Where this question was reported

  • PracHub · Google onsite, Medium; class responsibilities, deterministic hand comparison, extensible/testable
  • Glassdoor · Create a deck-of-cards class, write shuffle and poker-hand scoring (snippet only; URL mapping from search cluster)