Rock Paper Scissors

Hand Game

Design rock paper scissors as a small object model: rules as data, players that choose moves, and a best-of-N match that keeps score, so that a five-move variant needs no new code.

Overview

Two players each pick rock, paper or scissors at the same time. Rock beats scissors, scissors beats paper, paper beats rock, and the same pick is a draw. A match is best of N rounds. That is the whole game, which is why interviewers like it: there is nothing to explain, so all the time goes to your design.

The trap is to write the rules as if statements. It works for three moves and turns into a wall of conditions for five. The design below keeps the rules as a table, checks that the table is balanced, and lets a match run on any table. Players are objects that produce a move when asked; the computer's way of choosing is a strategy you can swap.

A strong answer names the three pieces in the first minute: a rules table, a player interface, a match that keeps score. Then it shows the variant with five moves as a new table and nothing else.

Scoping the problem

  • Human against human, or against the computer? — Decides whether a Player interface is needed and where the computer's choice lives — Both, behind one interface
  • How does a match end? — Best of N needs an odd N and a wins-needed count; draws do not count — Best of N, N odd
  • Do both players move at the same time? — In a local game the UI collects both moves first; over a network you need a commitment step — Both moves are collected before the round is judged
  • Will there be variants? — Rules as data or rules as code — Yes; rules are a table
  • Should the computer learn from the human? — A strategy interface with a history argument — Yes, two strategies
  • Does the computer's randomness need to be reproducible in tests? — The random source is injected — Yes, a seeded random.Random

Rules as data

Rules holds a dict from each move to the set of moves it beats. outcome(a, b) is a lookup: draw if equal, win if b is in a's set, lose otherwise. The constructor rejects a table where a move beats itself, names a move that does not exist, or is unbalanced.

  • Classic — 3 — 1 — 1 — 1 in 3 with random play
  • Lizard Spock (Sam Kass and Karen Bryla) — 5 — 2 — 2 — 1 in 5 with random play

The ten rules of the five-move table: rock crushes scissors and lizard; paper covers rock and disproves Spock; scissors cuts paper and decapitates lizard; lizard eats paper and poisons Spock; Spock smashes scissors and vaporizes rock. In the code that is five lines of data.

The match

The trace below is the demo in the implementation: a human who keeps playing rock against a computer that counters the human's most common move. Best of 5, so three wins end it.

  • 1 — rock — random — No history yet, so the fallback strategy picks — 0 or 1 — 0 or 1
  • 2 — rock — paper — Rock is the human's most common move; paper beats it — +1
  • 3 — scissors — paper — Rock is still the most common move — +1

How the computer chooses

  • Random, uniform — Each move with equal chance — Cannot be exploited: no history helps the opponent — Cannot exploit the opponent either; wins half the decided rounds in the long run
  • Counter the favorite — The move that beats the opponent's most frequent move — Beats a human who has a habit — A human who notices can lead it by the nose
  • Counter the last move — Assumes the opponent repeats or rotates — Catches short patterns — Same

Uniform random play is the game's equilibrium strategy: against it, no strategy does better than even, in the classic game and in the five-move one alike. Anything smarter is a bet that the opponent is predictable.

What you will be able to do

  • Write the rules as a table and check that it is balanced.
  • Draw the entities and say which one knows what.
  • Run a best-of-N match with draws that do not count and an odd N.
  • Add the five-move variant with data only.
  • Explain why uniform random cannot be beaten and when a learning strategy is worth it.

Functional requirements

  • Two players play rounds; each round both moves are collected first, then judged as win, lose or draw for the first player
  • A match is best of N with N odd; it ends when a player reaches N // 2 + 1 wins; draws add a round and no score
  • Human players submit a move before the round; computer players ask a strategy, which sees the opponent's past moves
  • Rules are a table of which move beats which; the classic table is provided and any balanced table is accepted
  • The match refuses a round after it is over and refuses a move that is not in its rule set

Non-functional requirements

  • A new variant is a new rules table; Match, Player, Round and the strategies do not change
  • The rules constructor rejects a table that is unbalanced, self-beating or names unknown moves
  • The computer's randomness is injected so tests are reproducible
  • Rounds are immutable records; the score is derived from them rather than kept in a counter that can drift

Interview questions

  • Why keep the rules in a table instead of a beats() method on the Move enum?
  • Add rock paper scissors lizard Spock. What changes?
  • Which computer strategy is best?
  • Over a network, the second player could wait to see the first move. How do you make the moves simultaneous?
  • Why does best_of have to be odd, and why do draws not count?

Sign up free to read the full topic

More Low-Level Design topics

Start free on Cariara · Coding problems · System design studio