Refactoring the Rock-Paper-Scissors game in Python

Posted on Sat 09 May 2020 in Python

Do one thing and do it well.

Unix Philosophy

In this post, we will refactor a shoddy procedural Rock-Paper-Scissors code written in Python into a more readable and maintainable code.

Python provides several tools to achieve this. In this post, you will see how we can use Python functions to write better code.

Shoddy Rock-Paper-Scissors code

A shoddy Rock-Paper-Scissors game is given below:

import random

options = ['rock', 'paper', 'scissors']
print('(1) Rock\n(2) Paper\n(3) Scissors')
human_choice = options[int(input('Enter the number of your choice: ')) - 1]
print(f'You chose {human_choice}')
computer_choice = random.choice(options)
print(f'The computer chose {computer_choice}')
if human_choice == 'rock':
    if computer_choice == 'paper':
        print('Sorry, paper beat rock')
    elif computer_choice == 'scissors':
        print('Yes, rock beat scissors!')
    else:
        print('Draw!')
elif human_choice == 'paper':
    if computer_choice == 'scissors':
        print('Sorry, scissors beat paper')
    elif computer_choice == 'rock':
        print('Yes, paper beat rock!')
    else:
        print('Draw!')
elif human_choice == 'scissors':
    if computer_choice == 'rock':
        print('Sorry, rock beat scissors')
    elif computer_choice == 'paper':
        print('Yes, scissors beat paper!')
    else:
        print('Draw!')
(1) Rock
(2) Paper
(3) Scissors
Enter the number of your choice: 1
You chose rock
The computer chose paper
Sorry, paper beat rock

The code above works. But do you see any problems with this code?

You will agree with me that the above code is:

  1. not readable
  2. not maintainable
  3. difficult to debug

Unless I told you that the above is a Rock-Paper-Scissors game, you would have to go through the code line by line to figure out the main concern of this code.

Can we do better? Certainly, we can.

Separation of Concerns

A concern is a distinct behavior the program deals with. The code becomes clear, if we can separate out these concerns into small, manageable pieces.

One of the ways we could achieve Separation of Concerns is by decomposing the code into functions. Each function should Do one thing and do it well.

Rock-Paper-Scissors concerns

Let's iron out the different concerns of the above Rock-Paper-Scissors code. Here are my list of concerns:

  1. Display the game options
  2. Get the human choice
  3. Get the computer choice
  4. Display the choices
  5. Determine the game result
  6. Display the game result

Your list might be different. But I hope it would be very close. Anyways, let's code out these concerns into functions.

1. Display the game options

import random


OPTIONS = ["rock", "paper", "scissors"]


def display_options():
    """ Iterates through the choices are prints the options. """

    for idx, choice in enumerate(OPTIONS, start=1):
        print(f"({idx}) {choice.capitalize()}")

2. Get the human choice

def get_human_choice():
    """ 
        Get the human input and return the 
        corresponding option.
    """

    choice = input("Enter the number of your choice: ")
    choice = OPTIONS[int(choice) - 1]
    return choice

3.Get the computer choice

def get_computer_choice():
    """ Randomly select an option and return it. """

    choice = random.choice(OPTIONS)
    return choice

4. Display the choices

def display_choices(human_choice, computer_choice):
    """ Display the choices of players """

    print(f"You chose {human_choice}")
    print(f"The computer chose {computer_choice}")

5. Determine the game result

OUTCOMES = {
    ("rock", "paper"): False,
    ("rock", "scissors"): True,
    ("rock", "rock"): None,
    ("paper", "paper"): None,
    ("paper", "scissors"): False,
    ("paper", "rock"): True,
    ("scissors", "paper"): True,
    ("scissors", "scissors"): None,
    ("scissors", "rock"): False,
}


def get_win_lose(human_choice, computer_choice):
    """ Determine the result of the game based on the choices """

    return OUTCOMES.get((human_choice, computer_choice))

I created a dictionary which maps the (human_choice, computer_choice) tuple to a result. rock loses to paper, rock beats scissors, rock draws with rock and so on. The function looks up the dictionary and returns the result based on the human and computer choices.

6. Display the game result

def display_result(human_choice, computer_choice):
    """ Display the game result """

    win_lose = get_win_lose(human_choice, computer_choice)

    if win_lose is None:
        print("Draw!")
        return

    if win_lose:
        print(f"Yes, {human_choice} beat {computer_choice}!")
    else:
        print(f"Sorry, {computer_choice} beat {human_choice}")

This function invokes the get_win_lose function to determine the result. Notice that I did not mix the concerns: (determine the game result and display the result) into one function.

Start the game

Invoke the functions in order. I will wrap the invocations in a function and invoke the wrapper like so:

def game():
    display_options()
    human_choice = get_human_choice()
    computer_choice = get_computer_choice()
    display_choices(human_choice, computer_choice)
    display_result(human_choice, computer_choice)


game()
(1) Rock
(2) Paper
(3) Scissors
Enter the number of your choice: 2
You chose paper
The computer chose paper
Draw!

Putting it all together

Below is the complete code:

import random


OPTIONS = ["rock", "paper", "scissors"]
OUTCOMES = {
    ("rock", "paper"): False,
    ("rock", "scissors"): True,
    ("rock", "rock"): None,
    ("paper", "paper"): None,
    ("paper", "scissors"): False,
    ("paper", "rock"): True,
    ("scissors", "paper"): True,
    ("scissors", "scissors"): None,
    ("scissors", "rock"): False,
}


def display_options():
    for idx, choice in enumerate(OPTIONS, start=1):
        print(f"({idx}) {choice.capitalize()}")


def get_human_choice():
    choice = input("Enter the number of your choice: ")
    choice = OPTIONS[int(choice) - 1]
    return choice


def get_computer_choice():
    choice = random.choice(OPTIONS)
    return choice


def display_choices(human_choice, computer_choice):
    print(f"You chose {human_choice}")
    print(f"The computer chose {computer_choice}")


def get_win_lose(human_choice, computer_choice):
    return OUTCOMES.get((human_choice, computer_choice))


def display_result(human_choice, computer_choice):
    win_lose = get_win_lose(human_choice, computer_choice)

    if win_lose is None:
        print("Draw!")
        return

    if win_lose:
        print(f"Yes, {human_choice} beat {computer_choice}!")
    else:
        print(f"Sorry, {computer_choice} beat {human_choice}")


def game():
    display_options()
    human_choice = get_human_choice()
    computer_choice = get_computer_choice()
    display_choices(human_choice, computer_choice)
    display_result(human_choice, computer_choice)


game()
(1) Rock
(2) Paper
(3) Scissors
Enter the number of your choice: 3
You chose scissors
The computer chose paper
Yes, scissors beat paper!

Done and Accomplished

Done

  1. Identified the distict behaviors/concerns of the program
  2. Decomposed the concern into functions. Each function has a specific intent.

Accomplished

  1. Just by looking at the code, we understand what it is doing.
  2. Code is clear, readable and maintainable
  3. Easy to debug

Can we do better?

This is a good start. If you look carefully, we are passing the human and computer choices into multiple functions. Also, multiple functions depend on the same OPTIONS variable. The functions, though distinct, accomplish a bigger concern: the Rock-Paper-Scissors game.

These are good indicators that we could possibly use another tool in Python: class? Can you try to refactor the code into a RockPaperScissors class?

Conclusion

In this post, I tried to illustrate one of the Foundations of Design - Separation of Concerns. Python function is one of those tools which can be used to decompose programs into small and clear pieces of code. Taking the time to design your code in this way can go a long way when developing complex softwares.

These days I am learning from this awesome book Practices of the Python Pro by Dane Hillard. This book introduces many concepts a software developer should use to write better software. Dane uses Python as the programming language in this book. However, the concepts could be applied to any programming language. Check it out!

Many thanks to Dane for allowing me to take the shoddy example program from this book for the purpose of writing this blog post.

I hope you have learned something new.

That's it readers, until next time! Happy Python coding!