Yehui He
Yehui He

Reputation: 47

Rock Paper Scissors abstraction in Python

I did a senior Python backend engineer interview that involved implementing a Rock Paper Scissors game. The result was not pretty. Their engineer said I have clumsy implementation all over the place. I did an upgrade afterwards, but still have one issue that confuses me. They would like me to change my original implementation of the game rules to a better abstraction.

My original implementation is:

def _outcome(self, player_move, ai_move):
    """Determine the outcome of current round.

    Paper beats (wraps) rock.
    Rock beats (blunts) scissors.
    Scissors beats (cuts) paper.

    Parameters
    ----------
    player_move : MoveChoice
        Player's move for current round.

    ai_move : MoveChoice
        AI's move for current round.

    Returns
    -------
    outcome : Outcome
        Outcome of the current round.
    """
    if player_move == 1 and ai_move == 2 or \
        player_move == 2 and ai_move == 3 or \
        player_move == 3 and ai_move == 1:
        return self.computer.name
    elif player_move == 1 and ai_move == 3 or \
        player_move == 2 and ai_move == 1 or \
        player_move == 3 and ai_move == 2:
        return self.player.name
    else:
        return 'Draw'

with a mapping:

MOVE_CHOICE = {
    1: 'rock',
    2: 'paper',
    3: 'scissors',
}

I have a role.Player and role.Computer class defined elsewhere. Their critiques are:

Their suggestions are:

So far I've created Enums for Moves and Outcome:

class MoveChoice(Enum):
    ROCK = auto()
    PAPER = auto()
    SCISSORS = auto()


class Outcome(Enum):
    WIN = auto()
    LOSE = auto()
    DRAW = auto()

But I'm having trouble abstracting the game rules like they asked. How would I do that?

Upvotes: 0

Views: 512

Answers (1)

CrazyChucky
CrazyChucky

Reputation: 3518

Based on the code you've shown and the recommendations they gave you, I would do something like this:

from enum import Enum, auto

class MoveChoice(Enum):
    ROCK = auto()
    PAPER = auto()
    SCISSORS = auto()

class Outcome(Enum):
    WIN = auto()
    LOSE = auto()
    DRAW = auto()

WIN_MAPPING = {
    MoveChoice.ROCK: MoveChoice.SCISSORS,
    MoveChoice.SCISSORS: MoveChoice.PAPER,
    MoveChoice.PAPER: MoveChoice.ROCK,
}

def outcome(player_move, ai_move):
    if player_move == ai_move:
        return Outcome.DRAW
    elif WIN_MAPPING[player_move] == ai_move:
        return Outcome.WIN
    else:
        return Outcome.LOSE

Note that I've changed your method to a function. This isn't a recommendation on overall program structure, it's just to present the logic as a more self-contained example.


As Luke Nelson pointed out in the comments, the interviewers suggested that the win mapping map each move to a list of moves it beats. I missed this detail. While it doesn't make any practical difference in Rock Paper Scissors, it is more generalized, and would make it easier if they then ask you to expand the rule set to something like Rock Paper Scissors Lizard Spock.

The only thing that would change about the mapping would be turning each value into a single-element list:

WIN_MAPPING = {
    MoveChoice.ROCK: [MoveChoice.SCISSORS],
    MoveChoice.SCISSORS: [MoveChoice.PAPER],
    MoveChoice.PAPER: [MoveChoice.ROCK],
}

Then the outcome function, instead of checking if the computer's move equals the value, would have to check if it's in the value (since the value is now a list).

def outcome(player_move, ai_move):
    if player_move == ai_move:
        return Outcome.DRAW
    # elif WIN_MAPPING[player_move] == ai_move:
    # Above line is replaced with:
    elif ai_move in WIN_MAPPING[player_move]:
        return Outcome.WIN
    else:
        return Outcome.LOSE

Upvotes: 2

Related Questions