Flappy Dird
I made a game. It’s called Flappy Dird. It’s Flappy Bird inside MacOS Finder.
ad placements start at $2,000
It has instructions, high score tracking, and marquee banner ads. You double-click to start a game and select any file in the window to jump. It runs at 4 frames a second and can’t run much faster. It occasionally drops inputs for reasons that you’ll understand if you finish this blog.
I’m going to lay out how Flappy Dird works and how it got there. Head to the github repo if you want to check out the code or play the game yourself.
Idea to Prototype
The original idea for Flappy Dird came when I noticed that Finder had a “Date Last Opened” field for directories. I knew that the atime
(file access time) field was controversial (updating an inode on every file read is expensive!) and wanted to learn how similar date last opened was. I found a few things:
- The field only updated when opened via Finder;
cd
ing didn’t update the timestamp. - The field did update if you made a symlink to a directory and then double-clicked that symlink within Finder.
- The field was accessible (with second-level precision) via
mdls
This got me pretty excited! I like putting games in weird places, and I realized I could combine those three facts to make a button! The basic idea:
- Create a directory
dir
. Insidedir
make a directorybutton
that symlinks back todir
. - On startup, read the ‘last opened’ timestamp of
dir
. - Repeatedly poll the ‘last opened’ timestamp and do something when it changes.
- Open
button
(insidedir
) to change the ‘last opened’ timestamp without changing your location in Finder.
I brainstormed some ideas for iconic games that could be played with a single button and came up with Flappy Bird1 pretty quickly. And then I started trying to figure out how I’d draw something like Flappy Bird in Finder.
I spent a while looking into making Finder’s font monospaced (to make ascii art easier) and measuring out the width of various ascii characters in Finder’s default font before realizing that I had a much better option: emojis have a constant width and if you put them in filenames Finder will display them.
emojis in filenames. the future is here
So I had a way to accept clicks and a way to draw to the screen - enough for a prototype!
vsync does NOT work for emojis in Finder
The basic idea:
- Set up
dir
so that it has 15 subdirectories that symlink back todir
- Wait until the player double-clicks a directory to start the game. Treat all future double clicks as flaps.
- Write a function from
bird_y_pos,pipe_locations,frame
to a 15x15 grid of emojis - Every frame, rename every symlink in the directory to a row from our emoji grid.
- Do some hackery to rename the symlinks in the right order so that we can tell Finder to sort by ‘Date Modified’
This works! But it’s really slow and the screen tears a lot. We can do better.
Vsync at home: AppleScript and double buffering
The biggest problem with the prototype was that the screen tearing was bad. I figured I’d try to get Finder to “refresh” the file listing in case the tearing was because it wasn’t updating frequently enough. I stumbled upon this Stack Exchange question which pointed me to the AppleScript invocation tell application "Finder" to tell front window to update every item
.
This line helped a little bit (I still saw tearing), but more importantly it planted the seed of using AppleScript. It also delighted me - I find AppleScript totally bizarre. slomobo made this joke on twitter and it feels pretty correct:
this is an image; i have no idea how to embed a tweet anymore
I shopped the tearing problem around to some smart friends and a bunch suggested that I find a way to do double buffering. The basic idea of double buffering2 is:
- Have two buffers, buf1 and buf2
- On frame 1, display buf1 and start writing your data for frame 2 to buf2.
- On frame 2, display buf2 all at once! And start writing your data for frame 3 to buf1.
- Etc.
Or something like that. The point is that you avoid the jitter that comes from writing some but not all of the pixels of a new frame to the screen.
We came up with several ideas for how to do double buffering. The big ones were:
- Use symlinks to atomically swap out the directory’s contents. This doesn’t work because you can’t expand the contents of a symlinked dir in Finder (you have to double-click on it, at which point Finder dereferences the symlink and won’t follow renames).
- Make two directories whose inner symlinks point at each other. This would solve the tearing problem but would mean that we could only advance a frame when the user clicked, which wouldn’t really work for this game.
- Magically find a way to make Finder display a different directory without changing the “last opened” timestamp.
The winning solution came from my friend Jake, who suggested that AppleScript might have a way to control Finder. And it does! tell application "Finder" to set target of front Finder window to ("PATH" POSIX file)
does exactly what you’d think.
look at those buffers go
I hacked together a double buffering implementation and got the game running smoothly at 1 frame per second! But 1 FPS is slow. We can do better.
Tap(pleScript) to Flap(pleScript)
The game couldn’t reasonably run above 1 FPS because our input mechanism (double-clicking a file) only had second-level precision - if Flappy Dird ran at 2 FPS you’d only be able to jump every other frame. So I needed a new way to accept input. At this point I figured that AppleScript was all-powerful and could tell me whether an item in Finder was selected. I checked and it totally could! I reworked the code to accept selection of any file in the window as a jump.
This worked well and even matched the original game better than double clicking. The game logic became something like:
- Wait until the user double clicks
- On every frame, shell out to AppleScript and check whether the user has selected a file in the current window.
- If they have (or if the “last opened” timestamp has changed), make the bird jump.
- Shell out to AppleScript to change the directory displayed in Finder.
- Sleep so that we maintain a constant framerate (e.g. if the above steps took 0.1 seconds and we want to run at 2 FPS, sleep for 0.4 seconds).
tap to flap
This could…kind of get us to 2 FPS. Except for one problem: Shelling out to AppleScript was really slow. Like 0.2 seconds slow. Which meant:
- There was no hope of going above 2 FPS, since we had fixed AppleScript costs of 0.4 seconds.
- When running at 2 FPS there was only a tiny window where you could actually tap a file - if you tapped the file after we shelled out to AppleScript to check whether you had tapped a file we’d miss the tap!
So the game wasn’t super playable at 2 FPS. And 2 FPS still felt too slow. We can do better.
Rewrite in Rust AppleScript
My profiling suggested that an AppleScript that just logged the number ‘1’ and exited took ~0.14 seconds. I assumed that that was a reasonable measure of AppleScript startup time and focused on bringing that number down.
BEWARE: Since writing this post I’ve realized that this is not a fair measure of AppleScript startup time, which is probably closer to 0.06 seconds (if you measure the time to run an empty AppleScript). I regret the error! The original version of the post continues below; since it’s about what I believed to be true when making the game it seems appropriate to leave it unedited. Just know that it’s not quite fair to AppleScript.
I searched for ways to improve AppleScript’s startup speed. I compiled my AppleScripts (basically useless), entertained ideas like “writing an AppleScript RPC server,” and repeatedly googled “AppleScript improve startup speed” and “AppleScript preload script.”
Eventually I posted in the Recurse Center chat and my friend Ian pitched a few suggestions. One of his first suggestions, “can you write the whole game in AppleScript?”, went a little far3 - but he also pitched inverting the control flow between my Python and AppleScript code: I could move my main loop to AppleScript while still shelling out to Python for most of the game logic, allowing me to only pay AppleScript’s startup cost once.
I was reluctant to do this because adding any amount of control flow to an AppleScript seemed hard - but I was also pretty excited to get to say “I rewrote it in AppleScript for speed.” Coming up with a way to communicate back from Python to AppleScript was tricky but I landed on something like:
do shell script "game.py await"
set shouldContinue to "continue"
repeat while shouldContinue = "continue"
do shell script "game.py start-frame"
tell application "Finder" to set sel to selection
set curBuf to do shell script "game.py tick " & (number of sel)
tell application "Finder" to set target of front ¬
Finder window to ("DIR" & curBuf as POSIX file)
set shouldContinue to do shell script "game.py sleep"
end repeat
That is:
- Wait for the user to double-click to start the game (
await
) - Record the time at which we’re starting the frame (
start-frame
) - Pass the number of files selected to
tick
to render the frame. - Save the response from
tick
(which is the name of the directory we just prepared) and navigate to it in Finder. - Sleep to achieve our target framerate and emit
continue
if the player hasn’t lost yet (so that we keep looping).
This worked really well! Ian pointed out to me that start-frame
and sleep
can be easily combined, and I ended up adding another layer of looping to add a way to restart after you die, but this is the basic structure that the game still uses.
Adding some flavor
The rest of the game was more straightforward - not necessarily easy, but it was just writing Python code to make the emojis on screen look right given some game state. A few notes about that process:
- To store state during and between runs I made a little
state.json
file that I read/wrote every frame - Adding text was hard because the text I chose was narrower than the emojis I was using. I used a lot of hardcoded spacing to make sure that things lined up correctly.
- Getting scrolling text across the top was particularly annoying because of the spacing - I ended up writing a function
read_n_ad_chars
to handle the guesswork around how many characters to show at a given time. - I didn’t want to mess with relaying the working directory to AppleScript, so the script has a bunch of template variables that get swapped out by the
first-time-setup
Python invocation. - AppleScript eats anything the python script outputs so I handled logging by appending to a file and catting it afterwards.
- You jump up an extra row if you tap on two successive frames, which I think makes the game feel a lot better.
The hardest bit here was the scrolling banner text. It was particularly tricky because I couldn’t use a debugger since the script was being invoked via AppleScript4.
Wrapping up
I loved making this. One thing I found particularly delightful was how simple writing the Python for this game was. The prototype was ~90 lines of code (it’s now ~550 lines but like a third of it is boilerplate or constants)! I did it all in vim! No clicking at all! It was great to work without an engine and keep all of my frame state in tiny 2D array. It was easy to keep the whole game in my head from start to finish (even as the control flow got wonkier). The experience made me excited to try making something bigger without an engine.
I presented an early iteration of Flappy Dird at Recurse Center and spent a whole lot more time on it because of the response it got during that presentation. Thanks so much to the folks at Recurse for the encouragement, especially since I was presenting 2 weeks after I “never graduated” (Recurse’s word for finishing a batch). If being encouraged to make nonsense like this sounds fun to you you should consider applying! And a special thank you to Kelin for telling me that the banner ads should be pulled by a little plane emoji.
4 frames a second and limited input is a pretty big constraint but I think that it’d be feasible to build some other games this way. Some folks on mastodon suggested building Tetris in Finder and I think that’s hard but feasible. If you want to chat about this please get in touch! I’ve been particularly enjoying cohost for gamedev chatting but I’m all over.
I’m currently on vacation (I wrote this on a train from Busan to Seoul) but I’ll be back with some more nonsense in November :)
-
Flappy Bird was probably on my mind because of a reference to it in the excellent game Chants of Sennaar, which I beat last month. ↩
-
I’m lying a little - there are a few approaches to double buffering / multiple buffering and some require a bunch of copying. Whatever, the idea comes across fine here, don’t worry about it. ↩
-
Although it was extremely tempting ↩
-
I bet there’s a clever workaround here. Please tell me if you know of one! ↩
get new posts via twitter, substack, rss, or a billion other platforms
or subscribe to my newsletter right from this page!