From 30 to 11 Lines of Code: Rock Paper Scissors in Python | by David Amos | Apr, 2022

Revisiting the favored newbie recreation

A brown rock, crumpled white piece of paper, and scissors with blue handles on a red background.
Picture by Mariet Jieopp for Getty Pictures (used with Canva Professional license)

If you be taught to program for the primary time, you search for — or, maybe, are assigned — initiatives that reinforce fundamental ideas. However how typically do you, when you’ve attained extra information and expertise, revisit these newbie initiatives from the attitude of a sophisticated programmer?

On this article, I need to just do that. I need to revisit a typical newbie undertaking — implementing the sport “Rock Paper Scissors” in Python — with the information I’ve gained from practically eight years of Python programming expertise.

Earlier than diving into code, let’s set the stage by outlining how “Rock Paper Scissors” is performed. Two gamers every select considered one of three objects: rock, paper, or scissors. The gamers reveal their choice to one another concurrently and the winner is decided by the next guidelines:

  1. Rock beats scissors
  2. Scissors beats paper
  3. Paper beats rock

Rising up, my buddies and I used “Rock Paper Scissors” to unravel all kinds of issues. Who will get to play first in a one-player online game? Who will get the final can of soda? Who has to go decide up the mess we simply made? Necessary stuff.

Let’s lay out some necessities for the implementation. Somewhat than constructing a full-blown recreation, let’s deal with writing a operate referred to as play() that accepts two string arguments — the selection of "rock", "paper", or "scissors" chosen by every participant — and returns a string indicating the winner (e.g., "paper wins") or if the sport ends in a tie (e.g., "tie").

Listed below are some examples of how play() known as and what it returns:

If one or each of the 2 arguments are invalid, that means they aren’t considered one of "rock", "paper", or "scissors", then play() ought to elevate some form of exception.

play() must also be commutative. That’s, play("rock", "paper") ought to return the identical factor as play("paper", "rock").

To set a baseline for comparability, take into account how a newbie would possibly implement the play() operate. If this newbie is something like I used to be after I first discovered to program, they’d in all probability begin writing down an entire bunch of if statements:

Strictly talking, there’s nothing mistaken with this code. It runs with out error and meets all the necessities. It’s additionally much like plenty of high-ranking implementations for the Google search “rock paper scissors python.”

Skilled programmers will rapidly acknowledge plenty of code smells, although. Particularly, the code is repetitive and there are a lot of potential execution paths.

One solution to implement “Rock Paper Scissors” from a extra superior perspective entails leveraging Python’s dictionary kind. A dictionary can map objects to those who they beat in response to the foundations of the sport.

Let’s name this dictionary loses_to:

loses_to gives a easy API for figuring out which merchandise loses to a different:

A dictionary has a few advantages. You should use it to:

  1. Validate chosen objects by checking for membership or elevating a KeyError
  2. Decide a winner by checking if a price loses to the corresponding key

With this in thoughts, the play() operate could possibly be written as follows:

On this model, play() takes benefit of the built-in KeyError raised by the loses_to dictionary when attempting to entry an invalid key. This successfully validates the gamers’ selections. So if both participant chooses an invalid merchandise — one thing like "lizard" or 1234play() raises a KeyError:

Though the KeyError is not as useful as a ValueError with a descriptive message, it nonetheless will get the job carried out.

The brand new play() operate is way less complicated than the unique one. As a substitute of dealing with a bunch of express circumstances, there are solely three circumstances to test:

  1. player2_choice loses to player1_choice
  2. player1_choice loses to player2_choice
  3. player1_choice and player2_choice are the identical

There’s a fourth hidden case, nonetheless, that you just nearly must squint to see. That case happens when not one of the different three circumstances are true, by which case play() returns a None worth.

However… can this case ever actually happen? Really, no. It will possibly’t. In response to the foundations of the sport, if participant 1 doesn’t lose to participant 2 and participant 2 doesn’t lose to participant 1, then each gamers should have chosen the identical merchandise.

In different phrases, we are able to take away the ultimate if block from play() and simply return "tie" if neither of the opposite two if blocks execute:

We’ve made a tradeoff. We’ve sacrificed readability — I’d argue that there’s a higher cognitive load required to know how the above play() operate works in comparison with the “newbie” model — to be able to shorten the operate and keep away from an unreachable state. In consequence, we’ve decreased the 30-line newbie resolution to simply 11 strains of code.

The earlier resolution works nice. It’s readable and a lot shorter than the “newbie” resolution. However it’s not very versatile. That’s, it could’t deal with variations of “Rock Paper Scissors” with out rewriting a number of the logic.

For example, there’s a variation referred to as “Rock Paper Scissors Lizard Spock” with a extra advanced algorithm:

  1. Rock beats scissors and lizard
  2. Paper beats rock and Spock
  3. Scissors beats paper and lizard
  4. Lizard beats Spock and paper
  5. Spock beats scissors and rock

How are you going to adapt the code to deal with this variation?

First, exchange the string values within the loses_to dictionary with Python units. Every set incorporates all the objects that lose to the corresponding key. This is what this model of loses_to seems to be like utilizing the unique “Rock Paper Scissors” guidelines:

Why units? As a result of we solely care about what objects lose to a given key. We don’t care concerning the order of these objects.

To adapt play() to deal with the brand new loses_to dictionary, all you must do is exchange == with in to make use of a membership test as an alternative of an equality test:

Take a second to run this code and confirm that every little thing nonetheless works.

Now exchange loses_to with a dictionary implementing the foundations for “Rock Paper Scissors Lizard Spock.” This is what that appears like:

The brand new play() operate works with these new guidelines flawlessly:

For my part, this can be a nice instance of the facility of choosing the right knowledge construction. By utilizing units to characterize all the objects that lose to a key within the loses_to dictionary and changing == with in, you’ve got made a extra basic resolution with out having so as to add a single line of code.

Let’s step again and take a barely totally different strategy. As a substitute of wanting up objects in a dictionary to find out the winner, we’ll construct a desk of all potential inputs and their outcomes.

You continue to want one thing to characterize the foundations of the sport, so let’s begin with the loses_to dict from the earlier resolution:

Subsequent, write a operate build_results_table() that takes a guidelines dictionary, like loses_to, and returns a brand new dictionary that maps states to their outcomes. For example, this is what build_results_table() ought to return when referred to as with loses_to as its argument:

If you happen to assume one thing seems to be off there, you’re proper. There are two issues mistaken with this dictionary:

  1. Units like "rock", "rock" cannot exist. Units cannot have repeated components. In an actual state of affairs, this set would appear like "rock". You do not really need to fret about this an excessive amount of. I wrote these units with two components to make it clear what these states characterize.
  2. You’ll be able to’t use units as dictionary keys. However we need to make use of units as a result of they maintain commutativity for us robotically. That’s, "rock", "paper" and "paper", "rock" consider equal to one another and may subsequently return the identical end result upon lookup.

The best way to get round that is to make use of Python’s built-in frozenset type. Like units, frozensets assist membership checks, and so they evaluate equal to a different set or frozenset if and provided that each units have the identical members. Not like commonplace units, nonetheless, frozenset situations are immutable. In consequence, they can be utilized as dictionary keys.

To implement build_results_table() you may loop over every of the keys within the loses_to dictionary and construct a frozenset occasion for every of the strings values within the set comparable to the important thing:

This will get you about midway there:

The states that end in a tie aren’t coated, although. So as to add these, you have to create frozenset situations for every key within the guidelines dictionary that map to the string "tie":

Now the worth returned by build_results_table() seems to be proper:

Why undergo all this bother? In spite of everything, build_results_table() seems to be extra difficult than the play() operate from the earlier resolution.

You’re not mistaken, however I need to level out that this sample could be fairly helpful. If there are a finite variety of states that may exist in a program, you may typically see dramatic boosts in velocity by precalculating the outcomes for all of these states. This may be overkill for one thing so simple as “Rock Paper Scissors,” however may make an enormous distinction in conditions the place there are tons of of 1000’s and even thousands and thousands of states.

One real-world state of affairs the place this kind of strategy is sensible is the Q-learning algorithm utilized in reinforcement studying functions. In that algorithm, a desk of states — the Q-table — is maintained that maps every state to a set of possibilities for some pre-determined actions. As soon as an agent is educated, it could select an motion based mostly on the possibilities for an noticed state after which act accordingly.

Usually, a desk just like the one generated by build_results_table() is computed after which saved in a file. When this system runs, the pre-computed desk will get loaded into reminiscence after which utilized by the applying.

So, now that you’ve a operate that may construct a outcomes desk, assign the desk for loses_to to an outcomes variable:

Now you may write a play() operate that appears up the state within the outcomes desk based mostly on the arguments handed to play after which returns the end result:

This model of play() is extremely easy. Simply two strains of code! You can even write it as a single line when you wished to:

Personally, I choose the two-line model over the single-line model.

Your new play() operate follows the foundations of the sport and is commutative:

play() even raises a KeyError if it will get referred to as with an invalid alternative, however the error is much less useful now that the keys of the outcomes dictionary are units:

The imprecise error would seemingly not be a problem, nonetheless. On this article, you’re solely implementing the play() operate. In a real implementation of “Rock Paper Scissors,” you’d most certainly seize consumer enter and validate that earlier than ever passing the consumer’s option to play().

So, how a lot sooner is that this implementation versus the earlier ones? Listed below are some timing outcomes to match the efficiency of the assorted implementations utilizing IPython’s %timeit magic operate. play1() is the model of play() from the Advanced Solution #2 part, and play2() is the present model:

On this case, the answer utilizing the outcomes desk is definitely slower than the earlier implementation. The offender right here is the road that converts the operate arguments to a frozenset. So, though dictionary lookups are quick, and constructing a desk that maps states to outcomes can probably enhance efficiency, you have to watch out to keep away from costly operations which will find yourself negating no matter good points you anticipate to get.

I wrote this text as an train. I used to be curious to understand how I’d strategy a newbie undertaking like “Rock Paper Scissors” in Python now that I’ve loads of expertise. I hope you discovered it fascinating. When you’ve got any inkling of inspiration now to revisit a few of your individual newbie initiatives, then I feel I’ve carried out my job!

If you happen to do revisit a few of your individual newbie initiatives, or when you’ve carried out so up to now, let me know the way it went within the feedback. Did you be taught something new? How totally different is your new resolution from the one you wrote as a newbie?

An acquaintance from the Julia world, Miguel Raz Guzmán Macedo, turned me on to a blog post by Mosè Giordano. Mosè leverages Julia’s multiple dispatch paradigm to put in writing “Rock Paper Scissors” in lower than ten strains of code:

I gained’t get into the small print of how Mosè’s code works. Python doesn’t even assist a number of dispatch out-of-the-box. (Though you need to use it with some assist from the plum package.)

Mosè’s article received my psychological gears spinning and inspired me to revisit “Rock Paper Scissors” in Python to consider how I may strategy the undertaking in a different way.

As I used to be working by the answer, nonetheless, I used to be reminded of an article I did a reviewed for Actual Python fairly a while in the past:

It seems the primary two options I “invented” listed here are much like the answer that Chris Wilkerson, the writer of Actual Python’s article, got here up with.

Chris’s resolution is extra full-featured. It consists of an interactive gameplay mechanism and even makes use of Python’s Enum kind to characterize recreation objects. That should have additionally been the place I first heard of “Rock Paper Scissors Lizard Spock.”

More Posts