hi! here is some text for you to read.
and here are some games for you to play:
Here's a snippet I made to launch webpages in the player's browser from within a pico8 cartridge. It only works in html exports, because it uses the GPIO pins to send the url to a custom html template.
Example (player perspective)
For an example of this in action, play https://pancelor.itch.io/make-ten-deluxe and click "info" on the title screen. A paper will pop up with some clickable URLs on them, which will load new tabs when clicked.
Setup
folder config
, open the "plates" subfolder, copy the default template (TODO: is there a default template, or did I make it myself by exporting a cart and then undoing pico8's##js_file##
/##label_file##
replacement?)- Find
var pico8_gpio = new Array(128);
(it's near the top, around line 30) - Replace it with this:
var pico8_gpio = new Proxy(new Array(128),{ // pancelor's gpio-url, https://www.lexaloffle.com/bbs/?tid=149864 [ [size=16][color=#ffaabb] [ Continue Reading.. ] [/color][/size] ](/bbs/?pid=169278#p) |
tl;dr: use left-mouse to play, this is a free demo, the full game is on itch. have fun!
Make Ten Deluxe is an expansion pack that builds on the original Make Ten, featuring 35+ variant modes. Some modes modify the rules slightly in ways that significantly change the flavor of the game, some modes are particularly puzzley situations for you to solve, and some modes are too strange and specific to describe.
There's something for everybody to love: each of the game's dozens of modes were carefully chosen to be interesting, surprising, or otherwise delightful.
An interactive quilt for TTJ10, made in <500 chars of code.
More toy than game; think of it as a jigsaw puzzle, if you like.
controls
Left click to pick up a patch, left click again to drop it.
Also hosted on itch.io
There are many ways to loop over an array-style table. For example:
local tab = {10,20,30,40} -- method 1 local calc1 = 0 for i,elem in ipairs(tab) do calc1 += i*elem end -- method 2 local calc2 = 0 for i=1,#tab do local elem = tab[i] calc2 += i*elem end |
Which way is fastest? Well it often doesn't matter, since the work inside the loop usually far outweighs the cost of the loop itself. Or tokens might matter more to you than speed. But in some situations you want your code to be as fast as possible, and that means minimizing the overhead from the loop itself.
Setup
So, which way of looping is fastest? Here are the methods we'll compare:
function for_i(tab) for i=1,#tab do local x=tab[i] -- do some work end end function for_all(tab) for x in all(tab) do -- do some work end end function for_ipairs(tab) for i,x in ipairs(tab) do -- do some work [ [size=16][color=#ffaabb] [ Continue Reading.. ] [/color][/size] ](/bbs/?pid=164729#p) |




repro steps:
- run cart bitplane_grid-3 in the web player
- press ctrl-c
- paste into an external text editor
expected behavior: clipboard should have poke(0x5f5e,0xff)
in it
actual behavior: clipboard is unchanged (my specs: linux, firefox, 0.2.6c dev8)
(cart from here)
Maybe this is a known bug, which is part of why it's "0.2.6cdev8" instead of "0.2.6c"? But here's a ping just in case you don't know about it @zep. (web clipboard stuff seems like a huge pain, I don't envy you trying to fix it...)
The cart has some debug print statements -- open the browser dev tools and you can see it print out "copying str:" right before calling printh(str,'@clip')
. I noticed there's no red "press c to copy" prompt that the web player used to show, maybe that's a clue.


Sometimes it'd be nice to mute the music in a game you downloaded from the BBS, but not mute the sound effects. Maybe their music isn't to your taste, maybe you've played it way too much and now you want to listen to your own music instead; there are plenty of reasons.
One easy trick is just typing music=min
to kill the music completely. If you'd rather be able to toggle the music on and off, here's a quick snippet:
-- pancelor music toggle v4 -- https://www.lexaloffle.com/bbs/?tid=146963 _music_fn,_music_last=music,-1 function music(...) if _music_muted then _music_last=... --save 1st arg else _music_fn(...) end end menuitem(5,"♪music",function() _music_muted=not _music_muted if _music_muted then _music_fn(-1,500) menuitem(nil,"…music") else _music_fn(_music_last) menuitem(nil,"♪music") end return true end) |

This is a collection of my various tweetcarts and other code art, circa 2019-2023. Most of my non-interactable postcarts etc are in here, including a few that are only included as a part of this collection. 25+ fun little animations!
(I posted this on itch a while back but forgot to post it here until just now, as I was reading zep's release notes for the BBS's new postcart feature. So I'm uploading my postcart gallery now; better late than never!)
Controls
These carts are non-interactive animations for you to look at. The only interaction is opening the menu (Enter/P), where you can change the active cart.
@zep o/ thanks for fixing the sfx arpeggio effects! here's another audio bug for you:
The music for embedded BBS carts has a noticeable music desync in some cases (the individual tracks sound out-of-sync from each other). I noticed it on linux+firefox with my music visualizer demo cart, reproduced here for convenience:
Individual tracks desync from each other; it consistently happens after the song switches from pattern 3 to pattern 4. The second screenshot here shows me catching it right when it happens:
>

Inspired by https://www.lexaloffle.com/bbs/?tid=2341, here's a description of how sfx and music are stored in Picotron. (Well, there's not much description here yet, just some helpful code. I'll add to this over time)
The data can be in unusual custom formats. The header data is supposed to help in these cases and is accounted for in this code (with some asserts to crash if the data format is unknown). But for most people using this code, the data will be in the standard format.
Library
Code:
[hidden]
-- sfx/music data wrangler -- by pancelor -- The docs call this the "index", I call it the "header" -- https://www.lexaloffle.com/dl/docs/picotron_synth.html#Index function sfxheader_read() local num_instruments, num_tracks, num_patterns, flags = peek2(0x30000, 4) -- ...8 unused bytes here... local insts_addr, tracks_addr, patterns_addr, unused1 = peek4(0x30010, 4) assert(unused1==0,"bad sfx header 1") local tick_len, def_len, def_spd = peek2(0x30020, 3) local def_spd2, unused2, unused3, unused4 = peek(0x30026, 4) -- TODO def_spd2 v. def_spd? assert(unused2==0,"bad sfx header 2") assert(unused3==0,"bad sfx header 3") assert(unused4==0,"bad sfx header 4") return { num_instruments = num_instruments, num_tracks = num_tracks, num_patterns = num_patterns, flags = flags, --0x1 use default track indexing (base+0x20000, increments of 328 bytes) insts_addr = insts_addr, --relative address of instruments tracks_addr = tracks_addr, --relative address of track index patterns_addr = patterns_addr, --relative address of pattern data tick_len = tick_len, --in 1/16ths of a sample at 44100Hz [0 means 5880 -- 120 ticks / second] def_len = def_len, --used by patterns that do not have a default length specified def_spd = def_spd, --used by patterns that do not have a default speed specified def_spd2 = def_spd2, -- ?? } end function pattern_read(i) local base = 0x30100 + sfxheader_read().patterns_addr -- normally 0x30100 local addr = base + i*20 local tracks = { peek(addr, 8) } local flow_flags, channel_mask = peek(addr+8, 2) local len = peek2(addr+10) -- ...8 unused bytes here... return { tracks = tracks, -- array with 8 track ids flow_flags = flow_flags, -- 0x1 loop forward, 0x2 loop backward, 0x4 stop channel_mask = channel_mask, -- 0x1 is tracks[1] unmuted? 0x2 is tracks[2] unmuted? 0x4 => tracks[3], 0x8 => tracks[4], ... 0x80 => tracks[8] len = len, --TODO: how does this interact with track length and header def_len/def_len2? } end function track_read(i) local header = sfxheader_read() assert(header.flags&1==1,"unknown track format") local base = 0x30000 + header.tracks_addr -- normally 0x50000 local addr = base + i*328 local len = peek2(addr) local spd, loop0, loop1, delay, flags, unused = peek(addr+2, 6) assert(unused==0,"bad sfx track") -- note: if len<64 then some of this data is irrelevant: local pitches = { peek(addr+8,64) } local instruments = { peek(addr+72,64) } local volumes = { peek(addr+136,64) } local effects = { peek(addr+200,64) } local effect_params = { peek(addr+264,64) } return { len = len, spd = spd, loop0 = loop0, loop1 = loop1, delay = delay, flags = flags, --0x1 mute -- 64-length arrays, 1 entry per note: pitches = pitches, instruments = instruments, volumes = volumes, -- a 0xFF entry means muted effects = effects, -- stored as their ascii code -- retrieve with ord() effect_params = effect_params, } end -- an alternate version of track_read that extracts a single row -- ti: track index -- ri: row index function track_row_read(ti,ri) local header = sfxheader_read() assert(header.flags&1==1,"unknown track format") local base = 0x30000 + header.tracks_addr -- normally 0x50000 local addr = base + ti*328 + ri return { pitch = peek(addr+8), instrument = peek(addr+72), volume = peek(addr+136), effect = peek(addr+200), effect_params = peek(addr+264), } end function instrument_read(i) local base = 0x30000 + sfxheader_read().tracks_addr -- normally 0x40000 local addr = base + i*0x200 assert(false,"not implemented") -- see https://www.lexaloffle.com/dl/docs/picotron_synth.html#Index -- or read /system/apps/sfx.p64/data.lua:clear_instrument() for info end -- i: channel index 0..7 function channel_current_track(i) return stat(464,0)>>i&1~=0 and stat(400+i,12) or -1 end -- i: channel index 0..7 function channel_current_row(i) return stat(464,0)>>i&1~=0 and stat(400+i,9) or -1 end -- number of ticks played on current pattern function channel_ticks_played(i) return stat(400+i,11) end --currently playing pattern, or -1 function current_pattern() return stat(466) end |
@zep o/
0.2.6b / linux:
- launch pico8 executable
- run
poke(0x5f54,0x80)
(in the console) - run
reboot
- run
poke(0x5f54,0x80)
(copied from the 0.2.6 release notes) - the executable crashes
Step 2 is optional -- it still crashes in step 5 if you skip step 2
In Step 4, running poke(0x5f54,0x60)
instead does not crash.
I'm not sure how to get a stacktrace on my machine, or if it's even possible. this is the best I know how to do:
> coredumpctl debug PID: 12604 (pico8) UID: 1000 (pancelor) GID: 1000 (pancelor) Signal: 11 (SEGV) Timestamp: Tue 2024-06-18 18:51:34 PDT (3min 17s ago) Command Line: /home/pancelor/.config/itch/apps/pico-8/pico-8/pico8 -root_path /run/media/pancelor/data/Users/pancelor/Documents/pancelor/src/pico8/ Executable: /home/pancelor/.config/itch/apps/pico-8/pico-8/pico8 Control Group: /user.slice/user-1000.slice/[email protected]/app.slice/app-pico8-b4acdb07c9cf4629b15a6b78f024fa7f.scope [ [size=16][color=#ffaabb] [ Continue Reading.. ] [/color][/size] ](/bbs/?pid=150091#p) |

hi @zep! This cart should not error, but it does (pico8 0.2.6b / linux)
-- wrap everything in func _init to fix it --function _init() --uncomment this to fix it --poke(0,0) --make this local to fix it coro = cocreate(function() -- uncomment this to fix it -- local x=1 for i=0,32000 do -- flr() is arbitary here; it's -- just a func that should -- return only one value local rets = pack(flr(i)) if #rets~=1 then local str="flr rvals:" for i,v in ipairs(rets) do str ..= "\n\t"..i..": "..tostr(v) end str ..="\nstat(1): "..stat(1) [ [size=16][color=#ffaabb] [ Continue Reading.. ] [/color][/size] ](/bbs/?pid=149328#p) |

I'm pretty sure this is a bug; I'm on pico8 0.2.6b / linux
You can turn on character wrapping with 0x5f36 / 24374, but it acts unexpectedly when you move the camera:
cls(1) poke(0x5f36,0x80) --turn on character wrapping msg='0123456789abcdefghijklmnopqrstuvwxyz' ?msg,0,0,12 camera(16,0) ?msg,16,12,13 |
Expected behavior: both prints should wrap at the edge of the screen.
Actual behavior:

There's a similar issue with p8scii "\^r" wrapping:
cls(1) msg='\^rf0123456789abcdefghijklmnopqrstuvwxyz' --note p8scii wrap at start ?msg,0,0,13 camera(16,0) ?msg,16,18,14 |


cart A: (broken)
msg="havent pressed test yet" menuitem(1,"★test",function() msg="pressed test!" end) ::_:: ?"\^1\^c" --https://www.lexaloffle.com/dl/docs/pico-8_manual.html#Control_Codes print(time(),0,0,7) print(msg) goto _ |
cart B: (working)
msg="havent pressed test yet" menuitem(1,"★test",function() msg="pressed test!" end) ::_:: flip()?"\^c" --this line is the only difference print(time(),0,0,7) print(msg) goto _ |
These two carts should behave the same, but the menuitem function in cart A never runs -- after opening the menu and selecting the menuitem, it still says "havent pressed test yet"
cart B works as expected -- it says "pressed test!"
My system is linux / pico8 0.2.6b
OUT NOW: Make Ten Deluxe. I'll leave this page up but you should go play the updated version instead!
Select numbers that add to ten, to remove them from the board. How high can you score in just 2 minutes?
An homage to Fruit Box. A FruitBox-like? There are some big differences in this version, the most obvious being the lack of music. (the music in the original is incredible)
How to play
- Drag your mouse to select groups of numbers that add up to 10.
- I recommend playing in fullscreen; it's easier to click on larger numbers.
- In the original game, you're awarded one point per number removed,
- Get the highest score you can within 2 minutes. Good luck!
.jpg)
@zep This maybe isn't a bug, but it feels very weird to me:
cls() if (1)<2 print"a" print"b" |
this prints a b
, but I would expect a syntax error instead -- the condition is "1<2" but the if-shorthand parens are only surrounding the 1, instead of the whole condition.
(if you change it to if (1)<0
then it only prints "b", which verifies that the condition isn't being interpreted as just if (1)
or something like that)
oh, and my system is pico8 0.2.6b / linux
Hi @zep!
pico8 0.2.6b / linux:
cls() if (false)?1 --hi print(2) print(3) |
expected output: 2 3
actual output: 3
Here's an altered version that works as expected:
cls() if (false)?1 print(2) print(3) |
(this prints 2 3
, as expected)
3 things seem required to trigger this bug:
- shorthand if
- shorthand print on the same line
- !! further text after the shorthand print (" --hi", in this example. but just a single trailing space triggers the bug too)
When these are all true, the next line seems to get scooped up into the shorthand line. This can include attaching an else
to the wrong if
, like in this more complicated example:
cls() if false then if (false)?1 --hi else print(2) print(3) end print(4) |
expected output: 2 3 4
actual output: 4

This is a silent tutorial video; skip to 2:10 for the juicy bit:
This is an animated wallpaper that shows your custom PNG files -- just place them in a particular folder! It also works as a screensaver. It's cpu-friendly, only drawing during transitions.
Installing
load #photo_carousel
mkdir /appdata/system/wallpapers
save /appdata/system/wallpapers/photo_carousel.p64
- run the cart once, to generate the appdata folder
cd /appdata/photo_carousel
folder
- using your host OS, copy any PNG files into this folder (don't put them in subfolders)
- set your wallpaper to
photo_carousel
in System Settings
Settings
Run podtree /appdata/photo_carousel/settings.pod
to edit the settings. Be sure to not press enter while editing (this crashes Picotron 0.1.0e) and you must save with the mouse, not ctrl-s (another Picotron bug(?) -- ctrl-s saves the current cart, instead of the settings file)
Set the transition time to 0 to disable transitions



hey @zep, I've found a nasty coroutine(?)/multival bug. I'm on Linux + picotron 0.1.0d.
tl;dr: sometimes select("#",tostr(i))
is 2, possibly triggered by calling coresume() with extra args.
I ran into this initially because add({},3,nil)
is a runtime error now (it used to work in PICO-8, but now it throws bad argument #2 to 'add' (position out of bounds)
). I had some code: add(list,quote(arg))
that was crashing as if quote() was returning a second value for some reason, even though the code for quote() definitely returned just one value. (surrounding it in parens (to only keep the first return value) fixed my bug: add(list,(quote(arg)))
)
Version A of the code is very short, and trips the assert inside spin
maybe 50% of the time? sometimes many runs in a row don't trigger the assert, but sometimes many runs in a row all trigger the assert. (maybe that's just statistics tho). Version B is a bit more complex but always trips the assert instantly for me.


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









