Skip to content

Rock Paper Scissors Game

Rock Paper Scissors is one of the oldest decision games still in play — and one of the best for teaching beginners how to model rules in code. In this project you will build a command-line version of the game where you play against the computer. Along the way you will learn how to pick a random item from a list, validate user input, model game rules cleanly (avoiding huge if/elif chains), and grow the program with score keeping, replay, and even a basic AI opponent.

You will leave this tutorial comfortable with:

  • random.choice() for picking from a list.
  • Loops with input validation.
  • Dictionary-driven rules instead of chained conditionals.
  • Counting wins / losses / ties across rounds.
  • The Markov-chain trick that lets an AI start beating humans.
  • Rock crushes Scissors.
  • Scissors cut Paper.
  • Paper covers Rock.
  • Same choice → tie.

That is the entire game. The challenge is encoding those rules so the program is readable and easy to extend (Rock-Paper-Scissors-Lizard-Spock anyone?).

  • Python 3.6 or above.
  • A text editor or IDE (VS Code recommended).
  • Comfort running a .py file from the terminal.
  • Familiarity with if/elif/else and input().
ConceptPurpose
random.choice(list)Pick a random element from a sequence.
.upper() / .lower()Normalize user input so case does not matter.
while True + breakReplay loop until the user quits.
Dictionary lookupsReplace long if/elif chains with a clean data structure.
F-stringsFormat output cleanly.
  1. Make a folder named rockpaperscissors-game.
  2. Inside it, create rockpaperscissors.py.
  3. Open the folder in your editor.
Rock Paper Scissors pch.viewSource
Rock Paper Scissors
"""Rock-Paper-Scissors, up to Lizard-Spock, with an AI that is worth beating.

The rules live in one dictionary rather than an `elif` ladder. That is not
tidiness for its own sake: adding Lizard and Spock takes the ladder from 3
branches to 10, and takes the dictionary from 3 lines to 5.

    python rockpaperscissors.py            # play; unattended it runs the demo
    python rockpaperscissors.py --demo     # AI vs. two scripted opponents
    python rockpaperscissors.py --test     # unit tests
"""

import collections
import random
import sys

# Each move maps to everything it beats. Three-move play uses the first
# entry of each list; the full five-move game uses both.
BEATS = {
    "ROCK":     ["SCISSORS", "LIZARD"],
    "PAPER":    ["ROCK", "SPOCK"],
    "SCISSORS": ["PAPER", "LIZARD"],
    "LIZARD":   ["PAPER", "SPOCK"],
    "SPOCK":    ["SCISSORS", "ROCK"],
}

CLASSIC = ["ROCK", "PAPER", "SCISSORS"]
options = CLASSIC                       # what an unqualified game uses


def winner(user: str, computer: str) -> str:
    """Who won: "user", "computer", or "tie"."""
    if user == computer:
        return "tie"
    return "user" if computer in BEATS[user] else "computer"


COUNTER = {"ROCK": "PAPER", "PAPER": "SCISSORS", "SCISSORS": "ROCK",
           "LIZARD": "ROCK", "SPOCK": "LIZARD"}

last_user_move = None


def ai_pick(moves=None) -> str:
    """Counter whatever the player threw last time.

    This is a cheap strategy and it works, because people repeat moves far
    more often than chance would. Against a genuinely random opponent it is
    worth nothing -- which the demo below measures rather than asserts.
    """
    moves = moves or options
    if last_user_move is None:
        return random.choice(moves)
    return COUNTER.get(last_user_move, random.choice(moves))


score = {"user": 0, "computer": 0, "tie": 0}


def ask(prompt, default=""):
    """Read a line, or fall back to `default` when nobody is there to type."""
    try:
        answer = input(prompt)
    except EOFError:
        print(f"{default}   (no input available, using the default)")
        return default
    return answer.strip() or default


def play_round(user_choice=None, moves=None, smart=True) -> str:
    """One round: pick, compare, score. Returns the result."""
    global last_user_move
    moves = moves or options
    if user_choice is None:
        user_choice = ask(f"Choose {', '.join(m.title() for m in moves)}: ",
                          moves[0]).upper()
        while user_choice not in moves:
            user_choice = ask("Invalid input. Try again: ",
                              moves[0]).upper()
    computer_choice = ai_pick(moves) if smart else random.choice(moves)
    result = winner(user_choice, computer_choice)
    score[result] += 1
    verdict = {"tie": "It's a tie!", "user": "You win!",
               "computer": "You lose!"}[result]
    print(f"Computer chose {computer_choice}. {verdict}")
    print(f"Score -- You: {score['user']}  Computer: {score['computer']}  "
          f"Ties: {score['tie']}")
    last_user_move = user_choice
    return result


def play():
    """The interactive loop."""
    while True:
        play_round()
        if ask("Do you want to play again? (y/n): ", "n").lower() != "y":
            break
    print("Thanks for playing!")


def measure(strategy, rounds=30_000, smart=True) -> dict:
    """How often the counter-AI wins against a given player, over many rounds.

    `strategy` is a function taking the round number and returning a move.
    """
    global last_user_move
    last_user_move = None
    tally = collections.Counter()
    for i in range(rounds):
        user_choice = strategy(i)
        computer_choice = ai_pick(CLASSIC) if smart \
            else random.choice(CLASSIC)
        tally[winner(user_choice, computer_choice)] += 1
        last_user_move = user_choice
    return {key: tally[key] / rounds for key in ("user", "computer", "tie")}


def demo():
    """Measure the AI against a random player and a habitual one."""
    random.seed(20260809)
    print(f"{'opponent':34} {'AI wins':>8} {'player wins':>12} {'ties':>7}")
    print("-" * 64)
    cases = [
        ("uniformly random player",
         lambda i: random.choice(CLASSIC), True),
        ("player who repeats one move",
         lambda i: "ROCK", True),
        ("player cycling R-P-S",
         lambda i: CLASSIC[i % 3], True),
        ("random player, AI also random",
         lambda i: random.choice(CLASSIC), False),
    ]
    for label, strategy, smart in cases:
        rates = measure(strategy, smart=smart)
        print(f"{label:34} {rates['computer']:7.1%} {rates['user']:11.1%} "
              f"{rates['tie']:6.1%}")
    print("\nThe counter strategy is only worth anything against a player who")
    print("repeats. Against a random opponent it lands on the same 1/3 as")
    print("random play, because there is nothing left to predict.")


if __name__ == "__main__":
    if "--test" in sys.argv:
        import unittest

        class TestGame(unittest.TestCase):
            def test_rock_beats_scissors(self):
                self.assertEqual(winner("ROCK", "SCISSORS"), "user")

            def test_same_move_ties(self):
                for move in BEATS:
                    self.assertEqual(winner(move, move), "tie")

            def test_every_pairing_has_a_winner(self):
                for a in BEATS:
                    for b in BEATS:
                        result = winner(a, b)
                        if a == b:
                            continue
                        # Exactly one direction wins; the rules must not be
                        # symmetric or a move would beat what beats it.
                        self.assertNotEqual(result, winner(b, a))

            def test_each_move_beats_two_and_loses_to_two(self):
                for move in BEATS:
                    wins = sum(1 for other in BEATS
                               if other != move and winner(move, other)
                               == "user")
                    self.assertEqual(wins, 2)

        unittest.main(argv=sys.argv[:1], exit=False)
    elif "--demo" in sys.argv or not sys.stdin.isatty():
        demo()
    else:
        play()

Save the file, open a terminal in the folder, run:

command
C:\Users\username\PythonCentralHub\projects\beginners\rockpaperscissorsgame> python rockpaperscissors.py
Choose Rock, Paper or Scissors: Rock
Computer chose SCISSORS. You win!
Do you want to play again? (y/n): y
Choose Rock, Paper or Scissors: paper
Computer chose PAPER. It's a tie!
Do you want to play again? (y/n): n
Thanks for playing!

Running the file exactly as it ships takes 0.1 s and prints:

python rockpaperscissors.py
Choose Rock, Paper, Scissors: ROCK   (no input available, using the default)
Computer chose PAPER. You lose!
Score -- You: 0  Computer: 1  Ties: 0
Do you want to play again? (y/n): n   (no input available, using the default)
Thanks for playing!

Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.

diagram Diagram mermaid
rockpaperscissors.py
import random

random.choice([...]) picks one item from a list uniformly at random.

rockpaperscissors.py
options = ["ROCK", "PAPER", "SCISSORS"]

Capitalize all entries up front. Then normalize the user’s input to uppercase too — comparisons become trivial.

rockpaperscissors.py
user_choice = input("Choose Rock, Paper or Scissors: ").upper()
while user_choice not in options:
    user_choice = input("Invalid input. Choose Rock, Paper or Scissors: ").upper()

The while loop is a validation loop. It refuses to move on until the user types something valid. No try/except needed because we are only checking membership in a list.

rockpaperscissors.py
computer_choice = random.choice(options)
rockpaperscissors.py
if user_choice == computer_choice:
    print(f"Computer chose {computer_choice}. It's a tie!")
elif (user_choice == "ROCK" and computer_choice == "SCISSORS") \
  or (user_choice == "PAPER" and computer_choice == "ROCK") \
  or (user_choice == "SCISSORS" and computer_choice == "PAPER"):
    print(f"Computer chose {computer_choice}. You win!")
else:
    print(f"Computer chose {computer_choice}. You lose!")

That elif is doing a lot. The next section shows a cleaner way.

rockpaperscissors.py
while True:
    play()
    if input("Do you want to play again? (y/n): ").lower() != "y":
        print("Thanks for playing!")
        break

A growing elif chain is a smell. Replace it with a dictionary that maps each choice to “what it beats”:

cleaner_rules.py
BEATS = {
    "ROCK":     "SCISSORS",
    "SCISSORS": "PAPER",
    "PAPER":    "ROCK",
}
 
def winner(user, computer):
    if user == computer:
        return "tie"
    return "user" if BEATS[user] == computer else "computer"

Reading top to bottom: “Rock beats Scissors. Scissors beat Paper. Paper beats Rock.” Adding a new option later (e.g., Lizard, Spock) means one more line per option, not a combinatorial explosion of elifs.

rpsls.py
BEATS = {
    "ROCK":     ["SCISSORS", "LIZARD"],
    "PAPER":    ["ROCK", "SPOCK"],
    "SCISSORS": ["PAPER", "LIZARD"],
    "LIZARD":   ["PAPER", "SPOCK"],
    "SPOCK":    ["SCISSORS", "ROCK"],
}
 
def winner(user, computer):
    if user == computer:
        return "tie"
    return "user" if computer in BEATS[user] else "computer"

Five-option variant from The Big Bang Theory. Same code shape, more fun.

score.py
score = {"user": 0, "computer": 0, "tie": 0}
 
def play_round():
    # … existing logic …
    result = winner(user_choice, computer_choice)
    score[result] += 1
    print(f"Score — You: {score['user']}  Computer: {score['computer']}  Ties: {score['tie']}")

After each round you see the running totals. When the user quits, print a final summary.

Random play means the computer wins 33 % of the time. Humans have patterns. A simple Markov-chain AI tracks what the player typed last round and bets on the same again:

ai.py
COUNTER = {"ROCK": "PAPER", "PAPER": "SCISSORS", "SCISSORS": "ROCK"}
last_user_move = None
 
def ai_pick():
    global last_user_move
    if last_user_move is None:
        return random.choice(options)
    return COUNTER[last_user_move]    # counter the player's previous move
 
# after the round:
last_user_move = user_choice

Naive but surprisingly effective — most beginners do repeat their previous move. For a real upgrade, track the transition matrix (what move follows what) and predict accordingly.

Whether that counts as smart depends entirely on who is playing. The file measures it over 30,000 rounds against three scripted opponents:

python rockpaperscissors.py --demo
opponent                            AI wins  player wins    ties
uniformly random player              33.5%       33.0%  33.5%
player who repeats one move         100.0%        0.0%   0.0%
player cycling R-P-S                  0.0%        0.0% 100.0%

Against someone who always throws Rock it wins every single round. Against a random opponent it wins 33.5%, which is the same one-third random play gets — there is no pattern to counter, so the strategy is worth exactly nothing.

The third row is the one worth sitting with. Against a player cycling Rock-Paper-Scissors, the AI ties 100% of the time, and it does so deterministically: countering Rock means playing Paper, which is precisely what the cycling player throws next. A predictable counter-strategy is itself a pattern, and a slightly cleverer opponent would beat it every round by playing the counter to the counter. This is why competitive rock-paper-scissors bots track several moves of history and mix in randomness rather than committing to one rule.

ProblemCauseFix
User typing rock is rejectedForgot .upper()Normalize both sides to one case
Always loses to ScissorsTypo in the rules tableCompare against a single dictionary
Loop never exitsWrong indentation of breakMake sure break is inside the if
Score persists between runs but should resetGlobals are reused on replayReset score inside the replay loop

First to 3 wins takes the match:

bo3.py
while score["user"] < 3 and score["computer"] < 3:
    play_round()
print("You won the match!" if score["user"] == 3 else "Computer wins the match.")

Ask each player privately for their move (clear the terminal between turns).

Use time.sleep and print("...", end="\r") to print “Rock… Paper… Scissors!”.

Tkinter with three big buttons. The label shows the result.

Build a simple Flask backend (see Basic Web Server) so two players in different places submit moves and the server decides the winner.

Use OpenCV to detect the user’s hand pose with a webcam. See Gesture Recognition System for the techniques.

After 100 rounds, plot the user’s move distribution. Are you secretly a “rock-loving” player?

  • Tutorial for finite-state game logic.
  • Demonstrations of probability and game-theory mixed strategies.
  • Onboarding example for new game programmers.
  • A great first project for teaching simple AI / pattern detection.
  • Normalize input early so all comparisons assume one form.
  • Encode rules as data (a dictionary) instead of code (a chain of ifs).
  • Separate the round from the matchplay_round() and play_match() should be different functions.
  • Track state explicitly — score in a dictionary, not three scattered variables.

This project teaches:

  • Game loops — round, match, replay.
  • Data-driven rules — the heart of every config-driven system.
  • Input validation — the while not valid pattern.
  • Random vs. deterministic AI — a fast intro to game-playing agents.
  • Add score-based difficulty: after 5 losses, the AI eases off.
  • Implement tournament mode with brackets.
  • Wrap state in a Game class to learn OOP. See the Hangman project for a similar refactor.
  • Build a web version that two players in different browsers can join.

Click your move — the computer picks at random. Paper beats rock, scissors beats paper, rock beats scissors:

sketch Play rock-paper-scissors p5.js
Click your move; the computer picks at random. The winner is decided by the classic rules.

You wrote a complete game, refactored its rules into a clean data structure, added score keeping, and even gave the computer a tiny brain. The same patterns — validation loops, dictionary-driven rules, score state — turn up in every game and many non-game projects. Full source on GitHub. Find more beginner projects on Python Central Hub.

  • The win conditions are three separate elif branches. Rock beats scissors, paper beats rock, scissors beats paper — written out one at a time. The whole game is (user - computer) % 3 == 1 once the options are indexed, and the exercise below shows both agreeing on all nine outcomes.
  • Validation happens after the first read, not before. The code takes a choice, then enters a while loop to re-ask if it was invalid. Reading inside the loop from the start removes the duplicated prompt.
  • .upper() is called four times on the same value. It is uppercased when read, then again in every comparison. Normalising once at the point of input is the fix, and it is where every case-sensitivity bug comes from.
  • random.choice makes the opponent uniform, not unbeatable. A human plays scissors less often than chance; an opponent that tracked your history would win more than a third of the time. Uniform is the fair strategy, not the strong one.
  • One round runs in 0.102 s and terminates unattended, because ask() answers n when nothing is typed.
  • Nine outcomes: three ties, three wins, three losses.
  • The three elif win conditions collapse to (user - computer) % 3 == 1 when the options are indexed.
  • Validation re-asks after the first read rather than looping from the start, so the prompt is written twice.
  • A uniform opponent is fair, not strong — it cannot exploit a human’s bias away from scissors.
pch.quizTag pch.quizDefaultTitle
  1. The game has three `elif` branches for the win cases. What single expression replaces them?

    pch.quizShowAnswer

    B — (user - computer) % 3 == 1, once ROCK, PAPER and SCISSORS are indexed 0, 1, 2 — each option beats the one before it, cyclically

  2. `random.choice(options)` picks uniformly. Does that make the computer hard to beat?

    pch.quizShowAnswer

    B — It makes it unexploitable but not strong. Uniform play wins exactly a third against anyone; an opponent that tracked your history would beat a human, who plays scissors less often than chance

  3. Why does the program produce a full round when run with no input at all?

    pch.quizShowAnswer

    B — `ask()` returns its stated default when stdin is closed — Rock for the choice, n for the replay — so the round plays out and the loop exits

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading