# “Rock, Paper, Scissors” game - follow-up

This is a follow-up to ""Rock, Paper, Scissors" game". I did almost everything the answers recommended and I hope you like it.

import os
from random import choice

def player_choice():
while True:
print("Rock, paper or scissors?")
choice = input(">").capitalize()
if choice in ('Rock', 'Paper', 'Scissors'):
return choice

def win_msg(player_choice, computer_choice):
msg = f"{player_choice} beats {computer_choice}. You won!"
return msg

def lose_msg(player_choice, computer_choice):
msg = f"{computer_choice} beats {player_choice}. You lost!"
return msg

def computer_choice():
return choice(('Rock', 'Paper', 'Scissors'))

def show_statistics(scores):
os.system('cls' if os.name == 'nt' else 'clear')
print(f"Wins: {scores[0]}\nLosses: {scores[1]}\nDraws: {scores[2]}")

def game_outcome(player, computer):
if player == computer:
return 'Draw'
elif player == 'Rock':
if computer == 'Paper':
return 'Lose'
else:
return 'Win'
elif player == 'Paper':
if computer == 'Rock':
return 'Win'
else:
return 'Lose'
else:
if computer == 'Rock':
return 'Lose'
else:
return 'Win'

def show_results(outcome, player, computer):
if outcome == 'Win':
print(win_msg(player, computer))
elif outcome == 'Lose':
print(lose_msg(player, computer))
else:
print("Draw. Nobody wins or losses.")

def update_scores(scores, outcome):
new_scores = list(scores)
if outcome == 'Win':
new_scores[0] += 1
elif outcome == 'Lose':
new_scores[1] += 1
else:
new_scores[2] += 1
new_scores = tuple(new_scores)
return new_scores

def rock_paper_scissors(scores):
player = player_choice()
computer = computer_choice()
outcome = game_outcome(player, computer)
show_results(outcome, player, computer)
new_scores = update_scores(scores, outcome)
return new_scores

def play_again():
while True:
print("\nDo you want to play again?")
print("(Y)es")
print("(N)o")
ans = input("> ").lower()
if ans == 'y':
return True
elif ans == 'n':
return False

def starting_scores():
return 0, 0, 0

def main():
scores = starting_scores()
still_playing = True
while still_playing:
show_statistics(scores)
scores = rock_paper_scissors(scores)
still_playing = play_again()

if __name__ == '__main__':
main()


Firstly, this is some nice clean code. Well done.

The changes I'd make:

1. win_msg and lose_msg can instead be global constant strings.
2. You should pick a preferred string delimiter and stick to using just that one. I prefer '.
3. I would make a function clear_console as the code is rather strange. And will ease use if you need it again.
4. game_outcome can be simplified by using a dictionary to store what beats what.

This also means you can expand to RPSLS with ease.

5. show_results can be changed to use a dictionary too.
6. update_scores can be changed in one of two ways:

• Add a dictionary that converts from outcome to the index.
• Change scores to a dictionary.

And so you can get the following code:

import os
from random import choice

MESSAGES = {
'Win': '{player} beats {computer}. You won!',
'Lose': '{computer} beats {player}. You lost!',
'Draw': 'Draw. Nobody wins or losses.'
}

BEATS = {
'Rock': 'Scissors',
'Paper': 'Rock',
'Scissors': 'Paper'
}

def player_choice():
while True:
print('Rock, paper or scissors?')
choice = input('>').capitalize()
if choice in ('Rock', 'Paper', 'Scissors'):
return choice

def play_again():
while True:
print('\nDo you want to play again?')
print('(Y)es')
print('(N)o')
ans = input('> ').lower()
if ans == 'y':
return True
elif ans == 'n':
return False

def computer_choice():
return choice(('Rock', 'Paper', 'Scissors'))

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

def game_outcome(player, computer):
if player == computer:
return 'Draw'

if BEATS[player] == computer:
return 'Win'
else:
return 'Lose'

def rock_paper_scissors(scores):
clear_console()
print(f'Wins: {scores["Win"]}\nLosses: {scores["Lose"]}\nDraws: {scores["Draw"]}')
player = player_choice()
computer = computer_choice()
outcome = game_outcome(player, computer)
print(MESSAGES[outcome].format(player=player, computer=computer))
scores[outcome] += 1
return new_scores

def main():
scores = {
'Win': 0,
'Lose': 0,
'Draw': 0
}
still_playing = True
while still_playing:
scores = rock_paper_scissors(scores)
still_playing = play_again()

if __name__ == '__main__':
main()


From here you can look into using enum to remove the strings from your code. This is good because it's easy to make a typo in a string, say I use 'rock' rather than 'Rock'. It also means that if you do make a typo then you'll be able to see where it was made when you reach that code. However with a string it'll move the error to a later part of the code and make debugging horrible.

Here is an example of how you can use them:

from enum import Enum

class Score(Enum):
WIN = 'Win'
LOSE = 'Lose'
DRAW = 'Draw'

MESSAGES = {
Score.WIN: '{player} beats {computer}. You won!',
Score.LOSE: '{computer} beats {player}. You lost!',
Score.DRAW: 'Draw. Nobody wins or losses.'
}


I agree with Peilonrayz that you can use a dict, but I would not use it this way. Not because it won't work but because it only works in this specific case.

WIN = {('paper', 'rock'), ('rock', 'scissors'), ('scissors', 'paper')}

def game_outcome(player, computer):
if player==computer:
print('Draw. Nobody wins or losses.')
return 0
elif (player, computer) in WIN:
print(f'{player} beats {computer}. You won!')
return 1
elif (computer, player) in WIN:
print(f'{computer} beats {player}. You lost!')
return -1
else:
raise NotImplementedError(f"no defined rule for '{player}' and '{computer}'")

>>> game_outcome('paper', 'paper')
Draw. Nobody wins or looses.
0
>>> game_outcome('rock', 'paper')
paper beats rock. You lost!
-1
>>> game_outcome('paper', 'rock')
paper beats rock. You won!
1


Why is it more generic, because if you want to play rock-paper-scissor-lizard-spock, you just change the dict to this:

RULES = {
('paper', 'rock'): 'covers',
('rock', 'scissors'): 'crushes',
('rock', 'lizard'): 'crushes',
('spock', 'rock'): 'vaporises',
('scissors', 'paper'): 'cuts',
('lizard', 'paper'): 'eats',
('paper', 'spock'): 'disproves',
('scissors', 'lizard'): 'decapitates',
('spock', 'scissors'): 'smashes',
('lizard', 'spock'): 'poisons'
}
def game_outcome(player, computer):
if player==computer:
print('Draw. Nobody wins or losses.')
return 0
elif (player, computer) in RULES:
print(f'{player} {RULES[player, computer]} {computer}. You won!')
return 1
elif (computer, player) in RULES:
print(f'{computer} {RULES[computer, player]} {player}. You lost!')
return -1
else:
raise NotImplementedError(f"no defined rule for '{player}' and '{computer}'")

>>> game_outcome('paper', 'spock')
paper disproves spock. You won!
1
>>> game_outcome('lizard', 'spock')
lizard poisons spock. You won!
1
>>> game_outcome('lizard', 'scissor')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 12, in game_outcome
NotImplementedError: no defined rule for 'lizard' and 'scissor'
>>> game_outcome('lizard', 'scissors')
scissors decapitates lizard. You lost!
-1


All this code just to say that {(inputs, ...): output} is generally a good pattern when the mapping between the inputs and the output isn't obvious.

• I find it funny how you say "don't use a dict" and use a dict in your second code. – Peilonrayz Mar 14 at 17:18
• "I agree with Peilonrayz that you can use a dict, but I would not use it this way" You probably misread. (and I didn't edit anything) – Benoît Pilatte Mar 15 at 10:21