Early last year I poked a little at the PICO-8 fantasy console and had a little fun with the core of the graphics system. Later on I made use of its features in a more traditional way to implement my shooting gallery Rosetta stone. In doing all of these, I ended up ignoring or neglecting the vast majority of its intended features. I’ve had less time to dedicate to blog projects as the holidays approach, but I did do a port of my other major Rosetta Stone program to PICO-8:
Getting this working right involved actually making use of every editor mode, and pushed me int…
Early last year I poked a little at the PICO-8 fantasy console and had a little fun with the core of the graphics system. Later on I made use of its features in a more traditional way to implement my shooting gallery Rosetta stone. In doing all of these, I ended up ignoring or neglecting the vast majority of its intended features. I’ve had less time to dedicate to blog projects as the holidays approach, but I did do a port of my other major Rosetta Stone program to PICO-8:
Getting this working right involved actually making use of every editor mode, and pushed me into many other dusty corners of the system. I would still say that in the end the implementation of this was pretty straightforward—Lights-Out is fundamentally not a complicated game—but those dusty corners were pretty interesting.
Rendering the Display
I’ve been using my NES port of Lights-Out as my gold standard for retro-style ports of the puzzle. I expect to be able to match it pretty closely on PICO-8; while the resolution is lower and I have fewer colors overall to work with, I can combine them more freely and everything still fits neatly within its restrictions.
However, I do have the problem of recreating some of the more complicated graphics within the system. Its command-line interface actually does allow you to import graphics into its sprite ROM as PNGs, fixing the palette as it goes… but it turns out I had an even simpler option. When I originally created the NES graphics I had done so by converting the tiles out of a wacky textual format with some custom Python scripts. PICO-8’s file format stores most of its resource data as uncompressed hexadecimal strings, which given its 16-color palette results in a textual format very much like my original jury-rigged system! A couple of global search-and-replace options and I could paste the logo data directly into LIGHTS.P8 under the its graphics section:
__gfx__
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000777770000777700777707007770077770777777700077707000000077770007777077707777777707700000000000000000000000
00000000000000000000000077000000077007700077000700007700700700700770077000000770077000777000707007700707700000000000000000000000
00000000000000000000000770000000070077000070000700007000000700000770007000007700007700070000700000700000770000000000000000000000
00000000000000000000000770000000770077000000000700007000000700000777000000007700000770070000770000770000770000000000000000000000
00000000000000000000007700000000700770000000007777777000000700000077700000007700000770077000070000770000007000000000000000000000
00000000000000000000007700000000700770000000007000007000007700000007770000007700000770077000070000070000007000000000000000000000
00000000000000000000077000070007700770007770077000007000007700000000777000000770000077007000070000077000000700000000000000000000
00000000000000000000077000770007000770000700077000077000007700000700077000000770000077007000077000007000000000000000000000000000
00000000000000000000770007700077000770007700077000077000007700000770077000000077000770007700077000007700000770000000000000000000
00000000000000000007777777700777700077777000777700777700077770000707770000000007777700000777770000077770000770000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
07070707777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777707070707
70707070777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777770707070
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
If you squint at that you should be able to see the logo hiding in there.
The rest of the graphics were simple enough that it was faster to just redraw it within the sprite editor, but that then led to my second problem: the game pointer is surrounded by an opaque black border, and black, as color 0 in our palette, is transparent on both the NES and in PICO-8. The NES solution was to assign a different color in the sprite palette to also be black, and that one would be opaque and all would be well. PICO-8 does not offer us this convenience directly, but we do still have options.
We actually faced this problem before: one of my first PICO-8 programs was an implementation o the Cyclic Cellular Automaton, and it needed to draw opaque black pixels as well. The command PALT(0,FALSE) would disable transparency and allow everything to appear on the screen exactly as commanded. We can rely on that here as well, but our pointer graphics still need a transparent component because we’re drawing it over the rest of the display! Fortunately, the PALT() function is very flexible; it can make any color in the palette count as transparent or opaque, so all we need to do here is pick a color we aren’t using anywhere in the display (such as, say, the dark blue that is color 1) and make it be our transparent color with PALT(1,TRUE). With that in place I can create the rest of the necessary graphics:
The NES version relied on tile-local palette shifting to move buttons between black and red, but with PICO-8’s multicolor bitmap display it’s much easier to just keep separate versions of Black and Red cells in each animation state.
Now we have to actually build the display out of it. We got good use out of the SPR() function in the shooting gallery, and it’s actually even more powerful than our use of it; it allowed us to copy tile-aligned rectangles out of the sprite graphics area to anywhere on the screen. That means that we can place down the title with a single call, as well as any individual puzzle cell.
However, we can do better still. PICO-8 also includes a map system that will pre-arrange tiles into patterns that can then be copied into the screen with a single call.
We can copy this over the entire screen with a single call to the MAP() function, and that leads us to a high-level plan for how we’ll render the screen each frame:
- Draw the basic display over everything with
MAP(). - Draw each cell that the map gets wrong with
SPR()in the appropriate place. - Draw the pointer on the appropriate spot on the board.
- Draw the status messages above and below the board.
For reasons we’ll get to below, the board state is stored in a 2D array named BOARD, with each element being 1 if on and 0 if off. We can start drafting our drawing function with just this:
FUNCTION _DRAW()
-- PREPARE SCREEN
CLS()
PALT(0,FALSE)
PALT(1,TRUE)
-- DRAW BOARD
MAP(0,0,0,0,16,16)
FOR Y=1,5 DO
FOR X=1,5 DO
IF BOARD[Y][X] != 0 THEN
-- THIS CELL IS RED
SPR(34,X*16+8,Y*16+16,2,2)
END
END
END
We’ll skip over the pointer for now, since its behavior is tied up with the game logic more tightly, and move on to the status line. This, however, gets a bit silly. I’m storing the status line messages as STATUS1 and STATUS2, but I don’t get to have those simply be strings. I’d like to automatically center these messages, but it turns out that characters in the PICO-8 font are variable width. The expression #STR tells us how many characters are in STR, but while most characters are 4 pixels wide, some are zero and some are 8! I chose to deal with this by making status information be an array. The value is NIL if it should be blank; otherwise it’s a two-element array where the first element is the string to print and the second is an integer that adjusts the effective length of the array. That means our drawing function will need to end with these two lines:
IF (STATUS1) PRINT(STATUS1[1],64-2*(#STATUS1[1]+STATUS1[2]),19)
IF (STATUS2) PRINT(STATUS2[1],64-2*(#STATUS2[1]+STATUS2[2]),120)
END
Our task isn’t completely done here—we still need to draw the cursor and animate the buttons being pressed—but this is as far as we can get without implementing more of the game logic, and it already looks pretty nice.
Managing the Game Logic
My modern implementations of Lights-Out have generally represented the board as a 32-bit integer, with the lower 25 bits each representing one of the cells in the puzzle. This makes it easy to make new moves (just XOR a cell-specific value into the board state), and even easier to see if a move wins the game (this will turn the board state value to zero). I figured I’d be in good shape doing that here, because Lua 5.1 had all numbers be IEEE 754 double-precision floating-point numbers and as I learned when porting a PRNG over into JavaScript, we can fit a 32-bit integer into a double with perfect precision at all times.
Alas for me, PICO-8 isn’t standard Lua, and its numbers are fixed-point values where the integer part can only hold a value within the signed 16-bit range of -32768 through 32767. Oh well. It would have worked on TIC-80. I’ll just settle for a 5×5 array of numbers and use 0 for black and 1 for red cells. That does mean that our most basic board operations are a bit more involved, though they’re not exactly difficult, either:
FUNCTION NEW_PUZZLE()
FOR Y=1,5 DO
FOR X=1,5 DO
IF (RND()<.5) MAKE_MOVE(X,Y)
END
END
END
FUNCTION WON_GAME()
FOR Y=1,5 DO
FOR X=1,5 DO
IF (BOARD[Y][X]==1) RETURN FALSE
END
END
RETURN TRUE
END
FUNCTION MAKE_MOVE(X,Y)
BOARD[Y][X]=1-BOARD[Y][X]
IF (X>1) BOARD[Y][X-1]=1-BOARD[Y][X-1]
IF (X<5) BOARD[Y][X+1]=1-BOARD[Y][X+1]
IF (Y>1) BOARD[Y-1][X]=1-BOARD[Y-1][X]
IF (Y<5) BOARD[Y+1][X]=1-BOARD[Y+1][X]
END
For the main game logic I also decided to diverge pretty heavily from my NES design. When I finished up that project, my retrospective was not really happy with the way input ultimately felt in that system, and it put some of the blame on the unusual program structure I was experimenting with at the time. I had set up a sort of “graphics driver” to run every frame on the VBLANK interrupt but kept as much control logic as possible organized as if we were a BASIC program that was dedicated to whatever single task was immediately at hand. This really didn’t work very well, and I concluded at the end:
Redesigning Lights-Out NES for more consistent input response would probably revolve around refactoring it into a single global frame loop that provided a single point of truth for managing things like reacting to the START button or handling cursor motion.
PICO-8 kind of twists our arm on this one. We are strongly encouraged to organize our program into a trio of init/update/draw callbacks, and that obliges us to have that single global frame loop. However, we have multiple approaches to how input should work depending on how the game is configured:
- We could have just started, or just won. The next button press should create a new puzzle.
- We could be in the middle of creating a new puzzle. All input should be ignored.
- We could be in the middle of animating a single move, showing the pointer and the button pushing down. We should also ignore movement during this time.
- We could be in normal gameplay mode and actually respect our controls.
The randomization and animation aspects are multi-frame affairs, and that’s a problem I faced when organizing screen refreshes for the SNES port of the Cyclic Cellular Automaton. The solution I used there was to have a state variable whose value tells me what I should be doing each update. I can do that here, too, with an extra trick: by having multiple states do the same thing, I can bake timing information into the overall game flow. In the end I define 40 states:
- State 0 is where we start. If the move button is pressed, proceed to state 1.
- States 1-31 are the initial scramble. If we’re in any of these states, randomize the board and increment the state.
- State 32 is the normal gameplay state. Handle normal input here and if the player pushes the fire button, make the move and transition into state 39.
- States 33-39 animate the move. This is basically alternate graphics for the pointer and the cell the pointer is pointed at. Input is ignored and the state is decremented each frame until we hit state 33. There, if the puzzle is solved we play a happy tune and transition to state 0; otherwise we proceed back down to state 32 for continued play.
Honesty forces me to note that this design is kind of a bad idea; it bakes the timing into the game logic in such a way that if I want to change animation timings I have to rewrite the core game loop! It would be less of an issue in a language like C where enumerated types get compile-time-resolved values, but a more robust version of this would definitely be keeping two state variables; one like the above but with one state each for scramble and move animation, and then a (possibly shared) timer variable. Rust’s enumerated types let you pack those in to its own enums so this is in fact one of those happy places where the design of popular languages starts encouraging better designs in the software.
The next task is input handling, and that also ends up being quite a bit simpler here than on the NES. I had considerable trouble with getting the cursor to move smoothly around and in the end I decided it probably wasn’t worth the effort and I should have just jumped from cell to cell immediately, with some kind of keyrepeat mechanic to make it more convenient. PICO-8 handles this directly: the BTNP() function checks for fresh presses of a button and does its own keyrepeat as well, distinct from BTN() which simply checks the current state.
One very funny thing about both of these functions is that you can use the input-cue icons in the main text to identify the direction you’re checking:
This gets even sillier when you look at the raw source code. PICO-8 code is obligatorily monocase, and these graphics characters are generated by shifting letters. I’d have expected them to show up in the source code as the upper-case letters LRUDOX, but no, it actually stores them as Unicode!
At this point I now have enough logic done to sensibly go back to write code to draw the cursor. If the state is over 32, we also redraw the current cell with the pressed-down version of its graphic and move the pointer a pixel down and to the left.
Sound and Music
I’d previously never really gotten a handle on the sound and music systems in PICO-8. I’d seen some stuff that looked a bit like old tracker interfaces under the music tab, but I struggled to make it all work sensibly and the Sound Effects screen was just completely opaque:
These editors aren’t terribly discoverable, it turns out, so—shock and surprise—reading the manual turns out to be pretty helpful here. Even that tends to be a bit abstract though, so I didn’t get to where I needed to be until I started working through manual alongside poking at sample song that shipped with their Hello World demo got me to where I needed to be for this. Thanks to previous ports, I knew roughly what I wanted the sound to be like from the start, so here’s how I got PICO-8 to do those things.
The first step was to click the icon in the upper left to switch from piano-roll mode into tracker mode:
At this point your keyboard becomes two rows of piano keys in a manner very similar to what I was used to from Schism Tracker and, long before that, Scream Tracker 3. The space bar plays the sound effect, and various numbers can be altered with left and right clicks. Each note has five entries: note, octave, waveform, volume, and effect. Two of my sound effects are simple beeps, which I can manage with the fade-out effect:
This is sufficient for two of the three sound effects I need: a single fading beep at Middle C and the C below it. The final sound I need is the fanfare for winning, which is a chord of the E and C above middle C. Despite the four columns, though, sound-effect elements are single element. The solution here is to put each voice into its own channel:
I faded the volume by hand for this one so that it would fade more slowly than my normal beeps. It may be that I don’t understand how to make the fadeout effect work the way I want, but this did the job. I still need to make sure that the sounds are played properly in sync with one another, though, and that’s where the music tab comes in.
If the sound effect editor looks like editing a pattern in an old tracker program, the music editor is where we assemble these channels in order to produce full songs. In our case we just set up the two channels to be the halves of the chord and then select the STOP icon in the upper right to indicate that this ends the song.
With these all in hand, adding them to the program is very easy: SFX(1) and SFX(2) play my two beeps, while MUSIC(0) starts music playback from point 0, which is the victory fanfare.
There’s quite a bit more power lurking in PICO-8’s sound system—from what I understand from the manual, it’s possible to use sound effects as instruments in their own right in other sound effects, and there’s limited support for custom waveforms—but this project doesn’t need to delve that deep.
Mid-Game Reset
I had one last problem to solve before I was happy with this, and it had more to do with game flow than with any of the features of the system. PICO-8 is has a fundamentally controller-based input system, with a simple D-Pad and two buttons named X and O. There is also a pause button of sorts, but that pause button calls up a system menu that lets you do things like reset the program or interact with file browsers and such.
However, it turns out that this menu can be edited! (Thanks to spratt’s fiendish puzzle game Trichromat for using this feature and thus bringing it to my attention.) The MENUITEM function lets one add up to five custom choices to this menu, and they may react to the left and right directional keys or a selection. These can be filtered with extra flag bits, though the documentation was a bit vague about exactly how this works. For me, though, this gave me a sensible place to put the “New Puzzle” operation: as a choice on the system-level pause menu. Better yet, it could be done by a single line of code in _INIT():
MENUITEM(1|0X300,"NEW PUZZLE",FUNCTION() STATE=1 END)
With that in place, the pause menu gives us what we need…
… and thanks to our single state variable that manages all game flow transitions, we can leap into the “create a new puzzle” state with a single assignment. Not bad at all!
Wrapping Up
The completed port is available in source form on the Bumbershoot GitHub. I expected this port to be fairly trivial, much like the Shooting Gallery program was, but I was pleasantly surprised at how much I learned from it even as a fourth project. I still have a few half-baked ideas for how I could get Simulated Evolution to work properly on the PICO-8, but I think I’ll be letting that go back to the back burner for awhile again. Maybe next year.
Despite just dabbling, I do still quite like PICO-8 as a platform. I think both it and TIC-80 are excellent places to work if one wishes to do retro-style development but hesitates to go fully into assembly language and period systems.