I needed a fun weekend project that actually taught me something useful about Python. Rock Paper Scissors seemed simple enough, but I quickly realized there are two versions of this game out there – the classic three-move version and the extended Rock-Paper-Scissors-Lizard-Spock from The Big Bang Theory. I decided to build both.

This article walks through building both versions in Python from scratch. By the end, you will have a working terminal game that handles player input, generates random computer moves, determines winners, and keeps running until the player quits. The same logic applies to any simple game you want to build in Python.

TLDR

  • Use a dictionary to map moves to numbers, then a win-lose matrix to determine outcomes
  • Random computer moves come from random.randint() – no bias, no patterns
  • The game loop runs inside a while True block and breaks only on quit
  • Two separate win-lose matrices handle the classic and extended versions
  • The complete code for both versions fits in under 150 lines

What is Rock Paper Scissors in Python?

Rock Paper Scissors is a hand-game usually played between two people. Both players choose one of three moves – rock, paper, or scissors – and reveal them simultaneously. Rock crushes scissors, scissors cuts paper, and paper covers rock. When both players choose the same move, it is a tie.

The Python implementation translates each move into a number and uses a win-lose matrix to look up the outcome. This approach separates the game logic from the user interface, which makes it easy to add features or change rules without rewriting the core code.

There is also an extended version called Rock-Paper-Scissors-Lizard-Spock, popularized by the TV show The Big Bang Theory. It adds two moves – lizard and Spock – which creates more tie-breaking scenarios and makes the game more interesting to automate.

Game Logic with Win-Lose Matrices

The cleanest way to handle Rock Paper Scissors logic in Python is with a win-lose matrix. Think of it as a lookup table where you check the cell at [player_move, computer_move]. If the value matches the player_move index, the player wins. If it matches the computer_move index, the computer wins. If the value is -1, it is a tie.

The mapping between moves and numbers uses a Python dictionary. This gives you a human-readable way to convert inputs like “rock” into the numeric index 0, which you then use to look up results in the matrix.


import random

game_map = {0: "rock", 1: "paper", 2: "scissors", 3: "lizard", 4: "Spock"}

rps_table = [[-1, 1, 0],
             [1, -1, 2],
             [0, 2, -1]]

rpsls_table = [[-1, 1, 0, 0, 4],
               [1, -1, 2, 3, 1],
               [0, 2, -1, 2, 4],
               [0, 3, 2, -1, 3],
               [4, 1, 4, 3, -1]]


game_map = {0: 'rock', 1: 'paper', 2: 'scissors', 3: 'lizard', 4: 'Spock'}
rps_table[0][1] = 1  # Player=rock(0), Computer=paper(1) -> computer wins
rps_table[2][0] = 0  # Player=scissors(2), Computer=rock(0) -> player wins

The 5×5 matrix for the extended version captures all the relationships from the show. Lizard poisons Spock, Spock smashes scissors, and so on. Each cell tells you who wins the matchup between any two moves.

Building the Game Loop

Every interactive game needs a loop that keeps running until the player decides to quit. In Python, a while loop with a break condition handles this cleanly. The loop displays a menu, reads the player choice, and calls the appropriate function for each game version.


def main_menu():
    while True:
        print()
        print("Let's Play!!!")
        print("Which version of Rock-Paper-Scissors?")
        print("Enter 1 to play Rock-Paper-Scissors")
        print("Enter 2 to play Rock-Paper-Scissors-Lizard-Spock")
        print("Enter 3 to quit")
        print()

        try:
            choice = int(input("Enter your choice = "))
        except ValueError:
            print("Invalid input. Please enter a number.")
            continue

        if choice == 1:
            play_rps()
        elif choice == 2:
            play_rpsls()
        elif choice == 3:
            print("Thanks for playing!")
            break
        else:
            print("Wrong choice. Enter 1, 2, or 3.")


Let's Play!!!
Which version of Rock-Paper-Scissors?
Enter 1 to play Rock-Paper-Scissors
Enter 2 to play Rock-Paper-Scissors-Lizard-Spock
Enter 3 to quit

Enter your choice = 1

The try/except block catches the ValueError that happens when someone types “hello” instead of a number. Without this, the program crashes on bad input. The continue statement sends the loop back to the top, prompting again instead of crashing. The if-elif-else pattern used for move matching follows the same logic.

Handling Player Input

The player-facing function reads a string, converts it to the numeric mapping, and passes those numbers to the win-lose matrix. I convert the input to lowercase so “Rock”, “ROCK”, and “rock” all work the same way.


def get_player_move():
    print("Enter your move (or 'help' for instructions, 'exit' to quit):")
    inp = input("Enter your move: ").lower().strip()

    if inp == "help":
        return "help"
    elif inp == "exit":
        return "exit"
    elif inp == "rock":
        return 0
    elif inp == "paper":
        return 1
    elif inp == "scissors":
        return 2
    elif inp == "lizard":
        return 3
    elif inp == "spock":
        return 4
    else:
        return None


Enter your move (or 'help' for instructions, 'exit' to quit):
Enter your move: paper
Returned: 1

This function returns None for invalid input, which the calling code checks to display an error message and prompt again. The “help” and “exit” special cases let players get instructions or quit from within any game without breaking the outer loop.

Generating Computer Moves and Determining Winners

The computer move comes from Python’s random.randint() function. For the classic version, the range is 0 to 2. For the extended version, it is 0 to 4. This gives each move an equal probability, which is exactly what you want for fair gameplay. The functions in Python pattern here keeps this logic reusable across both game versions.


def get_computer_move(version="rps"):
    if version == "rps":
        return random.randint(0, 2)
    else:
        return random.randint(0, 4)


def determine_winner(player_move, computer_move, table):
    result = table[player_move][computer_move]
    if result == player_move:
        return "player"
    elif result == computer_move:
        return "computer"
    else:
        return "tie"


get_computer_move("rps") -> 0, 1, or 2 (random)
get_computer_move("rpsls") -> 0, 1, 2, 3, or 4 (random)

determine_winner(0, 1, rps_table) -> "computer"  # rock vs paper
determine_winner(2, 1, rps_table) -> "player"  # scissors vs paper
determine_winner(1, 1, rps_table) -> "tie"      # paper vs paper

The table is passed in as a parameter so the same function works for both the 3×3 classic matrix and the 5×5 extended matrix. This is a small example of the kind of abstraction that makes code easier to extend and test. The same principle applies when you want to handle ValueError and other exceptions gracefully in Python programs.

Complete Game Implementation

Putting it all together, here is the full working implementation for both versions of the game. The play_rps() and play_rpsls() functions each run their own inner loop, separate from the main menu loop.


import random
import time
import os

def clear_screen():
    os.system('cls' if os.name == 'nt' else 'clear')

def show_instructions(game_type="rps"):
    if game_type == "rps":
        print("Rock crushes Scissors")
        print("Scissors cuts Paper")
        print("Paper covers Rock")
    else:
        print("Scissors cuts Paper")
        print("Paper covers Rock")
        print("Rock crushes Lizard")
        print("Lizard poisons Spock")
        print("Spock smashes Scissors")
        print("Scissors decapitates Lizard")
        print("Lizard eats Paper")
        print("Paper disproves Spock")
        print("Spock vaporizes Rock")
        print("Rock crushes Scissors")

game_map = {0: "rock", 1: "paper", 2: "scissors", 3: "lizard", 4: "Spock"}
rps_table = [[-1, 1, 0], [1, -1, 2], [0, 2, -1]]
rpsls_table = [[-1, 1, 0, 0, 4], [1, -1, 2, 3, 1],
               [0, 2, -1, 2, 4], [0, 3, 2, -1, 3],
               [4, 1, 4, 3, -1]]

def play_rps():
    while True:
        print("--- Rock-Paper-Scissors ---")
        inp = input("Enter move (rock/paper/scissors), 'help', or 'exit': ").lower().strip()
        if inp == "exit":
            break
        if inp == "help":
            show_instructions("rps")
            continue
        if inp == "rock":
            player_move = 0
        elif inp == "paper":
            player_move = 1
        elif inp == "scissors":
            player_move = 2
        else:
            print("Invalid move.")
            continue

        comp_move = random.randint(0, 2)
        print(f"Computer chooses: {game_map[comp_move].upper()}")
        result = rps_table[player_move][comp_move]
        if result == player_move:
            print("YOU WIN!")
        elif result == comp_move:
            print("COMPUTER WINS!")
        else:
            print("TIE GAME!")
        time.sleep(1)
        clear_screen()

def play_rpsls():
    while True:
        print("--- Rock-Paper-Scissors-Lizard-Spock ---")
        inp = input("Enter move, 'help', or 'exit': ").lower().strip()
        if inp == "exit":
            break
        if inp == "help":
            show_instructions("rpsls")
            continue
        move_map = {"rock": 0, "paper": 1, "scissors": 2, "lizard": 3, "spock": 4}
        if inp not in move_map:
            print("Invalid move.")
            continue
        player_move = move_map[inp]
        comp_move = random.randint(0, 4)
        print(f"Computer chooses: {game_map[comp_move].upper()}")
        result = rpsls_table[player_move][comp_move]
        if result == player_move:
            print("YOU WIN!")
        elif result == comp_move:
            print("COMPUTER WINS!")
        else:
            print("TIE GAME!")
        time.sleep(1)
        clear_screen()

def main_menu():
    while True:
        print("Which version?")
        print("1: Rock-Paper-Scissors")
        print("2: Rock-Paper-Scissors-Lizard-Spock")
        print("3: Quit")
        try:
            choice = int(input("Enter choice: "))
        except ValueError:
            print("Enter a number.")
            continue
        if choice == 1:
            play_rps()
        elif choice == 2:
            play_rpsls()
        elif choice == 3:
            print("Thanks for playing!")
            break
        else:
            print("Enter 1, 2, or 3.")

if __name__ == "__main__":
    main_menu()


Which version?
1: Rock-Paper-Scissors
2: Rock-Paper-Scissors-Lizard-Spock
3: Quit
Enter choice: 1
--- Rock-Paper-Scissors ---
Enter move (rock/paper/scissors), 'help', or 'exit': rock
Computer chooses: SCISSORS
YOU WIN!

The time.sleep(1) call pauses briefly after each round so the player can see the result before the screen clears for the next round. This creates a clean rhythm of play without requiring the player to scroll through a long history of moves.

FAQ

Q: Why use a win-lose matrix instead of if-elif conditions?

The matrix approach scales better as you add moves. With three moves, you can write six if-elif branches. With five moves, you need twenty-five comparisons. The matrix handles any number of moves with the same two-line lookup.

Q: Could the computer be made to learn from player patterns?

Yes. A common approach is to track the frequency of each player move over time and have the computer bias its selection toward whatever beats the most common player choice. This requires a history list and a weighted random selection instead of random.randint().

Q: How do you add a score tracker?

Declare a dictionary scores = {"player": 0, "computer": 0, "tie": 0} before the game loop. After each round, increment the appropriate key based on the result. Print the dictionary at the start of each round or at the end when the player quits.

Q: What does the -1 mean in the win-lose matrix?

The -1 is a sentinel value that means “tie.” It does not match any valid move index (0, 1, 2 for classic, 0-4 for extended), so the code treats it as a draw. Any negative number works here, but -1 is the conventional choice for a sentinel in Python.

Q: Can this run in a web browser instead of the terminal?

Yes, with a web framework like Flask or FastAPI. You would replace input() calls with form submissions and replace print statements with HTML responses. The game logic functions (determine_winner, show_instructions) do not need to change – only the input and output layer changes. Building a Flask CRUD application gives you the fundamentals of tying Python logic to web endpoints.

I kept the game logic in this article deliberately simple. The win-lose matrix approach is easy to understand, easy to extend, and easy to test. Once you have the basic structure working, adding features like score tracking, player history, or a GUI becomes a matter of adding code rather than rewriting what you already have.

Share.
Leave A Reply