Python Rock-Paper-Scissors via a class to handle the game Original inspiration was by this Python beginner, and it prompted me to rewrite a bunch of stuff with my flair and my Python experience: First attempt: Python Rock Paper Scissors


Okay, so I looked at the aforementioned post, and was bored and needed to kill an hour at work.  So I killed an hour - I took their RPS game and turned it into a class, and made it look less evil/ugly.

While this is by no means a fully-fledged program that I've cleanly created and really thoroughly tested, this is something that I can at least ask for input on.

Runs pretty cleanly, and uses a lot of strings that the OP of the original inspiration post had.  But, it has a lot of docstrings, too.  And the entire game resides in a class, and calls via the class and such.

Because this version uses f-strings, you must have Python 3.6 or newer to use this program/code.

rps.py:

import random


class RockPaperScissors:
    """
    Class to handle an instance of a Rock-Paper-Scissors game
    with unlimited rounds.
    """

    def __init__(self):
        """
        Initialize the variables for the class
        """
        self.wins = 0
        self.losses = 0
        self.ties = 0
        self.options = {'rock': 0, 'paper': 1, 'scissors': 2}

    def random_choice(self):
        """
        Chooses a choice randomly from the keys in self.options.
        :returns: String containing the choice of the computer.
        """

        return random.choice(list(self.options.keys()))

    def check_win(self, player, opponent):
        """
        Check if the player wins or loses.
        :param player: Numeric representation of player choice from self.options
        :param opponent: Numeric representation of computer choice from self.options
        :return: Nothing, but will print whether win or lose.
        """

        result = (player - opponent) % 3
        if result == 0:
            self.ties += 1
            print("The game is a tie! You are a most worthy opponent!")
        elif result == 1:
            self.wins += 1
            print("You win! My honor demands a rematch!")
        elif result == 2:
            self.losses += 1
            print("Haha, I am victorious! Dare you challenge me again?")

    def print_score(self):
        """
        Prints a string reflecting the current player score.
        :return: Nothing, just prints current score.
        """
        print(f"You have {self.wins} wins, {self.losses} losses, and "
              f"{self.ties} ties.")

    def run_game(self):
        """
        Plays a round of Rock-Paper-Scissors with the computer.
        :return: Nothing
        """
        while True:
            userchoice = input("Choices are 'rock', 'paper', or 'scissors'.\n"
                               "Which do you choose? ").lower()
            if userchoice not in self.options.keys():
                print("Invalid input, try again!")
            else:
                break
        opponent_choice = self.random_choice()
        print(f"You've picked {userchoice}, and I picked {opponent_choice}.")
        self.check_win(self.options[userchoice], self.options[opponent_choice])


if __name__ == "__main__":
    game = RockPaperScissors()
    while True:
        game.run_game()
        game.print_score()

        while True:

            continue_prompt = input('\nDo you wish to play again? (y/n): ').lower()
            if continue_prompt == 'n':
                print("You are weak!")
                exit()
            elif continue_prompt == 'y':
                break
            else:
                print("Invalid input!\n")
                continue


Any suggestions and input are welcome, as this is a rough attempt.  :)
I think it makes sense to use dictionaries to store values for win/tie/loss:

    def __init__(self):
        """
        Initialize the variables for the class
        """
        self.options = {'rock': 0, 'paper': 1, 'scissors': 2}
        self.outcome_count = {
            "tie": 0,
            "win": 0,
            "loss": 0,
        }


This makes check_win a bit more "mechanical" since you can now refer to the outcomes by name and by looking things up in static data, instead of needing a bunch of if/else:

    def check_win(self, player, opponent):
        """
        Check if the player wins or loses.
        :param player: Numeric representation of player choice from self.options
        :param opponent: Numeric representation of computer choice from self.options
        :return: Nothing, but will print whether win or lose.
        """

        result = ["tie", "win", "loss"][(player - opponent) % 3]

        self.outcome_count[result] += 1

        outcome_message = {
            "tie":  "The game is a tie! You are a most worthy opponent!",
            "win":  "You win! My honor demands a rematch!",
            "loss": "Haha, I am victorious! Dare you challenge me again?",
        }
        print(outcome_message[result])


though of course it ends up making print_score less self-explanatory:

    def print_score(self):
        """
        Prints a string reflecting the current player score.
        :return: Nothing, just prints current score.
        """
        wins   = self.outcome_count["win"]
        losses = self.outcome_count["loss"]
        ties   = self.outcome_count["tie"]
        print(f"You have {wins} wins, {losses} losses, and {ties} ties.")


Lastly, I think that the run_game loop can be made slightly clearer by writing instead

        while True:
            userchoice = input("Choices are 'rock', 'paper', or 'scissors'.\nWhich do you choose? ").lower()
            if userchoice in self.options.keys():
                break
            print("Invalid input, try again!")


I find an explicit "early-exit" with no else to be easier to follow [note that the condition is not inverted, which I think helps for clarity in this case] though this could be jarring if it's not conventional for the larger codebase.