3
\$\begingroup\$

I am practicing w/ Python and wanted to code a simple Rock, Paper, Scissors game w/o any user input. I used a list to hold the values and randomly pulled them with a function. I used the the return value from the first function to print the response in the second function, along with the printed results. What are some alternative methods to achieve this same functionality and make this more efficient. Thank you.

# Internal Rock, Paper Scissors Game  >>>>>>>>>>>>>>>>>>>>>>>>>

import random

# List string values for the game

r_p_s = [
    'Rock', 'Paper', 'Scissors'
]


# Function to return random values from List
def game():
    return random.choice(r_p_s) + " vs. " + random.choice(r_p_s)


result = game()  # Returned Value


# Function to print out the results of the returned value with a response

def response():
    print("Roll: " + "\n" + result)

    if "Scissors vs. Paper" == result or \
       "Paper vs. Scissors" == result:
        print("\nScissors beats Paper")

    elif "Scissors vs. Rock" == result or \
         "Rock vs. Scissors" == result:
        print("\nRock beats Scissors!")

    elif "Rock vs. Paper" == result or \
         "Paper vs. Rock" == result:
        print("\nPaper beats Rock!")

    else:
        print("\nDraw! Roll again.")


response()
\$\endgroup\$
4
\$\begingroup\$

Despite of very simple and "verbose" initial implementation it has a bunch of issues:

  • r_p_s = ['Rock', 'Paper', 'Scissors'] is better defined as constant of immutable values

  • result is accessed within response() function body as a global variable.
    Instead, it's better called within the function on demand:

    def response():
        result = game()
        ...
    
  • instead of arbitrary strings concatenation ... + " vs. " + ..., "Roll: " + "\n" + result
    - use flexible f-string formatting.
    For ex.: print(f"Roll: \n{result}")

  • the conditions like:

    if "Scissors vs. Paper" == result or \
        "Paper vs. Scissors" == result:
        print("\nScissors beats Paper")
    

    are too verbose and hard-coded to be manageable and reliable


Now, when you realize the bad things described above and wonder if there a manageable and advanced approach - take and consider the one below, which is OOP approach representing RPSGame class (as a main class) and powered by enum classes to represent the crucial shapes.
The new implementation itself is pretty descriptive (I hope). Enjoy!

import random
from enum import Enum


class RPSGame:
    class Shape(Enum):
        R = 'Rock'
        P = 'Paper'
        S = 'Scissors'

    # winning outcome bindings
    WIN_OUTCOMES = {
        (Shape.P, Shape.R), (Shape.R, Shape.S), (Shape.S, Shape.P),
    }

    @staticmethod
    def choice():
        shapes = list(RPSGame.Shape)
        return random.choice(shapes), random.choice(shapes)

    def play(self):
        shape1, shape2 = self.choice()
        print(f"Roll: \n{shape1.value} vs {shape2.value}\n")

        if shape1 == shape2:
            print("Draw! Roll again.")
        else:
            # check to swap shapes to get winning output order
            if (shape1, shape2) not in RPSGame.WIN_OUTCOMES:
                shape1, shape2 = shape2, shape1
            print(f"{shape1.value} beats {shape2.value}!")


if __name__ == "__main__":
    rps = RPSGame()
    rps.play()
    rps.play()
    rps.play()

Sample output (from 3 plays):

Roll: 
Paper vs Scissors

Scissors beats Paper!
Roll: 
Scissors vs Scissors

Draw! Roll again.
Roll: 
Rock vs Paper

Paper beats Rock!
\$\endgroup\$
  • \$\begingroup\$ I really appreciate the time you took to answer. Im getting my feet wet with classes currently. Quick question, what does @ staticmethod do? \$\endgroup\$ – Wicked1Kanobi Nov 13 at 22:27
  • \$\begingroup\$ @Wicked1Kanobi docs.python.org/3/library/functions.html#staticmethod \$\endgroup\$ – RomanPerekhrest Nov 13 at 22:38
  • 1
    \$\begingroup\$ I'd definitely advocate for including the full name in the Enum, eg Shape.Rock if that's the direction you want to go. \$\endgroup\$ – TemporalWolf Nov 14 at 19:57
  • \$\begingroup\$ You have a single method and no state to persist. Why would you want to write a class? \$\endgroup\$ – 409_Conflict Nov 15 at 11:42
  • \$\begingroup\$ @409_Conflict, Class is not mandatory as "something with persistent state". As well as specific Factory class could have NO state, but just generate a specific instances (with factory method). Here, I used classes to show how it could be flexibly organized and structured. And since it's a class now, it can be extended and filled with any needed state. \$\endgroup\$ – RomanPerekhrest Nov 15 at 12:13
3
\$\begingroup\$

Let's start with a list like you did:

rps = ['Rock', 'Paper', 'Scissors']

What do we notice with the ordering? Each one always beats the previous one: Paper beats Rock, Scissors beats Paper & Rock beats Scissors (when we wrap around).

We can exploit that logic:

difference = rps.index(first_shape) - rps.index(second_shape)

This difference means we now have a very simple way of determining the winner. If it's zero, it's a tie, otherwise the difference tells us who won, and we'll be using the ability to use negative list indexes:

For a length 3 list: lst[-1] == lst[2] and lst[-2] == lst[1]. So a difference of 1 means the first player won, and a difference of 2 means the second player won, and python is gonna handle the negative cases for us automatically. Nice!

So we can just use this directly to figure out who won with the help of a winner_lookup list that makes this

winner_lookup = ["\nDraw! Roll again!", 
                 f"\n{first_shape} beats {second_shape}!", 
                 f"\n{second_shape} beats {first_shape}!"]
print(winner_lookup[difference])

Now we can change the input/output strings without fear of damaging our business logic.

To diverge entirely from using strings for business, let's make our choices as just the index of the choice we want:

first_shape, second_shape = random.randint(0, 2), random.randint(0, 2)

To take player input, we'd just use rps.index(player_input) to get the index of their choice. This also means all the prints need to use rps[shape] to get the string instead of the index.

We can pull all of the strings outside of the play function, which all together gives us:

import random

rps = ['Rock', 'Paper', 'Scissors']
match_string = "Roll:\n{} vs {}"
tie_string = "\nDraw! Roll again!"
winner_string = "\n{} beats {}!"

def autoplay():
    first, second = random.randint(0, 2), random.randint(0, 2)
    difference = first - second
    winner_lookup = [tie_string, 
                     winner_string.format(rps[first], rps[second]),
                     winner_string.format(rps[second], rps[first])]
    print(match_string.format(rps[first], rps[second]))
    print(winner_lookup[difference])

Where you may want to use classes and objects instead of this is if you wanted to implement RPSLS (Rock Paper Scissors Lizard Spock), as this implementation relies on the game rules being only each entry is beaten by the next entry. But if we wanted to change any of the strings, we don't affect the code at all.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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