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:
- not readable
- not maintainable
- 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:
- Display the game options
- Get the human choice
- Get the computer choice
- Display the choices
- Determine the game result
- 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
- Identified the distict behaviors/concerns of the program
- Decomposed the concern into functions. Each function has a specific intent.
Accomplished
- Just by looking at the code, we understand what it is doing.
- Code is clear, readable and maintainable
- 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!