Deploying PICO-8 Cartridges with Electron

This article uses Linux. If you aren't running linux, there's a good chance you can still follow along if you have some command-line-fu.

There has been a lot of discussion about deploying PICO-8 cartridges to a native desktop application. Don't get me wrong, I think it's awesome that folks distribute the *.p8.png file format, but as most game developers know, most gamers honestly just want a double click format. I've done this with Vi8 using PICOLOVE and a bunch of hacks. It's dirty, painful and no one really wants to do that anyway.

I have found a way to do this using Electron.

Note to Reader: I'd like to think this article is a joke for the most part. I mean, we're taking about ~1.3MB of data, and we're making a deployable that's ~40MB. Reader beware, but I think it's reasonable to document this process anyway.

For this article, I will be exporting The Career of Peter to desktop!

1. Get your cartridge ready for electron

So, there's going to be some steps that need to be done, before we can even start.

Load and save please

Here we load our cartridge as we normally do with load foo.p8 and then we use PICO-8's export foo.html feature to make a website version of the game.

Here's the files we now have:

seppi@seppi7:~/TheCareerOfPeter♠ ls -lah
total 1.4M
drwxr-xr-x   2 seppi seppi 4.0K Oct 21 20:04 .
drwx------ 134 seppi seppi  20K Oct 21 20:00 ..
-rw-r--r--   1 seppi seppi 4.5K Oct 21 20:04 thecareerofpeter-v18.html
-rw-r--r--   1 seppi seppi 1.3M Oct 21 20:04 thecareerofpeter-v18.js
-rw-r--r--   1 seppi seppi  72K Oct 21 20:01 thecareerofpeter-v18.p8

Now we need to "clean up" the cartridge for deployment.

I. Update the title of the cart

         <meta charset="utf-8">
         <meta http-equiv="X-UA-Compatible" content="IE=edge">
-        <title>PICO-8 Cartridge</title>
+        <title>The Career of Peter</title>
         <meta name="description" content="">

 <STYLE TYPE="text/css">

II. Get rid of the body padding.

 canvas#canvas { width: 512px; height: 512px; }

+body {
+  padding: 0px;
+  margin: 0px;

III. Get rid of the .pico8_el stuff.

 .pico8_el {
    font-size: 9pt;
    cursor: pointer;
    cursor: hand;
+   display: none;

IV. Destroy those <br> tags with extreme prejudice.

 <body bgcolor=#303030>

-   <br><br><br>

    <center><div style="width:512px;">


-   <br>

    <div class=pico8_el onclick="Module.pico8Reset();">


-   <br>    

-   <br><br>


Awesome! Now the PICO-8 export should show up nicely in electron!

2. Set up Electron, Prebuilt and Packager

I then installed globally npm and updated it with:

sudo npm install npm@latest -g

I then install electron globally:

sudo npm install electron -g

Finally in the working directory, I run:

npm install electron-prebuilt and npm install electron-packager

This provides a back-end to getting binaries for every platform and the packaging system that will build the binary. You will now notice a big honking node_modules/ folder. Fun.

Now that we have that, we need to set up the required package.json for electron. Just put it in your working directory. Here's what I have (it seems to work ...):

    "name": "the-career-of-peter",
    "version": "1.18.0",
    "description": "The Career of Peter by Missing Sentinel Software",
    "main": "main.js",
        "start":"electron .",
        "build":"electron-packager . TheCareerOfPeter --all"
    "author":"Josef Patoprsty <josefnpat@gmail.com> (http://josefnpat.com)",
    "license":"Josef Patoprsty",
    "devDependencies": {

3. Set up main.js

So, now we need to inform electron how it's supposed to work. I modified an example main.js I found, and this one works for me.

const {app, BrowserWindow} = require('electron')

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win

function createWindow () {
  // Create the browser window.
  win = new BrowserWindow({
        width: 512,
        height: 512,
        title: "The Career of Peter",
        resizable: false,
        maximizable: false,
        fullscreenable: false

  // and load the index.html of the app.

  // Open the DevTools.

  // Emitted when the window is closed.
  win.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    win = null

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {

app.on('activate', () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (win === null) {

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

With this in place you can now test your application before building it with electron .

4. Build your application

Now we use our nifty build script defined in package.json:

seppi@seppi7:~/TheCareerOfPeter♠ npm run build

> the-career-of-peter@1.18.0 build /home/seppi/TheCareerOfPeter
> electron-packager . TheCareerOfPeter --all

Packaging app for platform linux ia32 using electron v1.3.8
Packaging app for platform win32 ia32 using electron v1.3.8
Packaging app for platform darwin x64 using electron v1.3.8
Packaging app for platform linux x64 using electron v1.3.8
Packaging app for platform mas x64 using electron v1.3.8
WARNING: signing is required for mas builds. Provide the osx-sign option, or manually sign the app later.
Packaging app for platform win32 x64 using electron v1.3.8
Packaging app for platform linux armv7l using electron v1.3.8
Wrote new apps to:

With that we have tons of builds! You could run one of them like this:


The Career of Peter running in Electron!

And just for "science", let's check out how big these files are:

seppi@seppi7:~/TheCareerOfPeter♠ ls -lah
total 442M
drwxr-xr-x  10 seppi seppi 4.0K Oct 21 20:37 .
drwx------ 134 seppi seppi  20K Oct 21 20:36 ..
-rw-r--r--   1 seppi seppi 1.7K Oct 21 20:31 main.js
drwxr-xr-x 187 seppi seppi 4.0K Oct 21 20:15 node_modules
-rw-r--r--   1 seppi seppi  482 Oct 21 20:07 package.json
drwxr-xr-x   3 seppi seppi 4.0K Oct 21 20:34 TheCareerOfPeter-darwin-x64
-rw-r--r--   1 seppi seppi 116M Oct 21 20:36 TheCareerOfPeter-darwin-x64.zip
drwxr-xr-x   4 seppi seppi 4.0K Oct 21 20:34 TheCareerOfPeter-linux-armv7l
-rw-r--r--   1 seppi seppi  36M Oct 21 20:36 TheCareerOfPeter-linux-armv7l.zip
drwxr-xr-x   4 seppi seppi 4.0K Oct 21 20:34 TheCareerOfPeter-linux-ia32
-rw-r--r--   1 seppi seppi  43M Oct 21 20:36 TheCareerOfPeter-linux-ia32.zip
drwxr-xr-x   4 seppi seppi 4.0K Oct 21 20:34 TheCareerOfPeter-linux-x64
-rw-r--r--   1 seppi seppi  41M Oct 21 20:36 TheCareerOfPeter-linux-x64.zip
drwxr-xr-x   3 seppi seppi 4.0K Oct 21 20:34 TheCareerOfPeter-mas-x64
-rw-r--r--   1 seppi seppi 114M Oct 21 20:36 TheCareerOfPeter-mas-x64.zip
-rw-r--r--   1 seppi seppi 4.5K Oct 21 20:32 thecareerofpeter-v18.html
-rw-r--r--   1 seppi seppi 1.3M Oct 21 20:04 thecareerofpeter-v18.js
-rw-r--r--   1 seppi seppi  72K Oct 21 20:01 thecareerofpeter-v18.p8
drwxr-xr-x   4 seppi seppi 4.0K Oct 21 20:34 TheCareerOfPeter-win32-ia32
-rw-r--r--   1 seppi seppi  42M Oct 21 20:36 TheCareerOfPeter-win32-ia32.zip
drwxr-xr-x   4 seppi seppi 4.0K Oct 21 20:34 TheCareerOfPeter-win32-x64
-rw-r--r--   1 seppi seppi  52M Oct 21 20:37 TheCareerOfPeter-win32-x64.zip

To check out the final result for yourself, mosey on over to The Career of Peter

OUYA: Pico-8's Fantasy Console

I recently discovered this small, yet interesting game engine, called Pico-8. The concept is simple; it's a fantasy console that pays homage to an older era of video game development, taking cue from classic systems like the Commodore 64, BBC Micro, ZX Spectrum and more.

Yet as of now it only runs in browsers and as desktop applications!

The Pico-8 Console

So why isn't there a console for Pico8? Source: Art by @bitmOO

More about Pico8

Pico8 enforces limitations that can create hostile reactions, such as:

  • I only have 128x128 spritesheet to work with! What?
  • How in gods name is this Lua? It's missing some of the most important functions!
  • Where did my floating point precision go?
  • Why do I only have sixteen colors? I'm running a 24-bit color monitor!
  • How do I play music and sound effects at the same time?

Overcoming these barriers does seem a bit odd, considering the smartphones we have in our pockets are multitudes faster than the computers used in the older apollo space missions, but it does provide for some great entertainment (or torture some might conclude.)

As an inquisitve programmer, I documented Pico-8's version of Lua and went into what one might consider way too much detail. Doing this did give me an understanding of what is and what isn't available to the developer.

To put this post into context of the OUYA, I personally contributed a lot of the documentation for the Love-android-sdl2 project by Martin Felis. A simple byproduct of writing much of the documentation is that now I understand how to deploy games written for the LÖVE Framework on android devices, one of them being the OUYA.

What's the Gameplan?

Putting one and one together (or perhaps one and one and one and one), I decided that I was going to port a small game I wrote for Pico8 to the OUYA using;

  • Pico8 to ensure that the codebase was Pico8 compatible.
  • PicoLove, a pico8 interpreter by fstf, so that I could package the game as a LÖVE game.
  • LÖVE 0.9.2 to ensure that the game was being interpreted correctly by PicoLove.
  • Love-Android-SDL2 to deploy the game to an APK that could be installed on an Android device.
  • An OUYA for a testing the game in a real environment.

So all this information boils down to the ultimate question;

Can I use the OUYA as the "hardware" component for the Pico8 engine?



The Pico8 Engine in action. Source: Lexaloffle

Developing this game wasn't all that painful. With a strong familiarity with Lua 5.2 and a good idea how to get around the pitfalls of the Pico8 Lua varient, I was able to create an interesting and colorful top down space shooter for two players.

I had some issues at first, considering that the Pico8 editor is abysmal (one can easily argue that this is on purpose.) My understanding was that while I didn't have a problem with the machine's limitations, there was no way I was going to give up my workflow just for the sake of nostalgia. Many "old school" game developers (and newer ones as well) use a much faster and more powerful machine in tandem with the actual piece of hardware they were deploying to.

I was able to figure out Pico8's *.p8 data format easily enough, and wrote some lower level unix-like utility scripts for working with them. Uncreatively I named them Pico8Utils.

I found myself modeling a lot of LÖVE's API, such as;

I wrote a function similar to love.graphics.printf, which allowed me to center text when drawing it:

function printf(string,x,y,width,alignment)
  if alignment == "center" then
  elseif alignment == "left" then
  elseif alignment == "right" then
    printh("invalid alignment")

Using objects to make the game stateful. For example;

states = {
  main_menu = { draw = function() end, update = function() end }
  game = { draw = function() end, update = function() end }

current_state = states.main_menu

function _update()

function _draw()

Writing helper functions to clarify user input controls.

_btnstr_data = {}
_btnstr_data.l = 0
_btnstr_data.r = 1
_btnstr_data.u = 2
_btnstr_data.d = 3
_btnstr_data.a = 4
_btnstr_data.b = 5

function btnstr(s,p)
  return btn(_btnstr_data[s],p or 0)

function btnpstr(s,p)
  return btnp(_btnstr_data[s],p or 0)

While there is a lot of talk on the Pico8 forums about the implied token and character limit, this game was not large enough for me to worry about size optimization.

PicoLove & LÖVE 0.9.2

It's LÖVE Time! No, dev, no!

Common misconceptions of the author. Source: A meme edited by the author

This step led me to believe that the remainder of the project would be easy. The only notable thing done for this stage was that I replaced the nogame.p8 file with the game itself. After making a few controller tweaks, I had a working .love package that would run against the LÖVE framework.



The Love-Android-SDL Logo Source: love-android-sdl2

Building and packaging the *.apk with the *.love file was not so hard, it was jut a matter of following along with my documentation. I was able to produce a package and loaded it on my device with adb install -r *.apk.

Then bad things started happening;

Consistent crashes when the OpenAL bindings were called. PicoLove uses LuaJIT's ffi bindings to access the OpenAL shared object to generate sounds. Unfortunately it couldn't find it on Android. A little time with arecord and a dirty hack later, everything was working:

__pico_sfx = {}
for i = 1,10 do
    __pico_sfx[i] = love.audio.newSource("sfx/"..i..".ogg")

function sfx(n)
  if __pico_sfx[n] then

Shaders were written in a way that they were not OpenGLES compatible. I had to start writing hacky workarounds to re-implement them so the palleting wouldn't be offloaded to the GPU. After writing a color table and setting the appropriate colors, I then disabled shaders in a very lazy way;

love.graphics.newShader = function()
  return {send = function() end}
love.graphics.setShader = function() end

This stuff was pretty hacky, I admit, but I had a vision in mind and was trying to keep the scope of the project down.

There is a lot of work to be done on PicoLove if it is going to run on Android with the entire Pico8 API. I will admit I didn't use much of the Pico8 API in my game, so porting the majority of what I did do was easy, but many functions have yet to be updated and tested on Android.

OUYA Specific configurations


This is an OUYA, in case you haven't seen one. Source: Wikipedia

PicoLove showed some interest in deploying to Android, but lacked some of the callbacks required for joystick controllers. For the OUYA, the most important part was joystick integration. Modeling love.mousepressed and love.mousereleased functions after the PicoLove API, I was able to create love.joystickpressed and love.joystickreleased variants.

It took a little fiddling and usage of adb logcat to figure out the joystick mapping, but I was able to get something that worked.

__joystick_table = {
    1,--[O]UYA (A LOCATION)
    2,--OUY[A] (B LOCATION),

function love.joystickpressed(joystick,button)
    local count
    for ccount,checkjoystick in pairs(love.joystick.getJoysticks()) do
        local js_id = joystick:getID()
        local cjs_id = checkjoystick:getID()
        if cjs_id == js_id then
            count = ccount-1
    if count then
        for imap,map in pairs(__joystick_table) do
            if map == button then
                log("pressed "..button.." for player "..count)
                __pico_keypressed[count][imap-1] = -1

function love.joystickreleased(joystick,button)
    local count
    for ccount,checkjoystick in pairs(love.joystick.getJoysticks()) do
        local js_id = joystick:getID()
        local cjs_id = checkjoystick:getID()
        if cjs_id == js_id then
            count = ccount-1
    if count then
        for imap,map in pairs(__joystick_table) do
            if map == button then
                log("released"..button.." for player "..count)
                __pico_keypressed[count][imap-1] = nil

Preparing the game for OUYA specifically wasn't all that complicated considering the majority of the implementation was joystick callbacks. I imagine that a touchscreen would have made for a more complicated interface.

Retrospect and the future

Someone mail me this

This could totally be a thing. Why not? Source: @bitmOO

There were some obvious issues in this gigantic process, but I feel like with a little more polish at every stage and there could be a real "marketplace" running Pico8 games on the OUYA or other Android microconsoles.

The big takeaway here for me is that it is possible and works well. This could give way to an "app" on the OUYA, GameStick or FireTV that could play any Pico8 compatible game. Perhaps when PicoLove matures to a stable point, one could coordinate with lexaloffle to create a parsable API that would allow this "app" to load content from the site dynamically. Perhaps a gamestation in a gamestation?

While there is much discussion about the OUYA being a contender in the microconsole market, perhaps the Pico8 community could embrace the limitations of the OUYA by supporting it with a fantasy console founded on technical limitations.


Game over, man. Source: itch.io

If you are interested in playing Vi8, the game mentioned in this article, it is free for the web/desktop version and only $1 for the OUYA release.

About Seppi

Howdy; my name's Seppi, and I'm an indie game developer. I am an active member of the LÖVE community. I've been making games for years now, and I'm always interested in helping prospective indie developers out.

One of the paradoxes I've learned over the years, especially as a software developer, is the more I know, the more I realize I don't know. I quote;

Only a fool would take anything posted here as fact.

Questions, comments or insults? Feel free to leave in the comments section, or contact me; you can hit me up on twitter @josefnpat

Subscribe to RSS - Pico8