PewPew: Python games on Playdate (WIP)

Time to start a devlog about this, since I’m making some progress lately! The project has been going for about a year, so this first story-so-far post will be a little long.

PewPew

PewPew is a family of small homebrew game consoles designed to teach game programming with Python. With an 8×8 pixel display and an API whose documentation fits on one A4 page, you can’t get lost in the details but start from pure basics.

I am a big fan of the concept – I own numerous different PewPews, have written games for them, have run workshops with them, and even built my own.

It was for similar reasons that I got a Playdate – not because I am much interested in playing games on it, but because I thought it was a neat piece of hardware and was impressed by the maker spirit behind its creation. When the Playdate arrived, the question arose naturally: Can I run PewPew games on it? Let’s find out!

Lua Prototype

The first thing I made was a look-and-feel prototype in Lua. A demonstration of it and some more talk can be seen in this MakerCast episode on YouTube (17 min). I felt it had turned out nicely, the Snake game actually was as much fun to play as on any other PewPew device.

Embedding MicroPython

The next step would be to get actual Python code running on the Playdate. PewPew devices are powered by MicroPython or its derivative CircuitPython, implementations of the Python language specifically made for the constrained resources of microcontrollers. Being originally made for the same STM32 microcontrollers as used in the Playdate, MicroPython should run on the Playdate just fine. However, on microcontrollers MicroPython is usually used “bare metal”, with no operating system beneath. MicroPython is the operating system. Throwing the Playdate OS off the Playdate and replacing it by MicroPython would certainly be an interesting exercise, but is not what I am aiming for. It wouldn’t really be a Playdate anymore then, and I doubt many users would be willing to lose all their Playdate games in exchange for being able to run PewPew games.

What I want is to embed MicroPython into a fully fledged Playdate application that can live alongside other Playdate games. Unlike Lua, MicroPython is not specifically made for that purpose, but it does have some provisions for it. While it doesn’t easily compile into a library, among its “ports” to different microcontrollers there is one named “embed” that is not for a specific microcontroller but spits out a bunch of .c and .h files as its build product that can then be added into the build system of any C project on any platform.

In current releases, the embed port turned out to be somewhat limited though. MicroPython has a large number of features that can be individually enabled or disabled at compile time to tailor it to smaller or larger microcontrollers, and in the embed port, only the most basic of them were turned on. No filesystem, no math libraries, many things missing that would be required or expected of a PewPew device.

Working on the Mac, not on the Playdate for a while, I tried to enable more features in the embed port. I found that for some of them, that required changes to MicroPython itself. I have collected those changes in a draft pull request on the MicroPython repository, in hopes of eventually kneading them into a shape that can be accepted into official releases. A couple of weeks ago, I felt that that work had progressed far enough to tackle the Playdate again and try it out in a real project.

Taking that work into a Playdate SDK project was straightforward, except for a problem where the linker would complain about “undefined reference to `_write'” and a couple other functions. That was quickly solved by a forum search and applying the --specs=nosys.specs fix suggested there. I have not examined yet where exactly in MicroPython these references come from and whether they could be avoided. After writing some additional code to display text output on a terminal on the Playdate screen, the MicroPython example code ran without a hitch, and adding the ability to input text using the msg command on the USB serial connection also allowed me to run a REPL (interactive interpreter).

REPL Terminal

For the terminal, I was intending to use the monospaced font that comes with the system (the one used for “sharing DATA segment as USB drive” and similar messages), but could not find it in System/Fonts/. So I set out to make my own based on some traditional 8×16 bitmap fonts, but couldn’t make up my mind which one looked best, so for now my terminal has 4 different fonts cycled through by the A button: Ones based on Monaco (the original Mac programming font – which doesn't exist as an 8×16 bitmap, but the TrueType version is hinted well enough to work nicely at 14pt) and GNU Unifont (the Debian/Ubuntu Linux console font), for both of which I drew my own boldened versions because the original 1 pixel strokes made them too hard to read on the Playdate screen, the XNU (Darwin kernel) console font, and the Atari ST system font. I will decide on one of these at some point.

Serial Input and Output

For text input and output, it would be ideal to connect standard input and output of Python directly to the USB serial connection, the same way it works on any hardware MicroPython or CircuitPython board. This way, you could connect any serial terminal application to it to work directly on the REPL, and also use the mpremote tool for its advanced functionality such as remote-mounting a folder from your computer into the MicroPython file system so you can edit and run code without having to copy it to the device over and over.

Unfortunately, that does not work for the input direction (computer to Playdate), because Playdate games do not get full control over the serial port. The OS constantly provides a command interface on it for various remote control purposes used by the simulator and Mirror, and only one command (msg) on it reaches the application. Also, messages transmitted that way cannot contain arbitrary binary content as I would need, but only a selection of printable ASCII characters. So I had to invent my own protocol for a binary stream on top of that: Messages that do not start with ! (a character rarely occurring at the beginning of a Python line) are taken as-is, followed by CRLF. That allows conveniently entering simple one-liners from any terminal program or the simulator console, but not line editing or anything else that needs partial lines or bytes outside of the allowed set. Messages that start with ! contain arbitrary binary content encoded in Base64. That allows transmitting anything, at the cost of requiring some adapter software to do the encoding. A rudimentary Python script that does that is included in the source code. I hope to eventually extend it to create a virtual serial port (pseudo-TTY) that other programs like mpremote can connect to.

For the output direction (Playdate to computer), it should in theory work to transmit arbitrary content from a Playdate application, but currently does not due to two deficiencies in the OS/SDK. If these are eventually fixed, I will be able to use the serial connection directly for this direction, if not, I will have to stack my own protocol on top as well, as for the other direction. For now, text output does appear on the serial connection, but somewhat garbled, but you can work by looking at the Playdate screen instead of the serial output.

Long-Running Code with Coroutines

There was one challenge with the potential to doom the entire project to failure that I knew about from the beginning: PewPew games are long-running, they expect to run their own main loop and only exit once the game is over. That translates through the MicroPython virtual machine: running a chunk of Python is one C call that only returns when the chunk is done, there is no way to say “run the next 100 Python instructions and then return”. This is at odds with the way the Playdate SDK works, where the framework controls the main loop and calls into user code to handle events, but expects these calls to return quickly. If you don’t return within 10 seconds, the OS considers your game crashed and kills it.

This problem already existed in the Lua prototype – the Snake game was translated from Python to Lua literally and still contained that infinite loop. I was able to solve it thanks to Lua’s built-in support for coroutines, which allows either side to not have to return to the other, but yield to the other from deep in a call stack and resume where it left off the next time. But MicroPython is written in C and C has no coroutines. Or does it? It turns out that with a little bit of low-level trickery, it is perfectly possible to implement coroutines in C. Even better, someone has already done it specifically for Playdate! I had to apply some tweaks to that library to make it work in the Mac simulator (it was written on Linux, and probably still doesn’t work on Windows), but with that out of the way, it worked beautifully both on the device and on the simulator. Thanks to that library, I can now run long-running Python code while still having the Playdate UI responsive and the OS watchdog not killing me. That makes me happy, one major risk for the project out of the way.

I haven’t checked yet whether I can make the MicroPython VM periodically yield on its own (I think that should be possible), so that no involvement from the Python code is needed, but even if that doesn’t work, that should be no problem, because PewPew games regularly call into the pew library that is under my control for input and display handling and I can do the yielding there.

Next Steps

Right now I am working on fixing some shortcuts I took with connecting the REPL to serial input and making that work more like the way it works on a hardware MicroPython board. After that, there is a lot of glue code to be written between MicroPython and Playdate OS, in particular for timing functions (sleep()) and for filesystem access.

Follow Along

The code is available here:

5 Likes

Really impressive work. And I appreciate that you sent in a PR to fix up the coroutine library for the mac simulator. :heart:

1 Like

I keep finding more nice terminal fonts! Here is UW ttyp0:
font5

Timing functions are implemented and the automatic yielding works nicely, so it should now be impossible for Python code to stall the Playdate run loop. Work is underway on integrating the filesystem.

1 Like

Filesystem integration seems to be working, on both simulator and device. Demo video here:

What’s next? I guess I’ll start porting over the PewPew display from the Lua look-and-feel prototype.

1 Like

The PewPew display is integrated and is the main screen now, the terminal is now reached via the menu.

A low-level Python function _pew.show() to write to it is implemented in C. The pew.show() API will later be implemented in Python on top of that.

On two occasions I was surprised to discover how much more convenient the Playdate Lua API is than the C API:

  • The drawRoundRect/fillRoundRect functions I was using for the display frame background are not available in the C API. I’m not sure why that is (Round Rects are Everywhere, after all), I would assume they are implemented in C. I started cobbling together my own roundrects from arcs and rectangles, but then settled on loading the background from a bitmap file instead. That might even be faster than drawing it procedurally (I didn’t check). It uses more memory, but we have the memory since games are written for hardware PewPews with less memory than the Playdate.
  • The code to save and restore the display inversion preference in a JSON file turned out a lot more involved than the playdate.datastore one-liners in Lua. (But it stands to reason that that API is not available in C because it intrinsically deals with Lua tables.)