Can We Fit Rock Paper Scissors in Python in a Tweet?

Can We Fit Rock Paper Scissors in Python in a Tweet Featured Image

If you’ve followed me on this saga to shrink my original behemoth of a solution to Rock Paper Scissors, then you know we’ve moved 1,389 characters down to 864 by introducing modular arithmetic. Then, we shrunk the program again down to 645 characters through some refactoring. Now, we’re going to try to get the program down to the size of a tweet or 280 characters. Can it be done?

This article is not clickbait. It is absolutely possible to write Rock Paper Scissors in 280 characters, and I did it! That said, I don’t think it’s possible without making some sacrifices to the original requirements.

At any rate, let’s get started!

Where Did We Leave Off?

At this point in time, here’s the latest version of the program:

import random

# Generate default outcome
choices = ["Rock", "Paper", "Scissors"]
pc_index = random.randint(0, 2)
pc_choice = choices[pc_index]
output = [f"I chose {pc_choice}", "You chose nothing.", "You lose by default."]

# Play game
user_pick = input("Choose Rock (0), Paper (1), or Scissors (2): ")
if user_pick.isdecimal() and (user_index := int(user_pick)) in range(3):
  user_choice = choices[user_index]
  output[1:] = [
    f"You chose {user_choice}", 
    [
      "Tie!", 
      f"{user_choice} beats {pc_choice} - you win!", 
      f"{pc_choice} beats {user_choice} - I win!"
    ][(user_index - pc_index) % 3]]

# Share outcome
print("\n".join(output))

Currently, our program sits comfortably at 644 characters, and it’s still very readable. Ultimately, what I want to do now is exploit a few things that can be found in my obfuscation article—namely remove spaces and shorten variable names. In addition, we’re going to try a few tricks in this code golf threadOpens in a new tab.. Let’s get started!

Begin Compression

Throughout the remainder of this article, I’ll be documenting my entire process for trying to shrink Rock Paper Scissors down to 280 characters (a.k.a. the size of a tweet).

As a quick warning, compressing code by hand is a long and messy process, and there are definitely better ways to go about it. That said, one of the things that I find missing in education is “expert” rationale. I’m not considering myself an expert here, but I think it’ll be valuable to see my approach to problem solving.

And if nothing else, you can watch me struggle to get this done! Don’t worry. I manage to get it down to tweet size—not without a few bumps along the way.

Iterable Unpacking

One of the very first suggestions in that code golf thread is to take advantage of iterable unpacking when assigning variables. In our case, we have several variables assignments at the top that we might try merging. For example, we might take the following:

choices = ["Rock", "Paper", "Scissors"]
pc_index = random.randint(0, 2)
pc_choice = choices[pc_index]
output = [f"I chose {pc_choice}", "You chose nothing.", "You lose by default."]

And, turn it into this:

choices, pc_index, pc_choice, output = ["Rock", "Paper", "Scissors"], random.randint(0, 2), choices[pc_index], [f"I chose {pc_choice}", "You chose nothing.", "You lose by default."]

Sadly, this doesn’t really have the payoff I was expecting. Perhaps because the answer I’m referencing uses a string as the iterable. That said, I’m determined to squeeze some sort of payoff out of this, so I’m going to try to restructure it:

*choices, pc_index, pc_choice = "Rock", "Paper", "Scissors", random.randint(0, 2), choices[pc_index]
output = [f"I chose {pc_choice}", "You chose nothing.", "You lose by default."]

Okay, so this was a bit of a let down, but it might help us later. Let’s retain the original program for now and try something else!

Rewriting Input String

Since all of our choices are stored in a list, I figured we could try dynamically generating the input string. Perhaps that would be a bit cleaner. In other words, instead of writing this:

user_pick = input("Choose Rock (0), Paper (1), or Scissors (2): ")

We could write something like this:

user_pick = input(f'{", ".join(choices)}! (0, 1, 2):')

Now, that’s some savings! It’s not quite as explicit as the original, but we’re going for compression. I’ll take 54 characters over 66 any day. Here’s what the program looks like now:

import random

# Generate default outcome
choices = ["Rock", "Paper", "Scissors"]
pc_index = random.randint(0, 2)
pc_choice = choices[pc_index]
output = [f"I chose {pc_choice}", "You chose nothing.", "You lose by default."]

# Play game
user_pick = input(f'{", ".join(choices)}! (0, 1, 2):')
if user_pick.isdecimal() and (user_index := int(user_pick)) in range(3):
  user_choice = choices[user_index]
  output[1:] = [
    f"You chose {user_choice}", 
    [
      "Tie!", 
      f"{user_choice} beats {pc_choice} - you win!", 
      f"{pc_choice} beats {user_choice} - I win!"
    ][(user_index - pc_index) % 3]]

# Share outcome
print("\n".join(output))

Now, we’re down to 653! Don’t worry; bigger changes are ahead.

Renaming the Import

At this point, I don’t think there is any way around using the random library. That said, we can give it a name which could save us a couple characters. In other words, instead of rocking this:

import random
pc_index = random.randint(0, 2)

We could try something like this:

import random as r
pc_index = r.randint(0, 2)

Unfortunately, a change like this doesn’t actually save us any characters: 45 no matter how you slice it! That said, this may have worked if we used random multiple times.

Renaming All Variables

At this point, I don’t see any value in trying to play with the existing code. Let’s go ahead and shrink all our variables and optimize on the other end if we’re still out of range. Here’s what that would look like:

import random

# Generate default outcome
a = ["Rock", "Paper", "Scissors"]
b = random.randint(0, 2)
c = a[b]
d = [f"I chose {c}", "You chose nothing.", "You lose by default."]

# Play game
e = input(f'{", ".join(a)}! (0, 1, 2):')
if e.isdecimal() and (f := int(e)) in range(3):
  g = a[f]
  d[1:] = [
    f"You chose {g}", 
    [
      "Tie!", 
      f"{g} beats {c} - you win!", 
      f"{c} beats {g} - I win!"
    ][(f - b) % 3]]

# Share outcome
print("\n".join(d))

Now, we’re down to 470 characters! How’s that for savings? We’re on our way to tweet size. Up next, let’s try removing all the comments and empty lines.

Removing Comments and Empty Lines

Another quick change we can make is the removal off all comments and empty lines. That way, we just get a wall of code like this:

import random
a = ["Rock", "Paper", "Scissors"]
b = random.randint(0, 2)
c = a[b]
d = [f"I chose {c}", "You chose nothing.", "You lose by default."]
e = input(f'{", ".join(a)}! (0, 1, 2):')
if e.isdecimal() and (f := int(e)) in range(3):
  g = a[f]
  d[1:] = [
    f"You chose {g}", 
    [
      "Tie!", 
      f"{g} beats {c} - you win!", 
      f"{c} beats {g} - I win!"
    ][(f - b) % 3]]
print("\n".join(d))

Unfortunately, this only buys us another 58 characters. Now, we’re sitting at 412 characters. How will we ever cut another 132 characters? Well, we can start trimming away spaces.

Eliminating Extraneous Spaces

At this point, I’m starting to grasp at straws, so I figured we could try removing any unnecessary spaces. For example, do we really need spaces around our assignment operators? Of course not! See:

import random
a=["Rock","Paper","Scissors"]
b=random.randint(0,2)
c=a[b]
d=[f"I chose {c}","You chose nothing.","You lose by default."]
e=input(f'{", ".join(a)}! (0, 1, 2):')
if e.isdecimal() and (f:=int(e)) in range(3):
  g=a[f]
  d[1:]=[f"You chose {g}",["Tie!",f"{g} beats {c} - you win!",f"{c} beats {g} - I win!"][(f-b)%3]]
print("\n".join(d))

Now, this really does a number on the total count. Unfortunately, it’s not quite enough! We’re only down to 348 characters. How will we shave off another 68? Well, since we’re on the topic of removing extra spaces, how about in our winner strings? Take a look:

import random
a=["Rock","Paper","Scissors"]
b=random.randint(0,2)
c=a[b]
d=[f"I chose {c}","You chose nothing.","You lose by default."]
e=input(f'{", ".join(a)}! (0, 1, 2):')
if e.isdecimal() and (f:=int(e)) in range(3):
  g=a[f]
  d[1:]=[f"You chose {g}",["Tie!",f"{g} beats {c}—you win!",f"{c} beats {g}—I win!"][(f-b)%3]]
print("\n".join(d))

That shaves off another four characters! Now, we’re just 64 away from freedom (i.e. 344 in total), and I have a couple ideas.

Crushing Branches

One idea I had was to see if we could reduce the if statement into a single line. To do that, we’ll need to remove the creation of g. The result looks like this:

import random
a=["Rock","Paper","Scissors"]
b=random.randint(0,2)
c=a[b]
d=[f"I chose {c}","You chose nothing.","You lose by default."]
e=input(f'{", ".join(a)}! (0, 1, 2):')
if e.isdecimal() and (f:=int(e)) in range(3):d[1:]=[f"You chose {a[f]}",["Tie!",f"{a[f]} beats {c}—you win!",f"{c} beats {a[f]}—I win!"][(f-b)%3]]
print("\n".join(d))

Unfortunately, it seems g was doing a lot of the heavy lifting because this only shaved off a couple characters! Oh well, we’re down to 341. What else can we do?

Removing Redundant Brackets

At this point, I’m really running out of options. That said, one idea I had was to remove any brackets that weren’t doing anything useful. For example, our a list stores the choices of Rock Paper Scissors. Surely, we can turn that into a tuple, right? Well, here’s to saving two more characters:

import random
a="Rock","Paper","Scissors"
b=random.randint(0,2)
c=a[b]
d=[f"I chose {c}","You chose nothing.","You lose by default."]
e=input(f'{", ".join(a)}! (0, 1, 2):')
if e.isdecimal() and (f:=int(e)) in range(3):d[1:]=[f"You chose {a[f]}",["Tie!",f"{a[f]} beats {c}—you win!",f"{c} beats {a[f]}—I win!"][(f-b)%3]]
print("\n".join(d))

Unfortunately, similar ideas can’t be used on the d list. That said, the list used in slice assignment can absolutely be trimmed:

import random
a="Rock","Paper","Scissors"
b=random.randint(0,2)
c=a[b]
d=[f"I chose {c}","You chose nothing.","You lose by default."]
e=input(f'{", ".join(a)}! (0, 1, 2):')
if e.isdecimal() and (f:=int(e)) in range(3):d[1:]=f"You chose {a[f]}",["Tie!",f"{a[f]} beats {c}—you win!",f"{c} beats {a[f]}—I win!"][(f-b)%3]
print("\n".join(d))

From here, though, there doesn’t appear to be any lists that we can trim. That said, we’ve saved even more characters. Now we’re down to 337! Can we achieve 280?

Reducing Redundant Strings

At this point, I had an ephany! What if we referenced d when building the successful game string? In other words, why type out “You chose” twice when we can extract it from d? Here’s what that would look like:

import random
a="Rock","Paper","Scissors"
b=random.randint(0,2)
c=a[b]
d=[f"I chose {c}","You chose nothing.","You lose by default."]
e=input(f'{", ".join(a)}! (0, 1, 2):')
if e.isdecimal() and (f:=int(e)) in range(3):d[1:]=f"{d[1][:10]}{a[f]}",["Tie!",f"{a[f]} beats {c}—you win!",f"{c} beats {a[f]}—I win!"][(f-b)%3]
print("\n".join(d))

Sadly, this bit of trickery actually costs us a character. Even in the best case scenario, we’d only break even. So, what if instead we just saved “You chose” into a variable? Here’s the result:

import random
a="Rock","Paper","Scissors"
b=random.randint(0,2)
c=a[b]
g="You chose "
d=[f"I chose {c}",f"{g}nothing.","You lose by default."]
e=input(f'{", ".join(a)}! (0, 1, 2):')
if e.isdecimal() and (f:=int(e)) in range(3):d[1:]=f"{g}{a[f]}",["Tie!",f"{a[f]} beats {c}—you win!",f"{c} beats {a[f]}—I win!"][(f-b)%3]
print("\n".join(d))

Again, we lose a couple characters! Perhaps if these strings weren’t so short, we’d get some type of savings, but this has been a huge letdown so far. Let’s try something else!

Removing Function Calls

With 57 characters to shave, I’m not sure we’ll meet our goal. However, we can keep trying. For instance, I already know a place where we can trim a couple characters. And, I might even let it serve double duty! Let’s go ahead and remove our call to range():

import random
a="Rock","Paper","Scissors"
g=0,1,2
b=random.choice(g)
c=a[b]
d=[f"I chose {c}","You chose nothing.","You lose by default."]
e=input(f'{", ".join(a)}! {g}:')
if e.isdecimal() and (f:=int(e)) in g:d[1:]=f"You chose {a[f]}",["Tie!",f"{a[f]} beats {c}—you win!",f"{c} beats {a[f]}—I win!"][(f-b)%3]
print("\n".join(d))

By storing our choices as a tuple, we were able to delete our call to range(). At the same time, I saw an opportunity to replace part of the input string with our new tuple. Even better, we no longer have to use the randint() function of random. Instead, we can pull a random choice from our tuple. Talk about triple duty!

While this is very exciting, we only managed to save 8 characters (i.e. 329 in total). With 49 to go, I’m not sure we’re going to hit our goal, but we can keep trying!

Converting Lists to Strings

One thing I thought we could try that might be slightly more drastic would be to overhaul d, so it’s a string rather than a list. In other words, if we can somehow get rid of the need for lists, we can drop the call to join() and print out the string directly. I think it’s worth a shot! Here’s what that would look like:

import random
a="Rock","Paper","Scissors"
g=0,1,2
b=random.choice(g)
c=a[b]
d=f"I chose {c}\nYou chose nothing.\nYou lose by default."
e=input(f'{", ".join(a)}! {g}:')
if e.isdecimal() and (f:=int(e)) in g:d=f"{d[0:9+len(c)]}You chose {a[f]}\n{['Tie!',f'{a[f]} beats {c}—you win!',f'{c} beats {a[f]}—I win!'][(f-b)%3]}"
print(d)

Despite this change, we only manage to save a single character. Instead, let’s try something else!

Another idea I had was to try using a string list instead of a numeric list of g. One of the main problems with this program is that we have to validate the input. Perhaps the easiest way to validate it is to check for the three values we expect directly. In other words, make g store strings and convert them back to integers as needed:

import random
a="Rock","Paper","Scissors"
*g,='012'
b=random.randint(0,2)
c=a[b]
d=[f"I chose {c}","You chose nothing.","You lose by default."]
e=input(f'{", ".join(a)}! {g}:')
if e in g:d[1:]=f"You chose {(f:=a[int(e)])}",["Tie!",f"{f} beats {c}—you win!",f"{c} beats {f}—I win!"][(int(e)-b)%3]
print("\n".join(d))

Surprisingly, this actually works! By cleaning up our if statement, we managed to save another 14 characters. Now, we’re down to 315. Can we remove 35 more?

Using Walrus Operators Everywhere

Another idea I had was to use walrus operators in the place of traditional assignment. Unfortunately, this doesn’t seem to actually save any characters because the walrus operator has an extra character. In addition, it often has to be embedded in parentheses to work. That said, I gave it a shot for fun!

import random
*g,='012'
d=[f"I chose {(c:=(a:=('Rock','Paper','Scissors'))[(b:=random.randint(0,2))])}","You chose nothing.","You lose by default."]
if (e:=input(f'{", ".join(a)}! {g}:')) in g:d[1:]=f"You chose {(f:=a[int(e)])}",["Tie!",f"{f} beats {c}—you win!",f"{c} beats {f}—I win!"][(int(e)-b)%3]
print("\n".join(d))

Now, this is a complete nightmare! But, surprisingly, there’s not a ton of additional baggage. As far as I can tell, this brings us back to 321 characters, and it works. So, let’s backtrack!

Taking Advantage of the Import

While reading through that code golf thread, I found this gem. Rather than importing random and using it, I can import all things random and save a character:

from random import*
a="Rock","Paper","Scissors"
*g,='012'
b=randint(0,2)
c=a[b]
d=[f"I chose {c}","You chose nothing.","You lose by default."]
e=input(f'{", ".join(a)}! {g}:')
if e in g:d[1:]=f"You chose {(f:=a[int(e)])}",["Tie!",f"{f} beats {c}—you win!",f"{c} beats {f}—I win!"][(int(e)-b)%3]
print("\n".join(d))

It’s not much, but we’re totally in “not much” territory. In other words, with 34 characters to go, a single character might be all we need!

Revisiting String Manipulation

A few sections ago I had mentioned that converting the lists to strings didn’t pay off. Well, I found a way to make it work!

from random import*
a="Rock","Paper","Scissors"
*g,='012'
b=randint(0,2)
c=a[b]
h=" chose "
d=f"I{h}{c}\nYou{h}nothing\nYou lose by default"
e=input(f'{", ".join(a)}—{g}:')
if e in g:d=f"I{h}{c}\nYou{h}{(f:=a[int(e)])}\n{['Tie',f'{f} beats {c}—you win',f'{c} beats {f}—I win'][(int(e)-b)%3]}"
print(d)

Previously, I had some slicing nonsense that required a computation to get right. This time around, I figured we could replicate the strings exactly. Then, replace duplicate words with a variable. And, somehow, it worked!

Now, we’re sitting pretty at 301 characters. We’re getting dangerously close to 280, and I’m starting to get excited.

From here, I started thinking: “what would happen if we started removing some of the duplication in the strings?” Well, it didn’t work out:

from random import*
a="Rock","Paper","Scissors"
*g,='012'
b=randint(0,2)
c=a[b]
h,i,j,k,n="chose "," beats ","You ","I ","\n"
l=k+h+c+n+j+h
d=f"{l}nothing{n+j}lose by default"
e=input(f'{", ".join(a)}—{g}:')
if e in g:d=f"{l}{(f:=a[int(e)])}\n{['Tie',f'{f+i+c+n+j}win',f'{c+i+f+n+k}win'][(int(e)-b)%3]}"
print(d)

Not only is this ridiculously unreadable, but it’s also a larger program than before. So, I scrapped it and started from the previous version.

Reworking User Prompts

At this point, I felt sort of defeated, so I decided to remove language from the game strings. For example, instead of saying “You”, the program says “U”. In the end, it came down to having a little bit of fun with it:

from random import*
a="Rock","Paper","Scissors"
*g,='012'
b=randint(0,2)
c=a[b]
h=" chose "
d=f"I{h}{c}\nU{h}death\nI win"
e=input(f'{", ".join(a)}!—{g}:')
if e in g:d=f"I{h}{c}\nU{h}{(f:=a[int(e)])}\n{['Tie',f'{f} beats {c}—u win',f'{c} beats {f}—I win'][(int(e)-b)%3]}"
print(d)

In other words, I basically let go of having exactly the same output. For example, I was able to swap words like “You” to “U” and “Nothing” to “Death”. In a sort of endearing way, I feel like these changes make the program better: it makes me feel like an edgy teen.

That said, the true magic of these changes is we’ve managed to squash the program from 301 characters to exactly 280. That’s it! That’s Rock Paper Scissors in a tweet.

Lessons Learned

Pretty much the moment I got this code down to 280 characters, I went ahead and posted it:

That said, as you can see, I’m not sure all the effort was worth it. At best, two of my friends found it funny, but I’m not sure it’s something I’ll do again. Honestly, there’s only so much meme value you can get out of something like this, and it just didn’t hit like I expected.

Of course, if you thought this was funny and you’d like to do something similar, let me know! I’d love for a silly challenge like this to take over social media for a day or two. And if not, no worries! I had fun trying to do this.

Also, I know this article is kind of messy, so let me know if you’d like me to distill some of what I’ve learned into another article. It would enjoy doing that! But, only if it’s what you want to read. It’s certainly not something people are going to search on Google.

At any rate, thanks again for sticking it out with me! In case you missed it, here are the articles that inspired this one:

Likewise, here are some Python related resources from the folks at Amazon (#ad):

Finally, if you’d like to support the site directly, here’s a list of ways you can do that. Otherwise, thanks for stopping by! Take care.

Jeremy Grifski

Jeremy grew up in a small town where he enjoyed playing soccer and video games, practicing taekwondo, and trading Pokémon cards. Once out of the nest, he pursued a Bachelors in Computer Engineering with a minor in Game Design. After college, he spent about two years writing software for a major engineering company. Today, he pursues a PhD in Engineering Education in order to ultimately land a teaching gig. In his spare time, Jeremy enjoys spending time with his wife, playing Overwatch and Phantasy Star Online 2, practicing trombone, watching Penguins hockey, and traveling the world.

Recent Content