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 thread. 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 epiphany! 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):
- Effective Python: 90 Specific Ways to Write Better Python
- Python Tricks: A Buffet of Awesome Python Features
- Python Programming: An Introduction to Computer Science
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.
Recent Code Posts
Python has a cool feature that allows you to overload the operators. Let's talk about what that means and how you might use it!
This week, we're hitting another beginner topic: the assignment operator. While the idea is simple, the concept is rich in related ideas like scope, iterable unpacking, and augmented assignment.