High-Card Game Rules Engine
Asked of: Software Engineer
Last updated

What's being tested
These problems test whether you can turn ambiguous game rules into a precise, stateful model with explicit invariants, termination behavior, and deterministic outcomes. The interviewer is probing data structures, simulation design, algorithmic complexity, object-oriented boundaries, and your ability to generalize from two players to players without duplicating logic. At Affirm, this reflects production engineering fundamentals: correctness under edge cases, reproducible behavior for debugging, and APIs whose rules remain extensible as requirements change.
Core knowledge
-
Domain modeling should separate immutable
Cardvalues (rank,suit), player state, the deck or card ownership, and the rules engine. Avoid embedding game rules insidePlayer; aGameorchestrator plus replaceableRuleSetkeeps extensions testable. -
Represent ownership explicitly: each card is in exactly one location—player hand, active round, persistent pot, or discarded/exhausted state. An invariant such as
sum(player.cards) + pot.size + deck.size == 52catches duplicate-card and lost-card bugs. -
A round-based simulation usually needs
deal(),playRound(),resolveTie(),awardPot(), andisTerminated(). Keep transitions atomic: determine eligible cards, compare ranks, move all played cards into the pot, then award the complete pot to winners. -
For a two-player high-card game, a queue or deque gives amortized removal from the front and insertion at the back. A complete simulation is rounds and space for cards; avoid repeatedly shifting an array.
-
For players, compare each active player's top card in one pass: work per round and temporary space. Uneven round-robin dealing requires an explicit policy—typically player receives the next card while cards remain—rather than assuming equal hand sizes.
-
Tie resolution must define whether all tied players share the pot, continue competing, or trigger a persistent-pot rule. For a persistent pot, add every played card to the pot before the next round; the eventual winner receives
pot + current round cards, preventing tied cards from disappearing. -
Define exhaustion semantics before coding. If a player has no card when a round begins, they may be eliminated, lose immediately, or cause a declared invalid state; selecting one policy is more important than choosing a particular implementation.
-
A deterministic shuffle requires an injectable seeded pseudorandom generator, such as
random.Random(seed), rather than a global RNG. Record the seed and initial deck ordering so a failing game can be replayed exactly in tests and debugging. -
Termination must be a formal predicate, not “the loop will eventually finish.” Typical rules are “one player owns every card,” “only one non-empty player remains,” or “all cards are exhausted”; add a maximum-round guard to detect malformed rules or cycles.
-
For winner reporting, distinguish cards won during play from cards currently held. If the requested result is final ownership, count the winner’s final collection; if it is round victories, maintain a separate score field rather than inferring it from card count.
-
An optimal-play game differs from random simulation: define legal moves, terminal utilities, and whose turn it is, then use minimax for small finite state spaces. Memoization reduces repeated states, while alpha-beta pruning can reduce explored nodes without changing the result.
-
Use property-based tests and invariant checks: every original card appears exactly once, a deterministic seed reproduces the same result, pot size changes consistently, and every completed game satisfies its termination predicate. Test empty decks, all-card ties, duplicate ranks, one player, and .
Worked example — Design a High-Card Game with a Persistent Tie Pot
For “Design a High-Card Game with a Persistent Tie Pot,” I would first clarify whether a tie means all tied players continue, whether non-tied players remain eligible, who receives the pot, and what happens when a player cannot contribute a card. I would declare a concrete default: each active player plays one card, all played cards enter a persistent pot, the highest unique rank wins the pot, and an all-way tie carries forward. I would organize the answer around four pillars: immutable card/deck modeling, player-owned queues, a round-transition state machine, and termination plus invariant validation. I would make ownership explicit so a played card moves from a player queue to the pot and then from the pot to the winner, never existing in two collections. The key tradeoff is whether to physically transfer card objects or track only counts and scores; physical transfer costs little for 52 cards but makes correctness and future rule changes much clearer. I would inject the shuffle RNG and expose the seed so two identical inputs produce identical games. I would close by saying that, with more time, I would add property-based tests, a replay log of state transitions, and a policy interface for exhaustion and tie resolution.
A second angle — Determine winner in optimal-play card game
“Determine winner in optimal-play card game” shifts from simulation to adversarial search: the engine must choose the best legal move rather than consume a predetermined shuffled sequence. I would define the state as the players’ remaining cards, current turn, and any accumulated pot, then define a terminal utility such as win/loss or final score. For small decks, recursive minimax with memoization is straightforward; encode the state canonically so equivalent positions share a cache entry. If the state space grows, alpha-beta pruning, symmetry reduction, or dynamic programming may be necessary, and I would state the practical size limit rather than claiming minimax scales indefinitely. The same emphasis on explicit rules, ownership, terminal conditions, and reproducibility still applies.
Common pitfalls
Pitfall: Treating a tie as merely “no winner this round” and dropping the played cards loses the persistent-pot rule. Move every played card into a pot before deciding whether the pot is awarded or carried forward.
A tempting implementation mutates player arrays while iterating over them, then accidentally skips players or miscounts cards. Prefer a snapshot of eligible players for the round and perform ownership transfers in a clear, ordered phase.
Pitfall: Saying “the game ends when someone wins” is not a sufficient termination rule. State the exact predicate, handle empty players and all-way ties, and include a defensive round limit or cycle detector for invalid configurations.
Connections
The interviewer may pivot to state machines, invariant-based testing, dependency injection for deterministic randomness, or minimax and memoization. They may also ask how the design changes for event sourcing, replayable debugging, or concurrent game sessions.
Practice questions
- Simulate a High-Card Deck Game for Two Players, Then N PlayersAffirm · Software Engineer · Technical Screen · hard
- Determine winner in optimal-play card gameAffirm · Software Engineer · Onsite · medium
- Design a High-Card Game with a Persistent Tie PotAffirm · Software Engineer · Technical Screen · easy
Related concepts
- Connect Four And Grid Game EnginesCoding & Algorithms
- Expense Rules EnginesSystem Design
- Transaction Rule Engines And Expression ParsingSoftware Engineering Fundamentals
- Grid Simulation And Spatial ReasoningCoding & Algorithms
- Match-3 Board SimulationCoding & Algorithms
- Stateful Data Structures And OOP API DesignCoding & Algorithms