Question: Three-card showdown between two players
Companies: OpenAI
Difficulty: Easy
Problem statement
Design a two-player card game that uses a standard 52-card deck.
Each card has a rank and a suit. From lowest to highest, the ranks are 2 3 4 5 6 7 8 9 10 J Q K A. The suits are C D H S. Suits never affect who wins.
Dealing. Draw cards from the top of the deck one at a time. Give them to player 1, then player 2, then player 1, and so on, until each player has three cards. Record every card dealt and the player who received it.
Hand categories, from strongest to weakest:
- Three of a kind: all three cards have the same rank.
- Pair: exactly two cards have the same rank.
- High card: all three ranks are different.
The hand in the stronger category wins. If both hands are in the same category:
- Three of a kind: the higher rank wins.
- Pair: compare the ranks of the pairs. If they are equal, compare the third card (the kicker).
- High card: compare the highest cards, then the second-highest, then the lowest.
If every compared rank is equal, the game is a draw.
Implement
- A deck that can be built from an exact list of cards (for tests) or shuffled, with an optional seed.
-
deal(): deals the six cards as described and records the deal order. -
winner() -> int: returns1or2for the winning player, or0for a draw. -
report() -> str: shows the deal log, both hands with their categories, and the result.
Example 1
Deck order (top first): 9C, QH, 9S, 4D, 2H, 4C
- Player 1 gets
9C 9S 2H: a pair of 9s. - Player 2 gets
QH 4D 4C: a pair of 4s. - Result: player 1 wins, because the pair of 9s beats the pair of 4s. Player 2's queen does not matter.
Example 2
- Player 1 holds
KS 8D 3Cand player 2 holdsKH 8C 3S. - Both hands are high card with the same ranks, K, 8 and 3, so the result is
0(draw).
Constraints
- Each game deals exactly six cards from one deck, and no card is dealt twice.
- If the deck runs out during the deal, raise an error.
Reconstructed from the reported question; details may differ from what interviewers ask.
Approach
The game has three separate jobs, and keeping them apart is what makes it easy to extend:
- Deck: hands out cards in a fixed order. Tests pass in an exact order; real games build all 52 cards and shuffle them with an optional seed.
-
Dealing: draw one card at a time and give it to player 1, player 2, player 1, and so on, until each hand has three cards. Write each
(player, card)to a deal log as you go. - Scoring: turn each hand into a strength key that Python can compare as a tuple. Comparing the two keys gives the winner, and equal keys mean a draw.
Brute force. The obvious first version is a chain of if branches: check both hands for three of a kind, then for pairs, then compare pair ranks, then kickers, then high cards one by one. It works, but each category needs its own tie-break code, so it is easy to get wrong.
Better: one tuple key. Count how many cards share each rank. Sort the (rank, count) groups by count first and rank second, both descending. The key is (category, ranks in that order):
- Three of a kind
7 7 7gives(3, (7,)). - Pair
K K 2gives(2, (13, 2)). The pair rank comes first, then the kicker. - High card
A Q 5gives(1, (14, 12, 5)).
Tuples compare element by element, so this one comparison applies every rule: category first, then the triple or pair rank, then the kicker, then high cards from highest to lowest. Suits are never part of the key, so they cannot affect the result. Two hands draw only when their keys are equal.
Invariants
- After
deal(), each player has exactly three cards and the deal log has six entries in the order 1, 2, 1, 2, 1, 2. - A card is drawn from the deck at most once, so no card appears in both hands.
-
hand_keydepends only on ranks, never on suits or card order.
Code
from __future__ import annotations
import random
from collections import Counter
from dataclasses import dataclass
from enum import IntEnum
RANKS = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
SUITS = ["C", "D", "H", "S"]
RANK_VALUE = {r: i for i, r in enumerate(RANKS, start=2)} # "2" -> 2 ... "A" -> 14
HAND_SIZE = 3
class Category(IntEnum):
HIGH_CARD = 1
PAIR = 2
THREE_OF_A_KIND = 3
@dataclass(frozen=True)
class Card:
rank: str
suit: str
def __post_init__(self) -> None:
if self.rank not in RANK_VALUE or self.suit not in SUITS:
raise ValueError(f"bad card {self.rank}{self.suit}")
def __str__(self) -> str:
return f"{self.rank}{self.suit}"
class Deck:
def __init__(self, cards: list[Card] | None = None, seed: int | None = None) -> None:
if cards is None:
cards = [Card(r, s) for s in SUITS for r in RANKS]
random.Random(seed).shuffle(cards)
self._cards = list(cards) # top of the deck is index 0
self._next = 0
def draw(self) -> Card:
if self._next >= len(self._cards):
raise RuntimeError("deck is empty")
card = self._cards[self._next]
self._next += 1
return card
def hand_key(hand: list[Card]) -> tuple[int, tuple[int, ...]]:
"""Comparable strength: (category, tie-break ranks, most important first)."""
if len(hand) != HAND_SIZE:
raise ValueError("hand must have exactly three cards")
counts = Counter(RANK_VALUE[c.rank] for c in hand)
# Group size first, then rank: the triple/pair rank leads, kickers follow high to low.
ordered = sorted(counts.items(), key=lambda kv: (kv[1], kv[0]), reverse=True)
top_count = ordered[0][1]
category = {3: Category.THREE_OF_A_KIND, 2: Category.PAIR, 1: Category.HIGH_CARD}[top_count]
return category, tuple(rank for rank, _ in ordered)
class CardGame:
PLAYERS = (1, 2)
def __init__(self, deck: Deck) -> None:
self.deck = deck
self.hands: dict[int, list[Card]] = {p: [] for p in self.PLAYERS}
self.deal_log: list[tuple[int, Card]] = [] # (player, card) in deal order
def deal(self) -> None:
for _ in range(HAND_SIZE):
for player in self.PLAYERS: # alternate 1, 2, 1, 2, ...
card = self.deck.draw()
self.hands[player].append(card)
self.deal_log.append((player, card))
def winner(self) -> int:
"""1 or 2 for the winning player, 0 for a draw."""
k1, k2 = hand_key(self.hands[1]), hand_key(self.hands[2])
if k1 == k2:
return 0
return 1 if k1 > k2 else 2
def report(self) -> str:
lines = [f"P{p} <- {c}" for p, c in self.deal_log]
for p in self.PLAYERS:
cat, _ = hand_key(self.hands[p])
cards = " ".join(map(str, self.hands[p]))
lines.append(f"Player {p}: {cards} ({cat.name.replace('_', ' ').lower()})")
w = self.winner()
lines.append("Result: draw" if w == 0 else f"Result: player {w} wins")
return "\n".join(lines)
def play(cards: list[Card]) -> CardGame:
game = CardGame(Deck(cards))
game.deal()
return game
play(cards) deals a fixed card order, which keeps tests deterministic. CardGame(Deck(seed=42)) plays a shuffled game.
Complexity
-
Deck.__init__: O(52) time and space to build and shuffle the deck. -
Deck.draw: O(1). It moves an index forward instead of popping from the front of a list. -
deal: O(H·P) for H cards per hand and P players. That is 6 draws here. -
hand_key: O(H log H) to count and sort. With H = 3 this is constant. -
winner: O(H log H).report: O(H·P) time and output size. - Space: O(H·P) for the hands and the deal log.
Test cases
Each line gives player 1's hand, then player 2's hand. Cards are dealt alternately: P1[0], P2[0], P1[1], and so on.
-
7H 7S 7DvsAH AS KD: player 1 wins. Three of a kind beats a pair. -
2H 2S 2DvsAC KD QH: player 1 wins. The lowest three of a kind still beats the best high card. -
JH JS JDvsQC QD QH: player 2 wins. Both have three of a kind, so the higher rank wins. -
4H 4S 9Dvs3C 3D AH: player 1 wins. The pair rank counts before the kicker, even when the other kicker is an ace. -
4H 4S 2Dvs4C 4D 3H: player 2 wins. Both have a pair of 4s, so the kicker decides (3 > 2). -
AH KS 2DvsAC QD JH: player 1 wins. High card: aces tie, then K beats Q. -
5H 3S 2Dvs5C 4D 2H: player 2 wins. The second-highest card decides. -
10H 9S 2Dvs10C 9D 2H: draw. The ranks are the same and only the suits differ. - The deal log for any game lists players in the order
[1, 2, 1, 2, 1, 2]. -
hand_keyforKH 2S KDis(PAIR, (13, 2)). The pair goes first even though2is the middle card. - A deck with fewer than six cards raises
RuntimeError("deck is empty")duringdeal(). - A card such as
Card("1", "H")orCard("2", "X")raisesValueError.
Follow-ups
-
Add straights and flushes (three-card poker). Add
STRAIGHT,FLUSHandSTRAIGHT_FLUSHto the enum in the right order. In three-card poker a straight beats a flush. Detect a straight from three distinct sorted ranks. TreatA 2 3as a special case and score it as high card 3.hand_keystill returns(category, ranks), sowinnerdoes not change. -
N players and bigger hands. Make
PLAYERSandHAND_SIZEconstructor parameters. Find the winner withmaxover the keys, and report every player tied at the top as a split. The deal loop already alternates in round-robin order. - Several rounds and a scoreboard. Keep one deck across rounds and deal until fewer than P·H cards remain, or reshuffle. Track wins per player. Clear the hands each round and keep the full log with a round number.
-
Replay and audit. The deal log is enough to rebuild any game. Add
CardGame.from_log(log)and check that replaying it gives the same hands and result. This matters when the deck is shuffled with a seed. -
Wild cards or a different rank order. Pass the rank order in as data, for example ace-low. For wild cards, give the wild card each possible rank and keep the best
hand_key. With 13 ranks per wild card this is still cheap.
Interviewer expectations
Mid-level (L4)
- Code that runs: deals alternately, records who got each card, and prints both hands and the result.
- Correct category order and tie-breaks, including the pair-then-kicker rule and draws.
- A few hand-written tests, including a kicker tie and a suit-only draw.
Senior (L5)
- A clean split between deck, dealing and scoring, so tests can pass in a fixed deck instead of mocking randomness.
- One comparable strength key instead of nested
ifs. Can explain why a tuple comparison applies every tie-break rule. - Handles an empty deck and invalid cards. Uses an index or iterator for
draw, notpop(0).
Staff (L6+)
- Plans for extensions early: more categories, N players, splits and multiple rounds fit without rewriting the comparison.
- Discusses how to keep the game reproducible and auditable: a seeded shuffle, an append-only deal log and replay.
- Talks about testing strategy, such as property tests checking that hand strength is the same for any card order or suit change, and then moves on to the follow-ups on their own.