What L5 looks like
- Makes the shuffle deterministic in tests (an injected seed or deck) and keeps each method small.
- Expects ties or duplicates before being asked, and designs the pot or tie rule so part 2 is a small change.
- States the card-conservation invariant and the O(N) total cost, and adds N players with little rework.
Approach
This is an object-design question disguised as a simulation. The loop is trivial, so most of the score comes from choosing clean types, keeping the randomness testable, and handling the edge cases the interviewer will add later.
Model the nouns.
-
Player: a name, ahand(a deque, because cards come off the top), and awonscoring pile. The score is the size of the pile, one point per card. -
CardGame: owns the players, deals the deck, plays rounds, and reports scores. It also keeps apotfor cards from tied rounds. A tie cannot happen with unique cards, but adding the pot now costs nothing.
Deal. Shuffle with a random.Random(seed) that the game owns. Do not use the global random, so tests can replay a game. Tests can also turn shuffling off and pass a fixed order. Deal round-robin: card i goes to player i % n. If the deck does not divide evenly, leave the extra cards out so every player has the same number of turns. State this choice out loud.
Round. Each player takes the top card from their hand. All the played cards go into the pot. If exactly one player played the highest card, that player takes the whole pot. With unique integers this always happens, so the winner gets both cards and 2 points.
End. The game ends when any hand is empty. With an even deal, all hands empty together. The winner has the highest score. If several players share the top score, the result is "tie".
A brute-force or "just lists" version (two halves of the list, zip them, count comparisons) gives the right answer for part 1. It falls apart on the follow-ups (ties, more players, per-round history), so lead with the small class design. You can mention that the one-liner gives the same result.
Invariants
- Every dealt card is in exactly one place: a hand, a scoring pile, or the pot. So
sum(scores) + len(pot) + cards in hands == dealt. - All hands have equal length after every round.
- The pot is empty after any round that has a unique high card.
Code
from __future__ import annotations
import random
from collections import deque
from dataclasses import dataclass, field
from typing import Deque, List, Optional, Sequence
@dataclass
class Player:
name: str
hand: Deque[int] = field(default_factory=deque)
won: List[int] = field(default_factory=list) # scoring pile
@property
def score(self) -> int:
return len(self.won) # 1 point per card
@dataclass
class RoundResult:
played: dict[str, int]
winner: Optional[str] # None means a tie
pot_size: int
class CardGame:
def __init__(self, deck: Sequence[int], names: Sequence[str] = ("A", "B"),
seed: Optional[int] = None, shuffle: bool = True) -> None:
if len(names) < 2:
raise ValueError("need at least two players")
cards = list(deck)
if shuffle:
random.Random(seed).shuffle(cards)
self.players = [Player(n) for n in names]
usable = len(cards) - len(cards) % len(self.players)
for i, card in enumerate(cards[:usable]): # deal round-robin
self.players[i % len(self.players)].hand.append(card)
self.pot: List[int] = [] # cards from tied rounds
self.log: List[RoundResult] = []
def is_over(self) -> bool:
return any(not p.hand for p in self.players)
def play_round(self) -> RoundResult:
if self.is_over():
raise RuntimeError("game is over")
played = {p.name: p.hand.popleft() for p in self.players}
self.pot.extend(played.values())
best = max(played.values())
leaders = [p for p in self.players if played[p.name] == best]
winner: Optional[str] = None
if len(leaders) == 1: # unique high card takes the pot
winner = leaders[0].name
leaders[0].won.extend(self.pot)
self.pot = []
result = RoundResult(played, winner, len(self.pot))
self.log.append(result)
return result
def play(self) -> str:
while not self.is_over():
self.play_round()
return self.winner()
def scores(self) -> dict[str, int]:
return {p.name: p.score for p in self.players}
def winner(self) -> str:
"""Name of the top scorer, or 'tie' if several share the top score."""
s = self.scores()
top = max(s.values())
best = [n for n, v in s.items() if v == top]
return best[0] if len(best) == 1 else "tie"
Complexity
- Construction: O(N) to shuffle and deal
Ncards. -
play_round: O(P) forPplayers (one pop, one max, one scan). Moving the pot costs O(pot size), and each card moves into a pile at most once, so this adds O(N) over the whole game. -
play: O(N) in total, which is N/P rounds of O(P) each. -
scores/winner: O(P). - Space: O(N) for hands, piles and the pot, plus O(N/P) for the round log.
Test cases
-
CardGame([5, 2, 1, 8, 7, 3], shuffle=False): A holds[5, 1, 7]and B holds[2, 8, 3]. The rounds go A, B, A. Scores{"A": 4, "B": 2}, winner"A". -
CardGame([9, 1, 2, 8], shuffle=False): A wins with 9, then B wins with 8. Scores{"A": 2, "B": 2}, winner"tie". - Odd deck
[1, 2, 3, 4, 5]without shuffling: the 5 is not dealt. B wins both rounds. Scores{"A": 0, "B": 4}. - Empty deck: the game is over at once. Scores are 0–0 and the result is
"tie". - Duplicates (follow-up)
[4, 4, 3, 1]: round 1 ties 4–4 and the pot holds 2 cards. A wins round 2 with 3 vs 1 and takes 4 cards. - Three players
[3, 9, 5, 2, 1, 4]with names A, B, C: B takes round 1 with 9 and C takes round 2 with 4. Scores{"A": 0, "B": 3, "C": 3}, result"tie". - Same seed on a 52-card deck: two games produce the same winner and scores, and the scores add up to 52.
- Calling
play_roundafter the game is over raisesRuntimeError.
Follow-ups
-
Cards are not unique. The pot already handles this. A tied round leaves its cards in the pot, and the next unique winner takes them. Cards still in the pot when the hands run out go to nobody, or are split, depending on the rule the interviewer picks. Another rule is a "war": each tied player puts down k face-down cards and then plays again. In that case, change
play_roundto loop over only the tied players, and handle a player who runs out of cards in the middle of a war. -
Winner puts won cards at the bottom of their hand (classic War). The game ends when one player has every card. Now it may never end, so add a cap on the number of rounds, or store a hash of
(tuple(hand_A), tuple(hand_B))in a seen-set and stop when a state repeats. That costs O(N) per round. -
N players, or players joining by name. The design already loops over
self.players. Discuss how to deal an uneven deck (drop the extra cards, or give them out in order) and how to compare when several players share the high card. -
Pluggable rules or strategies. Pass in a
compare(cards) -> winnersfunction or aScoringRule(for example, points equal to the card's value instead of 1 per card). Pass in achoose_card(hand)strategy so a player can play any card from the hand, not only the top one. That is the strategy pattern, and the hand becomes a list or a sorted container. -
Testability and replay. Inject the RNG or the deck, keep the per-round
log, and add astate()snapshot. Then a UI or a server can render each round, and a seeded game can be replayed exactly.
Interviewer expectations
Mid-level (L4)
- Gives a correct simulation with a clear deal, compare and score step, and gets the winner and tie right.
- Has at least a
Playerconcept, not two loose lists, and writes the code without help. - Walks through one small example by hand and asks about an odd-sized deck.
Senior (L5)
- Makes the shuffle deterministic in tests (an injected seed or deck) and keeps each method small.
- Expects ties or duplicates before being asked, and designs the pot or tie rule so part 2 is a small change.
- States the card-conservation invariant and the O(N) total cost, and adds N players with little rework.
Staff (L6+)
- Separates the game engine from the rules (comparison, scoring, end condition) and from player strategy. Explains which follow-ups each extension point handles.
- Notices that the War variant may never end, and proposes cycle detection with its cost.
- Talks about how the design would be used for real: event log and replay, a state snapshot for a UI or service, and validating input such as decks and player names.
Make it yours
Remix it: write each part yourself, grade it against this answer, then let the coach ask you follow-ups.
