Flappy Bird in 1234 bytes of Bash
Posted on Thu 25 August 2016 in Code
Contrary to an infamous opinion from a bygone era, 640KB is not really sufficient for anyone anymore. A typical website exceeds that easily, and executable programs are usually measured in megabytes.
But what if you only had 1234 bytes to work with?…
A friend of mine, Gynvael Coldwind, organized a game programming compo1 that had precisely this limitation. Unlike most demoscene ones, however, the size limit here applies to either the final binary or its source code. This can be chosen at the participant’s discretion.
Since my currently favorite compiled language produces the exact opposite of small binaries, I was quite intrigued by the source code option. But as the rules say, the final game must run on a clean installation (only standard packages) of either Windows or Ubuntu Linux. The choice of viable languages and technologies was therefore rather limited.
It was time to get a little creative.
Game theory
What must an environment provide to be a suitable platform for game development? Not much, really. We only need to be able to:
- put stuff on the screen
- react to user input
- execute time-dependent logic
You could arguably get away without the last one, but the kind of games you would end up with had gone out of fashion about half a century ago. For the “real” arcade games, we really ought to run our code at least a dozen times per second.
There’s only a handful of standard technologies that allow all of this out of the box.
I’m a wee bit out of touch with Windows these days but on Linux, there’s one thing that I really wanted to take for a serious spin. And luckily for me, it also has one extremely terse language to go hand in hand with.
I’m talking, of course, about the ANSI terminal that can be scripted in Bash. If there ever was anything that worked anywhere by default, then this got to be it2.
…put into practice
Note that I’ve stressed the “terminal” part. The shell itself is a neat instrument, but (perhaps surprisingly) it doesn’t actually concern itself with displaying anything on the screen.
This has traditionally been the job of a terminal emulator. To this end, it has a couple of special codes that are undoubtedly useful for an aspiring indie shell game developer. They are what allows us to display things in a specific position on the screen, complete with chosen color, background color, and (text) style.
So this nails down our first requisite feature.
As for the second one, the vanilla read
command
supports everything we may need for handling user input.
The only real “trick” is passing the -n
flag
which makes it wait for a specific number of characters (e.g. one)
rather than a whole line ending with Enter.
Add a few more flags — like the one that prevents text from being echoed back to the console —
and you can make a rudimentary input loop:
KEY='\0'
while :; do
read -rsn 1 KEY
done
I can imagine, however, that you’d want to do other things besides just waiting for input. Stuff like “updating the game state” and “drawing the next frame” is generally considered pretty important in games.
Normally, we would deal with those things in between checking for input events, leading to a particular structure of the so-called real-time loop.
But the shell doesn’t really handle input via “events”. Instead, you just ask for some text and wait until you get it. There is no “peek mode” that’d allow to squeeze in some rendering logic before the next key press.
What do we do, then, with a tight loop that leaves us no wiggle room?…
Why, we take a crowbar and pry it open!
(Don’t) be alarmed
Let’s start by noticing that to run some code whenever there is nothing else to do
has a rough equivalent of running it periodically.
This isn’t an exactly new observation:
the setTimeout
function in JavaScript
has been the basis of “real-time” animation
since the 90s era of falling snowflakes, and up to the contemporary browser games3.
Neither does the shell nor the hosting terminal support anything like setTimeout
, though.
But fortunately, they don’t need to: Linux itself does.
And it accomplishes it quite effortlessly, due to the sole fact of being an operating system.
All we have to do is access some of its capabilities directly from the shell script:
KEY='\0'
DT=0.05 # timeout value in seconds
tick() {
# .. do stuff ...
( sleep $DT; kill ALRM $$ )&
}
trap tick ALRM
tick
while :; do
read -rsn 1 KEY
done
What we’re doing here is set up the tick
function to be
a signal handler.
A callback, if you will.
Inside of this callback, we can do all the state updates and drawing we need,
as long as we follow it with “scheduling” of the next tick
call.
As a direct equivalent of a setTimeout
invocation, this can be done by:
- starting a subshell to run in the background (with
&
) - letting it sleep for however long we want to delay the next update
- sending a signal to the main script (
kill $$
)
The signal we chose is of course SIGALRM
4.
Technically, however, it can be anything,
as long as we can set up a trap
to actually handle it.
In any case, success! Bash is officially a game programming platform!
Integration in parts
And so having figured out the technicalities, I was faced with the crucial dilemma: what game could I actually write?
Nothing too complicated, that’s for sure. After the initial scaffolding has used up about 1/4 of the harsh size limit, I knew that radical simplicity was the order of the day.
And so I went for possibly the most trivial game ever.
Sorry, Pong!
Then, after hours of (ahem) meticulous research, I managed to reverse-engineer the core mechanic:
- let the bird fall down with a constant acceleration
- to jump, give it some upwards-facing velocity
Actually coding this in Bash was mostly a matter of finding out how to perform floating-point calculations. Rather unsurprisingly, this is done through an external program, while truncating of the fractional part involves — wait for it — string formatting.
Pipe dream
Based on the above nuggets of Stack Overflow wisdom, you’ve probably figured out that Bash isn’t exactly what you would call a programming language. With a little bit of perseverance, however, we can make it do our bidding… some fraction of the time.
So far, I had the player character — a beautiful red rectangle — fall down under the constant force of gravity, and maybe ascend if the Space key has been pressed. But a heroic protagonist necessitates the presence of formidable adversaries, so my next step was to figure out how to implement this crucial gameplay mechanic.
Which one?… Pipes, of course.
Pipes in Bash.
It was pretty evident I’m gonna need to represent them somehow, and Bash isn’t exactly known for its strong repertoire of data structures. Starting from version 4.0, it does however have arrays, so there is at least something we can work with.
Let’s not get too carried away, though. The somewhat obvious idea of mirroring the entire game field in a (pseudo) 2D array of pipe/not-pipe turned out to be completely unworkable. The fill rate of most (all?) terminal emulators is nowhere near sufficient to permit redrawing of the whole screen and maintaining FPS value above the slideshow threshold.
What I went with instead was a 1D array for the pipe itself, and a separate variable to denote its horizontal position. Working from there, it wasn’t too hard to make it move, and eventually to check for its collision with the player object.
Fitting in
That, of course,
was the most important milestone.
I added an objective.
It was an actual game.
And I still had about 100 bytes left!
Speaking of size, this is probably a good moment to talk about making the most of those meager 1234 bytes. It’s not exactly surprising that it was possible mostly thanks to minification.
While it’s extremely popular for JavaScript, the same abundance of minification utilities cannot be expected when it comes of shell scripts. Still, “bash minification” does return some useful search results, and one of them is what I used to shrink the final script.
Obviously, it didn’t go without some trouble. Since the minifier does little more than to swap newlines for semicolons, it got a few bugs that had to be ironed out. No big deal, really: a small batch of handcrafted, artisanal Python was enough to paper over the issues.
The other technique you can use to slim down is obfuscation, i.e. shortening of the identifiers. As the minifier didn’t offer this feature natively, I had to take care of it myself.
This lead to adding such interesting assignments as this p
:
p=printf
which absolutely shouldn’t be confused with this p
:
# put text at given position: p $x $y $text
p() { echo -en "\e[$2;${1}f$3"; }
The reason it works is that in POSIX shells, variables and functions effectively form two separate namespaces. Their members are thus referred to in two different ways:
p $X $Y "\e[1;37;41mB" # call the p() function
$p "\e[?25l" # expand the p variable (i.e. call `printf`)
Notice how functions have longer definitions but shorter usage, while the opposite is true for variables. Who can now say that Bash doesn’t find balance in all things?
Auditory sensations
Like I mentioned before, thanks to those and similar tricks I had managed to carve out about a hundred or so bytes of free space.
Now, what could you possibly do with such a staggering amount?
…no, that won’t even be one tweet.
Well, let’s add some sound effects, shall we?
Before you think that’s preposterous, remember the terminal bell.
Sounding the bell is as simple as printing the "\a"
character (ASCII 7),
which for this reason is also known as BEL
:
echo -e "\a"
Unfortunately, most terminal emulators silence the actual sound, and replace it with a visual indicator — typically a bell icon. If we want to make speakers reliably emit audible phenomena, we sadly have to look elsewhere.
Fortunately, modern Linux systems handle the sound card somewhat better than you may have remembered from a few years ago. This is usually thanks to ALSA, a dedicated subsystem in the Linux kernel, and its numerous userspace complements.
One of them is the inconspicuous speaker-test
binary
which, well, does exactly what it says on the can:
speaker-test # play some noise through the speakers
You can make it play a WAV file, too, but the most interesting option is to synthesize a sine wave. By adjusting its frequency, it’s easy to play higher and lower tones, forming the building blocks for more complex sounds.
What you cannot control is the tone’s duration.
That’s not a big problem, though, since we can run speaker-test
in a separate process
and then just kill
it dead:
# play a sine wave (requires ALSA): s $frequency $duration
s() { ( speaker-test >$n -t sine -f $1 )& _p=$!; sleep $2; kill -9 $_p; }
I’ve used this approach
to play a simple, two-tone sound
whenever the player successfully overcomes a pipe obstacle.
And I would’ve probably taken it further if “speaker_test
” wasn’t such a damn long string.
Unfortunately, it was one identifier I couldn’t afford to shorten,
and this had put a stop to my ambitious plan of improvising a sad trombone
upon player’s failure :(
; done
It wouldn’t be right to say I wasn’t very happy with the results, though. All in all, it was the most fun I had with coding in quite some time, and definitely the most amusing Bash script I’ve ever written.
It also got me curious what other games people have implemented purely as shell scripts. To my disappointment, there hadn’t been all that many. Of those I could find, this Snake clone in about 7KB of (unobfuscated) Bash is probably the most polished one.
As you can see then, this is clearly an under-appreciated platform that evidently displays a lot of potential! If you want to create games that are both very portable and extremely space-efficient, Bash is definitely a technology you should have a closer look at ;-)
-
Here’s the original announcement post in Polish and its somewhat understandable Google-translated version. ↩
-
Yes, I’m ignoring the elephant in the room which is the web browser. It’s probably because a pile of minified JavaScript doesn’t strike me as very interesting anymore :) ↩
-
Nowadays, though, the
requestAnimationFrame
function is closer to the actual continuous processing in the background. ↩ -
Regular programs could simply call the
alarm
function instead of forking a subprocess. But then again, regular programs could just run a normal game loop. ↩