3
\$\begingroup\$

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()
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

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.'
}
\$\endgroup\$
2
\$\begingroup\$

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.

\$\endgroup\$
2
  • \$\begingroup\$ I find it funny how you say "don't use a dict" and use a dict in your second code. \$\endgroup\$
    – Peilonrayz
    Commented Mar 14, 2019 at 17:18
  • 1
    \$\begingroup\$ "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) \$\endgroup\$
    – Benoît P
    Commented Mar 15, 2019 at 10:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.