Guide
Currently (in Picotron 0.1.0e) it's hard to use a PNG image as a sprite. You can fetch("myimage.png"), but the result isn't in the right image format. So, here's a small tool to convert PNG files into picotron sprites.
Drag any .png or .qoi image into the tool to convert it into a sprite pod on your clipboard. You can paste this into the graphics editor, or into code.
Drag in a .hex file (e.g. from lospec.com) or a .pal file (e.g. from OkPal) before importing your png to change the import palette.
Details
Import speed
This tool prioritizes import speed. Images with only a few unique colors (e.g. an image already in the picotron palette) will import much faster than images with many colors.

p8x8: convert PICO-8 carts into Picotron carts (some assembly required)
I'm declaring p8x8 good enough for public release! It's a tool to convert pico8 carts to picotron -- it's not perfect and it requires some manual intervention in most cases, but it's magical being able to play a bunch of games on the new system without much effort.
Lots more info (instructions, compatibility notes, CC license, etc) here: https://github.com/pancelor/p8x8/
Teaser video here: https://mastodon.social/@pancelor/112162470395945383
changelog
v1.8 (#p8x8-8, unreleased)
- music/sfx conversion!! just waiting on picotron 010h to add instrument effects
v1.7 (#p8x8-7)
- fix secret palette
Here's my /appdata/system/startup.lua file (picotron automatically runs it on startup)
-- take str, delete all chars between
-- indices i0 and i1 (inclusive), and
-- insert newstr into that space
local function splice(str,i0,i1, newstr)
return sub(str,1,max(1,i0)-1)..(newstr or "")..sub(str,i1+1)
end
-- use str:find to do sed-like file editing.
-- no lua patterns, just literal string matching
-- return str, but replace the first instance of cut with paste
local function _replace(str,cut,paste)
local i0,i1 = str:find(cut,1,true) -- no pattern-matching
if not i0 then
return str,false
end
return splice(str,i0,i1,paste),true
end
local function sedish(fname,mods)
local src = fetch(fname)
if not src then
printh("sedish: couldn't find file "..fname)
return
end
for i,mod in ipairs(mods) do
local cut,paste = unpack(mod)
-- printh(cut.." "..paste)
if not cut or not paste then
printh("sedish: bad cut/paste data in "..fname)
return
end
local changed
src,changed = _replace(src,cut,paste)
if not changed then
printh("sedish: mod #"..i.." did nothing to "..fname)
end
if not src or #src==0 then
printh("sedish: bad result in "..fname)
return
end
end
-- printh("storing "..fname..": "..sub(src,1,100):gsub("
[ [size=16][color=#ffaabb] [ Continue Reading.. ] [/color][/size] ](/bbs/?pid=143638#p) |

update: there are basically 3 tools that are relevant here:
- this tool (sprimp) -- maybe still useful, but superceded by p8x8:
- p8x8: a tool to fully convert PICO-8 carts to Picotron carts (some assembly required) -- this grew out of sprimp
- my aseprite exporter plugin -- still useful
You probably want to check out p8x8 instead, but I'm leaving this thread as it is since it may still be useful or interesting to some people.
tada! now you can import pico8 spritesheets. and map too!
importing a .p8 file
- put your game.p8 file somewhere inside picotron's file system
just a port of a tweetcart of mine to the new system :)
move this into /system/screensavers/ and it'll show up as an option in your settings
If you run it on its own as a cart, you'll want to run reset() vid(0) in the console afterwards to get things back to normal
edit: if you want it to be permanently available, you need to put it in /appdata/system/screensavers (otherwise, you need to re-add it every time you start picotron). create the folder by copying the system folder: cp /system/screensavers /appdata/system/screensavers
Oh no, ghosts are approaching the town!
Mayor Wombledon has begged ALFREDO THE GHOUL BANISHER to team up with THE WILY WIZ to protect the town. Can they put aside their differences and work together, or will evil spirits devour the populace?
Controls
- Arrow keys: move
- Z: swap heroes
- Enter/P: pause (level select, volume controls)
- X: next level
Outcomes
- 1 night protected: You have the villagers' sincere gratitude 🙏
- 5 nights protected: The villagers are beginning to hope again 😭
- 10 nights protected: Valiant Heroes 🏆
- 40 nights protected: Local Deity 🤯
Tips
- Alfredo can dig up graves with his fearsome claws.
Hi @zep, found a parser bug for ya:
for --[[a]]e=0,1 do print(e) end print(fore) |
expected: 0 1 [nil] (this is what lua 5.4 outputs)
observed: [nil] 0
pico-8's highlighting works correctly, but the runtime seems to see this somehow:
fore=0,1 do print(e) end |
system: linux / pico8 0.2.5g
I ran into this while using shrinko8's annotations (for --[[preserve]]e=0,1 do)
Workaround: add an extra space (for --[[preserve]] e=0,1 do)
edit: ah! this thread has more cases / info: https://www.lexaloffle.com/bbs/?tid=51618
@zep if you open this cart in pico8 and save it, pico8 inserts an extra byte at the end (0xff) (this is wrong, but additionally it is very confusing because my text editor thinks the text encoding has changed and starts displaying weird unicode everywhere)
pico-8 cartridge // http://www.pico-8.com version 41 __lua__ ?'hi' __meta:title__ cooltitle |
After some minimal testing, the conditions necessary seem to be:
- the file ends with a
__meta__section (__gfx__doesn't trigger the bug) - the file does not have a trailing newline (the last byte of this particular file is 'e', not '\n')
platform: linux / pico8: 0.2.5g

Bitplanes are powerful, but they can be difficult to understand. How do you use them in PICO-8?
The short version: bitplanes let you draw colors to the screen without completely overwriting the existing colors, making it possible to do effects like shadows, transparency, etc. But be warned: they come with a lot of unintuitive restrictions, like monopolizing half your screen palette or requiring it to be set up in a particular way.
Longer version: PICO-8 colors are 4-bit numbers (0-15). The screen is a 128x128 4-bit image, but you could instead imagine it as 4 separate 128x128 1-bit images, overlaid on top of each other. By poking a particular location in memory, we can tell PICO-8 to draw to these "bit planes" separately. Normally, drawing overwrites any existing colors, but if we selectively disable some of the bitplanes, some bits of the old colors will remain onscreen.
Technical version: see "Technical details" below.
This post lists some specific examples and tricks that you can do with bitplanes. I won't attempt to fully explain how bitplanes work, but I'll leave some resources at the end.

PICO-8 supports bitplane drawing; the wiki (search "bitplane") has a description of how they work:
> 0x5f5e / 24414
> Allows PICO-8 to mask out certain bits of the input source color of drawing operations, and to write to specific bitplanes in the screen (there's 4 of them since PICO-8 uses a 4BPP display). Bits 0..3 indicate which bitplanes should be set to the new color value, while bits 4..7 indicate which input color bits to keep.
> For example, poke(0x5f5e, 0b00110111) will cause drawing operations to write to bitplanes 0, 1, and 2 only, with 0 and 1 receiving the color value bits, 2 being cleared, and 3 being unaltered.
> This formula is applied for every pixel written:
> dst_color = (dst_color & ~write_mask) | (src_color & write_mask & read_mask)
This is precise and correct, but I find it a bit hard to understand. So I made this cart to give myself an interactive sandbox where I can play around with bitplanes, to see how they affect drawing.

update 2024: the 0.2.6 update improves things! search the update post for menuitem(0x301 for details. (My original menuitem post remains below, unchanged)
PICO-8 has fancy menuitems but there are some gotchas and bugs to be aware of.
Here's an example of what I do by default; the rest of this post will explain how the code works and why I do things this way:
L/R pitfall
Imagine you want to add a "mute" button to your game's menu. Can you spot the issue with this code?
I often use shell scripts to export and then upload my games to itch.io, and there's a small inconvenience that trips me up sometimes: If my game is too large, the export fails, but I have no way of detecting that from my shell script.
> pico8 game.p8 -export "-f froggypaint.html" EXPORT: -f froggypaint.html failed: code block too large > echo $? 0 |
I would expect the status code (echo $?) to be 1 or some other failure code
(Hm, now that I've written this up I suppose I could work around this by reading stderr stdout and checking for the string "failed"...)

A slow but token-efficient sort:
-- 35-token bubblesort, by pancelor
function sort(arr)
for i=1,#arr do
for j=i,#arr do
if arr[j] < arr[i] then
add(arr,deli(arr,j),i) --slow swap
end
end
end
end |
I did a brief speed analysis and this function seems reasonable to use for arrays up to around 50 or 100 elements, depending on how much CPU you have available to burn.
speed analysis:
[hidden]
I did some minimal testing, using this code:
cls()
function _draw()
arr={
--20,5,8,3,7,4,1,9,2,-30,
--20,5,8,3,7,4,1,9,2,-30,
--20,5,8,3,7,4,1,9,2,-30,
--20,5,8,3,7,4,1,9,2,-30,
20,5,8,3,7,4,1,9,2,-30,
}
sort(arr)
end |
By commenting out lines of the array, I changed it's length. Here's how much CPU the function spent, as a percentage of a single 30fps frame (measured with the ctrl-p monitor, on pico8 0.2.5g)
The "best case cpu" was calculated by defining arr outside of _draw, so that the array was already sorted after the first frame
| Array length | Typical cpu (30fps) | Best case cpu (30fps) |
|---|---|---|
| 10 | 0% | 0% |
| 20 | 1% | 1% |
| 50 | 5% | 4% |
| 100 | 21% | 15% |
| 200 | 81% | 58% |
| 300 | 181% | 130% |
| 400 | 321% | 231% |
I believe this algorithm is O(n^3): O(n^2) for the two loops, and an additional O(n) b/c the swap step uses add and deli, which shift around the array elements. But the chart seems to indicate that doubling the length will quadruple the cpu cost (instead of octupling it) so maybe it's only O(n^2) somehow? It doesn't much matter, since you don't want to use this for large arrays anyway, but maybe add/deli are cheaper than I would expect.

mildly interesting: when drawing a perfectly vertical line (line(1,1,1,h)) or perfectly horizontal line (line(1,1,w,1)), it used to be cheaper (in cpu cycles) to use rect or rectfill instead. but not anymore! I'm testing this on 0.2.5g; I'm not sure when this changed.
tl;dr: line and rectfill cost the same for orthogonal lines (rect is more expensive for vertical lines only)
simple test:
full test:
[hidden]
function one(fn,opts)
local fmt=function(n)
-- leftpad a number to 2 columns
return n<9.9 and " "..n or n
end
local dat=prof_one(fn,opts)
printh(fmt(dat.lua)
.."+"
..fmt(dat.sys)
.."="
..fmt(dat.total)
.." (lua+sys)")
end
function hline(x) line(0,0,x,0,2) end
function hrect(x) rect(0,0,x,0,2) end
function hrfil(x) rectfill(0,0,x,0,2) end
function vline(x) line(0,0,0,x,2) end
function vrect(x) rect(0,0,0,x,2) end
function vrfil(x) rectfill(0,0,0,x,2) end
printh"--"
for i=0,127 do
printh(i..": ")
one(hline,{locals={i}})
one(hrect,{locals={i}})
one(hrfil,{locals={i}})
one(vline,{locals={i}})
one(vrect,{locals={i}})
one(vrfil,{locals={i}})
end
--[[
...
14:
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
15:
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
16:
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
9+ 4=13 (lua+sys)
9+ 2=11 (lua+sys)
17:
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
9+ 2=11 (lua+sys)
9+ 4=13 (lua+sys)
9+ 2=11 (lua+sys)
...
]] |
summary:
rect:
when h is 1, sys cost is:
max(1,w\16)*2 (agrees with CPU wiki page)
when w is 1, sys cost is:
max(1,(h-1)\8)*2 (disagrees? unsure)
line and rectfill
a,b = max(w,h),min(w,h)
when b is 1, sys cost is:
max(1,a\16)*2 (agrees with CPU page for rectfill, but not line(?)) |
welcome to the tower
welcome to the tower
welcome to the tower
welcome to the tower
controls
- arrow keys: walk, interact
- enter: pause menu (volume, toggle effects, etc)
the game will autosave your progress each floor
credits
made by tally (art, room design, dialog) and pancelor (code, game design, music, dialog)
pixel art created with tiles from teaceratops and Yelta

switching between custom and default fonts (?"\015") behaves a bit strangely; the first line of text is spaced differently from the rest, depending on whether:
- custom fonts are enabled by default (
poke(0x5f58,0x81)), and - the line height (
peek(0x5602)) is more or less than 6 (the size of the default font)
run this cart to see what I mean:
specifically:
- when the custom font is enabled by default and the custom font's height is more than 6, the default-font text in this cart has a large gap between the first and second lines:
- when the custom font is not enabled by default and the custom font's height is less than 6, the custom-font text in this cart has a large gap between the first and second lines:

how many tokens should this cart cost?
s="x".."=" a=1+2 |
in 0.2.1b, it costs 10 tokens (5 for each line). this seems correct to me. however, in 0.2.5e, the first line only costs 4 tokens for some reason.
edit: even weirder: s="x".."=" costs 4 tokens but s="x".."y" costs 5 tokens. it seems like concatenating any string that starts with the equals symbol is 1 token cheaper than it should be; how odd! maybe this is somehow due to the recent parser updates for += etc?

When I'm making games or tweetcarts, I often adjust numbers just a tiny bit, then rerun the whole game. e.g. I change the player speed by 0.1, then change it back, then try 0.05...
This is a bit slow, so here's a library I made to help me do it faster. I also find it useful for analyzing other people's tweetcarts -- if I can easily adjust the values they use, I can quickly figure out what they mean
setup
load #twiddler- copy the
knobs.lua+helperstabs into your game - use
kn[1],kn[2], ...kn[8]in place of any number - add
twiddler()to the end of your_drawfunction
Now, run your code:
- press tab and adjust the values (see "controls" below)
- press tab again -- the values will be copied to your clipboard
- paste the values into the start of your code to save them
example 1
Start with a tweetcart you want to study. For example, this one by 2DArray: https://twitter.com/2DArray/status/1492566780451205120
[ [size=16][color=#ffaabb] [ Continue Reading.. ] [/color][/size] ](/bbs/?pid=119729#p) |
A demake of the classic minesweeper.
The game cartridge is just 1024 bytes -- see https://gist.github.com/pancelor/a3aadc5e8cdf809cf0a4972ac9598433 for some lightly commented source code
RULES / CONTROLS:
- left click to reveal a tile
- if you hit a mine, you lose
- revealed tiles will show a number, telling how many of their 8 neighbors are mines
- right click to flag a tile
- reveal all non-mine tiles to win!
- click the smiley face to restart
TIPS
- left click + right click (simultaneous) to auto-reveal neighbors, if the number of nearby flags matches the number on the tile you clicked
- mines left and a timer are displayed in the top corners

When exporting a game to a binary format (.exe, etc), the manual says:
> To include an extra file in the output folders and archives, use the -E switch:
> > EXPORT -E README.TXT MYGAME.BIN
I tried this (pico8 game.p8 -export "-f game.bin -e examples/ -e samples/") but it doesn't include those subfolders. If I -e examples/kick.pcm, then that file is included, but it's included at the top level, and not in an "examples" subfolder
Am I doing this wrong somehow? I assume this just isn't supported (yet? fingers crossed)






16 comments