PICO-8 0.2.4c is now up on lexaloffle, Humble, itch.io, and for PocketCHIP.
This is mostly a bug-fixing update; you can see the main 0.2.4 change notes in the 0.2.4 thread.
URL Cartridges
With the release of PICO-8 Education Edition, it is now possible to encode cartridges into a 2040-character URL that runs it in a web version of PICO-8's editing tools. Only the code and graphics sections are stored.
> SAVE @URL COPIED URL TO CLIPBOARD 265 / 2040 CHARS |
Tiny Cartridges
When exporting cartridges to .p8.rom format (the raw 32k block of data that is encoded inside .p8.png argb data), 0.2.4c allows only the code section to be stored using the -t (for tiny) switch:
> EXPORT -T TINY.P8.ROM WRITING 154 BYTES (CODE ONLY) |
You should get a file that is exactly as large as the compressed code size reported by INFO. When using LOAD, that file will be loaded into the code section, and the other sections reset to their default states.
There isn't much practical use for tiny .P8.ROM files, but I think it is nice to be able to store a tiny program in its true tiny form on disk.
On the subject of tiny cartridges, for those making tweetcarts, you can now ctrl-click on the character count to get a twitter-character count which counts most (but not all!) glyphs as 2 characters.
Metadata
This is another technical one. 0.2.4c introduces a new metadata section to the .p8 file format suggested by @SquidLight that can be used by external tool authors. Sections with a heading of \n__meta:somestring__\n
are preserved by PICO-8, but not (yet?) utilised by PICO-8 itself. So tool authors can add data to .p8 files by choosing their own meta:label without needing to stuff it into comments, or risk losing data the next time the file is saved by PICO-8 or another tool.
More details: https://www.lexaloffle.com/bbs/?tid=47063
Full Changelog
v0.2.4c Added: save @url -- stores code + gfx as a URL if it can be encoded in 2040 characters Added: html exports store volume/mute and other settings Added: ctrl-g in sprite editor to toggle grid lines when zoomed in Added: IMPORT -L FOO.PNG to import a 128x128 png to the cartridge label Added: EXPORT -L FOO.PNG to export a 128x128 png of the cartridge label Added: EXPORT -T FOO.P8.ROM to export only code section (t for tiny) Added: ctrl-click on character count (bottom right) to see the twitter count (glyphs count as 2) Added: __meta:*__ section to .p8 format -- can be used by external tools to store custom data Added: extcmd("audio_rec") works from exported binaries, and with custom exported filenames Added: read_controllers_in_background in config.txt (0 by default) Changed: .p8.rom files that are 0x3d00 bytes or less are loaded into code section Changed: saved filenames can not include gylphs, or any of !"#$%&'()*+,-:;<=>?[\]^`{|}~ Fixed: can't drag and drop png into sprite editor Fixed: binary exports: ctrl-r causes crash when there is no whitespace at end of source code Fixed: Using -run switch to launch a cart that fails to run -> get stuck in boot screen. Fixed: selection after ctrl-a reports length chars+1 Fixed: draw palette is not observed after changing colours using p8scii control characters Fixed: music playback does not follow cursor after first pattern change (regression in 0.2.4b) Fixed: transform_screen (config.txt) not observed by pause menus and other overlayed elements Fixed: Double-clicking sfx thumbnail (in sfx overview screen) only works after playing music Fixed: Pressing [a] to release looping sfx in sfx editor is broken Fixed: sfx(46)..sfx(56) return -1 immediately after playing music but before host OS has called audio mixer Fixed: Tokens counted as 2 instead of 1: ..= ^= >><= <<>= Fixed: Negative number counted as 2 tokens instead of one when preceeded by: \ & | ^^ << >> >>> >>< <<> Fixed: tostr(tbl) / print(tbl) acts like tostr(tbl, 1) when tbl has a metatable Fixed: ?"\tx" does not advance to next tab stop Fixed: ?"a\*5\nb" does not repeat newline 5 times Fixed: exported label alpha is 0 for colour 0 |

\o/
Say hi to the newest member of the PICO-8 family! A free, web-based, account-less version of the console making it a more accessible way to learn how to program, push pixels and write chip tunes. It comes with a fully functional set of cartridge editing tools, and can load and save .p8 and .p8.png files to and from your local drive (as well as storing them to a temporary filesystem in the browser's cache).
Just run it from any browser that has a keyboard + mouse attached:
If you are new to PICO-8 you can find a manual and tutorials on the main site, or click on the blue bunny for some tips. Here's a 2-minute GIF showing the creaton of a simple PICO-8 cartridge from scatch:

In addition to the standard 32k .p8.png cartridges, PICO-8 Education Edition also comes with a new cartridge format: the URL CARTRIDGE. GFX and CODE can be encoded as a URL string, as long as it fits within 2040 characters. Here's one I prepared earlier:
// EDIT: changed to a bit.ly url so that the BBS doesn't munge it!
To generate a url cartridge, use SAVE @URL
and you should see the address bar change. I'm hoping this will make it easier to pass snippets around, and will add a new dimension to code size golfing and tweetcart'ing. And of course, it is still possible to capture screenshots (ctrl-6) and gifs (ctrl-8, ctrl-9).
Although exporters and SPLORE (the built-in cartridge browser) are not included, larger cartridges can also be shared either as .p8.png files or by first uploading to the BBS as usual (publicly listed or semi-private) and then giving students the cartridge id to load directly from their machine:
LOAD #MY_TUTORIAL_ID |
I hope you enjoy it, and I'll be back soon with some 0.2.4c binaries to match the web version!
-- zep
Hi All
PICO-8 0.2.4b is now up on lexaloffle, Humble, itch.io, and for PocketCHIP.
This is mostly a bug-fixing update; you can see the main 0.2.4 change notes in the 0.2.4 thread.
There are a few handy editor features though; here's a demo of setting animation loops: press l to start and end, and then -,+ or q,w or a,z (for azerty keyboards) to switch frames. This also works at different sprite selection sizes (shift-click and drag a selection from the spritesheet). This gif also shows using ctrl-b to "paste big" multiple times.

Full 0.2.4b changelog:
Added: l in sprite sheet navigator to set loop start / end points (then q,w or a,z to navigate)
Added: ctrl-b in gfx editor to paste 2x2 original size ("paste big")
Added: DEL / backspace to clear selected region in gfx / map editors, and ctrl-x to cut
Added: aggressive_backups option in config.txt (off by default)
Added: transform_screen in config.txt to globally rotate / flip the video output
Added: stat(57) (boolean) true when music triggered by music() is playing or about to start
Changed: memset() faster than using peek4/poke4; now 2 cycles per byte at 8MHz (was 4)
Changed: "running at < 30fps" warning on boot now only for raspi builds, and w/ higher tolerance
Changed: Controller inputs are accepted even when PICO-8 is not the foreground application
Changed: Map can be located at 0x1000 .. 0x2f00 using poke(0x5f56, 0x10) .. poke(0x5f56,0x2f)
Changed: Dotty text mode is now "\^=" ("Stripey") instead of "\^." // #gunayawuho #klax #impossible
Fixed: (not confirmed) crash causing 0-byte .p8 when audio mixer is called during save / run
Fixed: preprocessor not counting comments as white space; should allow: ".. end--[[]]if .."
Fixed: pal(nil) behaving the same way as pal(0); should be same as pal() // broke #rtype-2
Fixed: note entry in sfx tracker is silent after running cartridge until pressing space to playback
Fixed: sub("abc", 4, 4) returns "c" (regression in 0.2.4)
Fixed: SPLORE cart update prompt does not appear when server replies too quickly (race condition)
Fixed: SPLORE cart update prompt only checks version once per session (can't refresh until it shows up)
Fixed: EXPORT command does not flatten includes when exporting to .p8.png / .p8.rom format
Fixed: EXPORT command discards source code changes since last run or load
Fixed: printing a one-off glyph using "\^." terminates the whole string when last byte is a zero
Fixed: Crash when loading a cart with fewer tabs, then creating a new tab and pasting.
Fixed: . command runs at 30fps even for a 60fps cart (-> _update60 is called twice, _draw once)
Fixed: Custom menu items become broken after suspending a cart, entering a lua command, and then resuming
Fixed: memset() with a non-zero value in shared memory region (0x1000..0x1fff) causes garbage corresponding mget() values
Fixed: web player/exports: ctrl-r causes erroneous "external changes reloaded" message and code corruption
Happy Holidays, everyone! This cartridge is my addition to the 2021 PICO-8 Advent Calendar, which you should also check out if you haven't already!

Controls
- LEFT/RIGHT: Slow down or speed up. It is vital to get the right speed for some jumps
- O (Z/C): Jump. You can get more height by going fast and/or holding the button longer.
- X: To restart after crashing.
This game gets reasonably tricky; especially from level 3. But if you can reach the end, you'll get a simple ending along with your total time and number of restarts. The randomish elements in the game (you'll see what I mean) are actually deterministic, so it's possible to form strategies and optimise your time, if that's your cup of tea.
Anyway, I hope you enjoy it even if only as a crashing-into-trees simulator.
This cartridge was heavily inspired by Skyroads and @jackson_allen's most excellent Christmas Sweater Generator

![]() 44 ![]() ![]() |

PICO-8 0.2.4 is now up on lexaloffle, Humble, itch.io, and for PocketCHIP.
This update is another attempt at freezing the API before the last feature update (0.3 -- high scores), but let's see how it goes! 0.2.4 also includes an important security update (see below for details), so please do consider updating. Cartridges posted after this release post will only be visible from SPLORE when running 0.2.4 or later.
64k RAM
64K of Base RAM is now enabled by default. In 0.2.4 it is safe to PEEK/POKE/MEMCPY memory 0x8000 and above (i.e. -0x8000 .. -1) without setting the hardware extension bit at 0x5f36.
Because PICO-8's numbers only cover 0xFFFF, this means it is impossible to poke or memcpy out of range. The original motivation for having 32k by default was to give new programmers some concept of illegal memory operations, and as an extra form of feedback when code isn't working as expected. But over time, these reasons haven't proved as useful as the benefits of allowing 64k by default. Additionally, 0.2.4 introduces the first feature that explicitly uses the upper 32k section (see Big Maps).
Video Remapping
The video memory sections can now be mapped to each other, allowing the screen to be used as if it were the spritesheet, and/or to draw to the spritesheet as if it were the screen.
The mapped address for the spritesheet is stored at 0x5f54, and the mapped address for the screen is stored at 0x5f55. There are only 2 legal values that should be poked to each address: 0x00 (which means map to 0x0000), and 0x60 (map to 0x6000). This gives 4 combinations:
0x00, 0x60: default settings 0x60, 0x60: gfx functions (spr, circ..) use the screen as sprite data 0x00, 0x00: gfx functions all draw directly into the spritesheet 0x60, 0x00: swap spritesheet and screen |
Here's an example of re-colouring and zooming a section of the screen, pasted in at the end of jelpi.p8's _draw() function. This was possible in earlier versions, but required a lot of memcpy()'ing to backup, use and then restore contents of the spritesheet:
-- lighter colours; no transparency pal({[0]=1,5,13,13, 9,13,7,7, 14,10,15,11, 6,6,15,15}) palt(0) -- set the screen memory as the spritesheet -- and stretch screen->screen poke(0x5f54, 0x60) sspr(48,80,32,32, 32,32,64,64) poke(0x5f54, 0x00) pal() -- return to defaults |

Big Maps
Similar to gfx memory mapping, the map can now be placed at address 0x8000 and above (in increments of 0x100). This gives 4 times as much runtime space as the default map, and an additional POKE is provided to allow customisable map sizes.
Legal values are 0x20 (the default) and 0x80 and above. There are two pokes you need:
poke(0x5f56, 0x80) -- set the map to 0x80. Default is 0x20 poke(0x5f57, 0) -- set the width of the map (0 means 256). Defaults to 128 |
The height of the map is determined by how much data is available in the memory section. So in this case, there is is 32k available divided by 256, which gives a map height of 128 cels -> 256x128.
Note that the map editor still always writes and loads at 0x2000. This feature doesn't come with extra cartridge space to match! So to use 0x8000 and above, you'll need to manually mset() or memcpy() some map data. This feature will be most useful for carts that procedurally generate their maps or have some custom data storage scheme.
The map address is observed by all map functions: mget(), mset(), map(), tline()
One-off Characters
P8SCII character data can be specified and printed in-line using "\^." followed by 8 bytes of raw binary data, or "\^:" followed by 8 2-digit hexadecimal values. The data format is the same as custom fonts; each byte specifies a row of 1-bit pixel values, with the low bit on the left.
?"\^.$|へ|>○¹⁶" ?"\^:247cb67c3e7f0106" |

Audio Sync
STAT(46)..STAT(56) can be used instead of STAT(16)..STAT(26) to query the state of the sound system with more precision. Although there are many carts that managed fine with the old STAT() calls, it required a bit of fiddling and data smoothing to get right, derived from whatever state the mixer is in at the start of each frame (which only changes ~20 times a second, depending on the host platform and phase of the moon).
The new version (STAT 46..56) works by storing a history of sound mixer states at each tick, along with timestamps for sound driver mix requests, to estimate which state snapshot is currently audible at any moment in time. It's still not perfect, but requires less to work to get pretty good sync going.
Test cart: LOAD #AUDIO_SYNC_TEST
API Changes
Some small conveniences:
CHR() can now take multiple arguments to build a string:
> ?chr(104,101,108,108,111) hello |
SUB() can take nil or _ as the last argument to return a single character (instead of the rest of the string). This is useful when the second parameter (the index) is some long expression that you don't really want to repeat in the last parameter, or go to the trouble of assigning a temporary variable to.
PAL(N) can be used to reset one of the three palettes to their default state (0 draw palette, 1 display palette, 2 secondary palette).
> sub("hello", 3, _) l > sub("hello", 3) llo |
Cartridge Formats
The .rom.p8 format (a raw 32k block of cart data) can now be used by CSTORE(), RELOAD() and in multicarts. This should make it easier to generate cartridge data using external tools without needing to jump through the hoops of writing to .p8 / .p8.png format.
On a related note, the EXPORT command can now also be used with any cartridge format. So, to convert between cartridge formats from commandline, the -export switch can be used (pico8 foo.p8.png -export foo2.p8.png
). From inside PICO-8, it is also a handy way to save a copy of the current working cartridge without altering the current working filename:
> EXPORT COPY_TO_UPLOAD_TO_BBS.P8.PNG |
64-bit Raspberry Pi Builds
The file archive ending _raspi.zip now includes a build for the 64-bit version of Raspberry Pi OS called pico8_64. Exported binaries also have a matching file (e.g. mygame_64).
Security Patch
PICO-8 0.2.3 had quite a serious security flaw affecting Mac OS and Linux machines, that allow arbitrary commands to be injected into LOAD("#...") calls and executed on the host machine with the same privileges as PICO-8. PICO-8 0.2.4 fixes this in two places: the illegal cartridge ID is not allowed to be processed in the first place, and the URL used to fetch cartridges / listings is only allowed to contain a limited subset of characters. Earlier versions only had the second check, which was faulty.
As far as I can tell, there are no BBS cartridges that have used this exploit, and as a precaution SPLORE will no longer allow cartridges newer than this post to be listed to older versions of PICO-8.
So please do update to 0.2.4 to ensure you are not affected by this vulnerability, including for cartridges obtained from somewhere outside of SPLORE.
Server Test: Doodlemud
The last remaining part of PICO-8's api is SCORESUB() -- a simple api function for submitting highscores, but with some internal complexity. I've set up some new infrastructure for this to be reasonably future-proof and scalable, and a simple game for battle-testing it. You can try it here:
https://www.doodlemud.com/#p8024
Doodlemud is a multiplayer world-building / exploration game where each player can switch between drawing each room and being a player in it. This test requires a keyboard and mouse (no touch / tablet support); press cursors to move around, and G to toggle gravity for everyone in that room. Feel free to change the url to start new empty worlds (any alphanumeric name after the hash is ok), but keep in mind that it is experimental, and all of the data will be erased when it gets updated!
ASCII Manual
The .txt version of the manual is back! It is included in the archives, and you can find it on the Resources Page. The text and html versions of the manual are now both generated from the same source file, so should always be in sync.
Changelog
You can find the full changelog here:

(click to expand)
Heeey, it's Squiddy! This is a game made in 1017 bytes for PICO-1k Jam, co-designed and pixelled by @castpixel (twitter). This is our second production as pod; you can find the first one here. Squiddy is also up on itch.
Technical Details
1024 bytes is an interesting size limit for a game; it's large enough to try for some relatively detailed game logic or visuals, but small enough that you need to execute some weird programming tricks and design pivots to get everything to fit. If you found this post because you heard that PICO-8 is a nice introduction to game programming, I apologize for the following code snippets! First up, here is the full source code for the game that includes the graphics and map data. It can be pasted into PICO-8 0.2.3 or later:
➡️=0t=8⬇️=7for x=0,3800do ➡️-=1if(➡️<1)➡️=2r=3⬇️=7-⬇️ while r>2do r=ord("○ュ?\0゜ョヤャ◝◝⁷ツん¹ュ=◝に◝+ンョャヤ○る³ユ\n <◝エ𝘤ムもにュよ◝トラ⁙⁴よ░²リムリᵇ³ト■𝘨○ワ;◝エらョ0\0pクシチ◜◝=ュoれ/ウワ◝モャいユ𝘪ゆ?=0◝cりdオトは⁷ト?⁵\0𝘴リ◆○☉6サ¹6ア?ュ◝❎。。ユユ◝ャヤ2v\0q○¹\0ワw3v\0001よ¹\0◝?𝘰v⁴ャ゜⁴ら█pタp⁵ュ?オ; <○○ヨ◝oに◝𝘰ュ?1░ろヲア◝ᵇp○\0s³ヲュひ\0|れツ◝リツ𝘰゜6ト⁴ュ?p◜◝モpᶜオん▶○◜ャ\r○𝘣◝▶トリに1◝⁷ョ_ンん゜ョょ737⬇️○ョ¹ュのら◜-エs○ユト⁴らメ\0³•~◝エろヨ/゜3\0な?𝘯ュ◝よ▮ラヨsクろ?リトンミエセろ⁵ᵇムり3オ◝𝘮rユ⁷ワ>pンᶠ゜シてン>ナレ◝𝘰⁙ュuワ◝シ◝1゛ュ4チ◝?◝◝𝘰\0゜ᵇユ◝エ◝4◀0○ ◝◝◝◝◝\0ら𝘬\0",t\8)>>t%8&3t+=2➡️+=r end sset(x%95,x\95,⬇️)end o=128w=256f=0r=4128🐱=cos ➡️=0 ❎=64🅾️=r*2s=spr::_::t+=.03camera(❎,🅾️)map()v=0for i=0,t/2do x=i\32y=i%32v/=2v+=sget(r%o+x/4,r\o+y/4-🐱(i/870)/2)&4mset(x,y,v)p=i%4y=i\4*5%31 if(mget(x,y+1)==4)s(0,x*8+🐱(p/4-t),y*8-p*4) if(r%5==2and i<o)s(i&2,-t*i%w,i*i%w+🐱(i/9+t)*3)➡️-=1>>12 end if(r==4136)s(8,92,112,5,5) s(20,o+🐱(t/2)*9,90+r+🐱(t)*5,1.5,2)b=btn()n=b&32f=f/2+n/20k=b%4\2-b%2if(f>n)➡️+=f*k ⬇️-=f f=0 ❎+=➡️ 🅾️+=⬇️ ➡️=-➡️ ⬇️=-⬇️?"ᶜe\^wfin ♥",108,60+r if(mget(❎/4,🅾️/4)<1)➡️*=-.95⬇️=.05-⬇️*.9 r+=(❎\o+🅾️\o*o)*8❎%=o 🅾️%=o s(16+(f&2)+k%2*32,❎*2-8,🅾️*2-8,2,2,k<0)?"⁶1⁶c" goto _ |
Sprite Storage
Here's the 95x40 spritesheet (hidden for spoilers).
The sprites and map data is stored in the long string passed to the ord() function. The challenge was to compress the data in a way that the decoder + compressed data would be smaller than the raw uncompressed sprite data. This is hard to do with a small amount of 1-bit graphics!
The format we settled on quite early is a variant of RLE (Runtime Length Encoding -- storing the lengths of spans of the same colour) with an interesting constraint: the smallest span length must be 2 pixels. This means that it is impossible to have a single-pixel vertical line, but thin horizontal lines are ok. It is also quite different from using double-width pixels, as spans can stop and start anywhere.
Each span length is stored as a sequence of 2-bit values. Each value is added to the span length, and the sequence is finished when the value is not 0b11 (3). This means that that 2-pixel spans take 2 bits to store (the worst case -- same as raw), 4-pixel spans also take 2 bits (the best case -- half the data needed), and for very long spans the data compresses to around 2/3 of the original size. The number of bits needs to store a span length jumps up every 3 values:
2 pixel span: 00 3 pixel span: 01 4 pixel span: 10 5 pixel span: 11 00 <- 2 more bits because the first value is 0b11 6 pixel span: 11 01 7 pixel span: 11 10 8 pixel span: 11 11 00 ... |
The code for the decoder is 106 characters after re-using some game state variables, and the sprite data compresses to 340 bytes, which becomes 358 characters with the required escape codes to store it in the source code (e.g. value 0 becomes "\0"). This gives a total of 464 characters -- around 50 less than using raw data + some code to transfer it to the sprite sheet. Worth it!
-- decode sprite data v=0 -- last read 2-bit value l=0 -- length of span i=8 -- input data position in bits c=7 -- current span colour for x=0,3800 do -- for each spritesheet pixel l-=1 -- one less pixel left to draw of the current span -- read the next span length (l) if ran out of pixels to write -- ord() grabs an 8-bit value from the data string if(l<1) l=2 r=3 c=7-c while r>2 do r=ord(data_string,i\8)>>i%8&3 i+=2 l+=r end pset(x%95,x\95,c) -- set a single pixel end |
Map Data
The map is generated from sprite pixels: each pixel in the spritesheet corresponds to a 4x4 block of map tiles. This allows for a 2x2 screen room to be described by an 8x8 pixel sprite. This produces a very blocky world however, so the sampling position in the spritesheet is also distorted by a sine wave that is chosen to line up between neighboring rooms, so that it is not possible to enter the next room and already be inside a wall. Here is an example of a room without and with distortion:
Apart from obscuring the coarse resolution of the source data, using this distortion had some other nice side-effects. It produces local organic details like a single protruding tile at the edge of some 4x4 clusters, and larger features like the raised 'platform' that the statue is sitting on. As the seaweed placement is based on both x and y position, having varying y positions for the ground blocks also means that the seaweed placement test could leverage that to look scattered without using an additional pseudo-random number expression.
It is also possible to reuse the structure of regular sprites (that are used as visible graphics) as map shapes. This only ended up happening once: the left side of the last tunnel is shared by the sprite data of the seahorse. But if you're really keen, you can also glitch through a wall to get out of bounds, and then freely explore the spritesheet.
Superloop
When making code-golfed games and tweetcarts, I normally end up having a single large loop that is used by anything that needs to be looped, to avoid the "FOR .. DO .. END" 16-character overhead. Unfortunately (or fortunately) I was unable to merge the sprite unpacking in this way, but the rest of the game uses a single superloop. The map data is fetched from the spritesheet, bubbles are drawn, and segments of seaweed are drawn at random top-of-ground locations, all using the same loop counter.
-- time starts around 2722, and t/2 is cheaper than writing a 4-digit number for i=0,t/2 do -- unpack map from the spritesheet in 4x4 map tiles -- Using v = (sget() + v)/2 gives tile values that differ -- based on their vertical neighbour so that viable seaweed -- locations can be identified (top is always 4) x=i\32y=i%32 v/=2 v+=sget(r%o+x/4,r\o+y/4-sin(i/870)/2)&4 mset(x,y,v) -- seaweed p=i%4 -- which segment of seaweed y=i\4*5%31 -- y position of seaweed to test (unevenly scattered) if(mget(x,y+1)==4)spr(0,x*8+sin(p/4-t),y*8-p*4) -- draw bubbes and apply water current to player -- only for 1/5 rooms, and for the first 128 iterations if(r%5==2and i<128)spr(i&2,-t*i%w,i*i%w+cos(i/9+t)*3)player_dx-=1>>12 end |
Janky Physics
The player uses a coordinate system (0..128 in each room) that is rigged so that the world position of the player is the same as the screen position after adjusting for camera position and scrolling. It also gives nice ranges of values that can be manipulated with expressions containing only small integer values.
-- control player -- accumulate force (f) while button X is held, and apply when released b=btn() n=b&32 -- buttons states f=f/2 + n/20 -- f approaches but does not exceed 3 k=b%4\2-b%2 -- -1,1 when LEFT or RIGHT is pressed if(f>n) then -- apply force and reset player_dx += f*k player_dy-=f f=0 end -- add velocity to the player's position player_x += player_dx player_y += player_dy -- invert the velocity before map collision test so that -- bouncing, friction and gravity can all be applied when -- there is /not/ a collision. works out slightly shorter player_dx = -player_dx player_dy = -player_dy if (mget(player_x / 4, player_y / 4) < 1) then -- no collision: invert the velocity to prevent bouncing, -- and apply friction and gravity in the same expression player_dx *= -.95 player_dy = .05-player_dy*.9 end |
Squiddy Racer
Because there is only a single loop used to generate the map and to do things every frame, it means that map data is generated every frame. This is what it looks like if the map distortion accidentally has a time component given to the sine wave:

![]() 37 ![]() ![]() |
PICO-8 0.2.3 is now up on lexaloffle, Humble, itch.io, and for PocketCHIP.
This update is focused on resolving runtime issues, with a couple of small but handy features thrown in for good measure:
Lucky Draw
To grab 32 random cartridges from the BBS, there is now a 'Lucky Draw' list in SPLORE. It is more likely to select cartridges that have more stars, but even unrated carts have a decent chance of appearing. Perhaps this will be a way to unearth some undiscovered gems, or just to find something new to play without scrolling back through several years of carts.

The list is cached, so it only changes every 2 minutes or so. But you can keep paging through the list to get new items forever.
Live Token / Character Count
Select some text in the code editor to view how many characters or tokens are contained within.

Shorthand Print Statements
The shorthand version of print (?"hello") used to require no preceding statement be on the same line, but you can now mix it with things like the shorthand IF statement, as long as it is the last thing on the line:
-- PLAY A SOUND EFFECT WHEN HIT IF (PGET(X,Y)>0) HIT=1 ?"\ADAF" |
Thanks to @Liquidream for suggesting this. It should be handy for things like tweetcarts and PICO-1K Jam (which coincidentally is also organized by Liquidream!)
New Manual
The PICO-8 Manual is still a work in progress, but it went through a large update last month, with new formatting, dark mode, linkable headers, and more printer-friendly. You can find it on the resources page:
https://www.lexaloffle.com/pico-8.php?page=resources
The changelog has been separated into a text file here:
https://www.lexaloffle.com/dl/docs/pico-8_changelog.txt
The pico-8.txt included in the distributables has changed to a short version that links to the online manual, but later on I'll bring back the full ascii version and keep it in sync with the html version.
.P8.ROM Format
This isn't a very useful feature unless you are manipulating PICO-8 cartridges with external tools, but it has always irked me that it is not possible to write 32k of PICO-8 cartridge to a file that is 32k!
> SAVE FOO32K.P8.ROM SAVED FOO32K.P8.ROM > LOAD FOO32K.P8.ROM LOADED FOO32K.P8.ROM (0 CHARS) |
Majestic.
Large Numbers
tostr() and tonum() now take format flags that make it easier to deal with large numbers.
https://www.lexaloffle.com/dl/docs/pico-8_manual.html#TOSTR
PICO-8's numbers are all 16:16 fixed point, which basically means that every number is internally stored as a large 32-bit integer that is divided by 65536 to get the value in Lua. So for example, if you want to use that large integer for something like scores that need to go above the usual maximum of 32767, bit 0x2 can be used to display it in the raw integer form:
?TOSTR(3, 0x2) 196608 ?TONUM("196608", 0x2) 3 |
To add 10 points to a score stored in this way, you'd need to shift it right 16 bits:
SCORE += 10>>16 |
CPU Counting
0.2.3 contains two changed to cpu costs which does not effect many cartridges:
-
In 0.2.2c, it was cheaper to write "local a=0+b" than "local a=b" and also "local a=0-b" than "local a=-b". This is because the Lua vm instruction for addition was cheaper than local assignment (OP_MOVE), and for unary minus (OP_UNM). The best solution I could find was simply to reduce the cost of those two instructions to 1 cycle instead of 2, resulting in a slight speed increase in some cases.
- peeking or poking multiple values in 0.2.2c did not cost any additional cpu, so doing something like: poke4(0,peek4(0x1000,0x1000)) was completely free! The same line in 0.2.3 now costs the same as doing an equivalent memcpy.
Future Plans
Next up for PICO-8 is a 64-bit Raspberry Pi Build, and also a preview of a web version that is also handy for running PICO-8 on Chromebooks. I'm also still working on online scores, which required developing some bespoke infrastructure that is optimised for PICO-8's usage patterns, and can handle traffic from exported carts without needing to start charging for PICONET subscriptions or anything like that. I'll test it next month with the release of https://www.doodlemud.com, which is a multiplayer drawing game designed to battletest PICO-8's backend before exposing it to precious cartridge data!
Changelog
![]() 15 ![]() ![]() |


My entry for TweetTweetJam #6: a game in 2 tweets (at 558 characters)
Roll right without crashing for a high score (or left for a low score)
CTRL-R to restart
Hold X when landing to jump.
o=128x,y,u,v,h,p,s,d=o,0,0,0,0,32,sin,circfill::_:: ?"\^1\^c" for i=-o,384do j,l,b=x\1+i,h,btn()h,a=s(j/o)+s(j/93),p+i/4 line(a,0,a,h*2.1+p)h=p+h*5*max(s(j/3^7)) if(i==0and y>h)then if v>0then u+=v*(h-l)v*=-.7else v+=u*(h-l)end y=h if(b>9)v=-4 end for z=0,2,.2do pset(p+i/z,67+h/z,12+(h-l)*4)end end for i=-o+x&-p,x+o,p do a=-t()/(4+i^2%29)m,n=o+i+cos(a)*p-x,s(i/999)*80+s(a)*p d(m,n,8,7)end v+=.2u*=.7x+=u y+=v if pget(p,64+y)==7then::w::circ(p,64+y,v,v)v+=1 for i=24576,32767do poke(i,@i/1.2)end ?"\^1\^jef"..x\1 goto w end d(p,64+y,2,7)u-=b&1u+=b&2goto _ |
Dig to collect gems, and don't get squashed! A wee game made in a weekend for Ludum Dare #48. You can find the compo entry here: https://ldjam.com/events/ludum-dare/48/diggleoid
PICO-8 0.2.2b 0.2.2c is now up on lexaloffle, Humble, itch.io, and for PocketCHIP.
This is a bug-fixing patch for 0.2.2 -- see that thread for a summary of recent features.
0.2.2b does have a few small features though. You can now use a .lua.png extension with the EXPORT command to get the source code (all tabs) of your cartridge printed out to an image with roughly A4 dimensions. This is intended mostly for visualisation purposes, and lines are cropped to 188 pixels (47 characters) wide. The jelpi demo source code is bigger than I imagined!
EXPORT JELPI.LUA.PNG |

dots3d.p8 is much cuter

There are also some new extcmd() commands for setting screenshot output filenames, and the window title (intended for exported binaries).
I've also added a way to force the pause menu to come up, even if the cartridge is blocking it with poke() trickery. Simply hold the pause button for half a second, and the menu should come up no matter what (it's implemented at a low level). This might be useful when using SPLORE from a sofa, and a keyboard isn't available to terminate stubborn cartridges.
Full list of changes:
![]() 60 ![]() ![]() |

Hey All! PICO-8 0.2.2 is now up on lexaloffle, Humble, itch.io, and for PocketCHIP.
This release follows a pattern set by previous 0.2.* updates in that I set out to fix a bunch of bugs and resolve design details, but in doing so, went down some deep rabbit holes and came out the other end with brand new features. As a result, some of the new features are on the advanced side, and this post will be likewise be more technical than usual. But I hope everyone can find something fun to mess around with!
SFX Filters
At the bottom of the SFX editor, you can now find 5 switches that alter the characteristics of each instrument. You can get a much wider variety of sounds and textures now, but they're meant to feel like variations on the existing instruments rather than completely new ones. I settled on this scheme after working on Voxatron's sound system and found that I could boil the set of parameters I wanted down to just 7 bits of information -- which is fortunate because there were only 7 unused bits left in the SFX data!

The 5 switches are:
- NOIZ: Generate pure white noise (applies only to instrument 6)
- BUZZ: Various alterations to the waveform to make it sound more buzzy
- DETUNE-1: Detunes a second voice to create a flange-like effect
- DETUNE-2: Various second voice tunings, mostly up or down an octave
- REVERB: Apply an echo with a delay of 2 or 4 ticks
- DAMPEN: Low pass filter at 2 different levels
SFX instruments (the green ones) can also use these filters, so it is possible for example to have a detuned square wave in the same SFX as a dampened triangle. When both the parent SFX and the SFX instrument have the same switch set at 2 different levels, the greater of the two is used.
Here's @Gruber explaining filters, along with a newly added control over the length of each SFX (useful for implementing uneven time signatures):
And here's @tesselode putting them to good use:
[tweet]
The BBS SFX player (copy a range of patterns and paste them as text) works with the new filters, but is still janky on mobile:
P8SCII
In previous versions of PICO-8 there were some characters in the range (0..15) that were sitting around doing nothing useful when you printed them. That's clearly no good, so 0.2.2 has introduced a new control codes that allow control over things like text formatting, cursor position and even sound generation. Along with some kana improvements, 0.2.2 now has complete set of 256 characters -- or P8SCII (PICO-8 Standard Code for Information Interchange).

An example: the "command" character (6) can be written as "\^" followed by a command character and sometimes a parameter:
cls()cursor(20,20) ?"\^iinvert\n" ?"\^ppinball" ?"\^wwide \^ttall\n" ?"\feset colour\n" ?"\#5solid background\n" |
Character 7 ("\a") can be used to play audio using a compact description
?"\a" -- beep ?"\asfc3ccbdcbae2ee" -- play a wee ditty |
This might be useful for squeezing extra sound effects into a cart (or tweetcart), or for simple sound effects in type-in zine listings.
For more details on the P8SCII control characters, see the manual: https://www.lexaloffle.com/pico-8.php?page=manual
Custom Fonts
A custom font can be enabled using control character 14 (or by setting bits 0x81 at address 0x5f58). A fixed character width and height can be specified at 0x5600,0x5602, followed by 8 bytes per character (8x8 bitmap) starting from 0x5608 (character 1). It can be a little fiddly setting the correct data up, so to get started, here's a wee tool that grabs font characters from the spritesheet and generates a snippet:
LOAD #FONT_SNIPPET |
The output snippet is 7 tokens, and can be pasted into your cartridge. Here are some example fonts and their snippets:

The one on the right is from @thattomhall's SPRITEFONT cartridge: https://www.lexaloffle.com/bbs/?pid=75073#p
Snippets: https://www.lexaloffle.com/dl/files/font_snippets.txt
(that reminds me, there needs to be a good way of posting long snippets like this on the bbs)
Sprite Fill Patterns
It is now possible to apply fill patterns to sprites, which includes spr(), sspr(), map() and tline(). A special bit (0b0.01) is set when calling fillp() to turn this feature on. Each sprite pixel is mapped to TWO colours using the (previously undocumented!) secondary palette, and these two colours are used to draw the pattern.
There are more technical details in the manual, but here's an example:
If you add the following fillp call to /demos/jelpi.p8 in init_level() (tab 5):
cls()reset() fillp(♥\1|0b.011) |
The result is:

There's a lot going on here!
- I've added the call to fillp() after reset(), as it sets fill pattern state to default values.
- ♥\1 is a fill pattern constant with \1 (integer divide) to remove the alpha bit (0.5)
- 0b.01 (0.25) is the bit that turns on fill patterns for sprites
- 0b.001 (0.125) is another bit that applies the secondary palette mapping to ALL drawing functions, including a rectangle that is drawn at the bottom of the mountains.
- the default secondary palette is being used, which consists of the original colour + a slightly darker counterpart. For example, 12 (0xc) maps to 0xdc. This could be changed with pal(12,0x78,2).
I think there there will be some interesting unexpected ways to use this feature, but a nice immediate example by @johanp is to add dithering to textured polygons drawn with tline:
[tweet]
(as the truck turns around, you can see a few frames of checkerboard dithering without destroying the look of the texture)
MULTIPOKE
POKE, POKE2 and POKE4 can be given up to 2048 values to poke into memory in sequence, similar to the DATA statement found in early BASIC variants. Try this to write 6 pixels to video memory:
poke(0x7000,8,9,0xac) |
This can be used with unpack() and split() to dump a bunch of values to ram.:
poke(0x7000,unpack(split"8,9,0xac")) |
Values can be read out in a similar manner:
a,b,c=peek(0x7000,3) -- 8,9,0xac |
There is a limit of 2048 values in both cases, which means you can copy up to 8k in one go using PEEK4/POKE4.
Locked Mouse Pointer
This is a feature in 'devkit' mode which is normally intended for making development tools and exported binaries, but of course you are welcome to use it for whatever you like :)
(but keep in mind that some players using the BBS don't have a mouse or keyboard)
POKE(0x5F2D, flags) -- where flags are: 0x1 Enable 0x2 Mouse buttons trigger btn(4)..btn(6) 0x4 Pointer lock (use stat 38..39 to read movements) |
.. so you need to poke(0x5f2d,0x5) to enable mouse with lock. stat(38),stat(39) will then give the mouse movements in desktop pixels rather than PICO-8 ones, so that the window size is irrelevant; they can be thought of as abstract motion events with a comparitively high sensitivity. PICO-8 attempts to match mouse movement when entering and exiting locked mode, but this requires setting the mouse cursor at the operating system level, which is not supported in web. In any case, you might get more consistent results by setting mouse lock once at the start of your cartridge and leaving it there (other mouse events will still work as usual via stat 32,33,34).
You can see it in action in @freds72 and @paranoidcactus's POOM: https://freds72.itch.io/poom
Custom Menu Control
Menu item callbacks added with MENUITEM can now elect to keep the pause menu open by returning TRUE, and can also detect if the left and right buttons were pushed. The callback takes an integer parameter that is a bitfield of left and right button presses. Buttons 4..6 all map to each other (can't tell them apart).
For example:
function my_menu_item(b) if(b&1 > 0) menuitem(_,"left!") if(b&2 > 0) menuitem(_,"right!") if(b&32 > 0) menuitem(_,"selected!") return true -- stay open end menuitem(1, "select me", my_menu_item) function _draw() cls(5) print(t()) end |
Another example:

LOAD #CUSTOM_MENU |
Web Gamepad Improvements
The default PICO-8 0.2.2 HTML exporter (and the BBS web player) now includes @weeble's improvements that allow the DPAD mapping, and better hotplugging / controller indexing behaviour.
Thread: https://www.lexaloffle.com/bbs/?tid=41293
Optimisation / CPU Changes
PICO-8 0.2.2 underwent a fairly aggressive optimisation pass; heavier cartridges use around 20% or in extreme cases 30% less cpu / battery life. I did some further tweaks of CPU costs to keep the theoretical host cpu ceiling as low as possible, which helps a lot on devices like the Raspberry Pi 2 & 3. Let me know if you have a cart that's running too slow on 0.2.2 -- I don't think there are many affected, and I'd be happy to help optimise it by swapping in the new binary operators etc. See the changelog for other cpu cost changes.
The following are some notes for the curious -- this shouldn't affect performance considerations when making PICO-8 carts. There was/is a lot of potential to change cartridge behaviour in subtle ways however, so as always, please let me know if you see something weird that was working in previous versions, even if it seems like a small thing.
There were 5 areas that needed improvement:
Map Export
EXPORT FOO.MAP.PNG |
.. to get a full-scale png of your map!

Changelog
![]() 34 ![]() ![]() |


[O] (z/c): Run / Fly
[X] (x/v): Jump
Left / Right: Turn while in mid-air
v3 update: [X] also accelerates to make mobile controls easier

This is intended to be mostly a toy rather than a game, but you can get points for doing tricks!
Front / back flips (more points for 2x, 3x)
Early Santa: Santa lands early
Sneaky Weasel: Back flip close to the ground
Moon Grazer: Jump High
Glider: Jump Long
Fishtail: Do a bunch of wavey turns in mid-air
Firebird: Dangle Santa like he's the pod from the videogame Thrust
Santa Smash: Santa lands upside down. Don't do that trick.
This game is my contribution to the 2020 PICO-8 Advent Calendar. The calendar always surprises me with its sheer variety and depth of joyful creations, and this year is no exception! I encourage you to have a rummage around inside the advent calendar for the Full Experience (here's the menu cartridge), but to whet your appetite, here is also a partial gif dump:
A couple of carts for #tweettweetjam 5 that fit in 560 chars or less.
Cosmic Painter
L/R to rotate
O to accelereate
X to paint
Comets
Just avoid the comets for as long as you can! My best is 49
Crashing into the score kills you.
![]() 165 ![]() ![]() |
Wobblepaint started as a secret cartridge in my 2019 Advent Calendar entry, but I think it's time for a proper release! This version has some extra controls and nicer, less crinkly wobble.
Instructions
Your brush has a size, colour, pattern and shape that can be adjusted separately. There are 4 presets you can select and modify using keyboard shortcuts, or by clicking and dragging the top menu bar down to reveal a palette of attributes.

Instructions
CTRL-Z, CTRL-Y (or S,F) to undo/redo
CTRL-C, CTRL-V to copy and paste between doodles
W,R to switch between doodles (or use the menu buttons)
TAB to toggle menu
Mouse wheel (or e,d) to change brush size
RMB to pick up a colour
RMB in menu colour palette to select secondary colour (used for patterns)
LMB+RMB in menu colour palette to set the background colour
To save all doodles, use the cartridge icon button in the pull-down menu.
Wobblepaint saves data to itself. To start a new wobble cart, type LOAD #WOBBLEPAINT from inside PICO-8 and then save it as something. The data storage is reasonably efficient so you can get around 20~100 doodles to a cart depending on complexity.
To save a gif to desktop, use the gif button to record a second of looping wobble. If you want to record multiple doodles (e.g. for an animation or story), press tab to hide menu, CTRL-8 to start a gif, W,R to flip through the doodles, and then CTRL-9 to save the gif.
Gamepad controls
Turn off the devkit input in the options menu ("turn off mouse") and use a gamepad:
LRUD to move the cursor
[X] to paint
[O] + L/R to undo/redo
[O] + U/D to adjust brush size
In the menu, [X] and [O] behave the same as LMB,RMB
Using Wobblepaint doodles in your cartridges
CTRL-C copies doodles in a text format that can be pasted into code (or bbs posts)
Paste the code from tab 5 into your cartridge to load and draw them:
wobdat="1f00514302d06ee1179c8d34a74033b359e834319ba6504fa4690ade340000" str_to_mem(wobdat, 0x4300) mywob = wob_load(0x4300) function _draw() cls(mywob.back_col) wob_draw(mywob) end |
Or alternatively, copy the binary data straight out of the spritesheet and use load_library (tab 2) to load all of the doodles into a table.

Changes
v1.5: fixed uneven frame times when recording gif and increased length to 2 seconds (was 1)
![]() 40 ![]() ![]() |

Hi All! PICO-8 0.2.1b is now up on lexaloffle, Humble, itch.io, and for PocketCHIP. This update started as a continuation of 0.2.0 bug-fixing work, but I also relaxed my position on API minimalism just enough to add some new features :D
UPDATE: 0.2.1b is now live and fixes the print() bug, and a few other things. See the changelog below for details.
Ovals
You can draw ovals (technically, ellipses) both when running a cartridge, and when using the shape tools in the graphics/map editors. Ovals are specified by their boundary rectangle, and follow the usual draw state rules.
pattern={[0]= …,∧,░,⧗,▤,✽,★,✽, ˇ,░,▤,♪,░,✽,★,☉, ░,▤,♪,░,✽,★,☉,…, ∧,░,⧗,▤,✽,★,✽,★ } function _draw() cls(1) for i=0,31/32,1/32 do local x=64+cos(i+t()/8)*48 local y=64+sin(i+t()/8)*44 local w=8+cos(i*2+t()/2)*6 local h=8+sin(i*3+t()/2)*6 fillp(pattern[i*32]) ovalfill(x-w,y-h,x+w,y+h, (i*32)%8+8) end print("pico-8 0.2.1",40,62,13) end |
Serial I/O
To make it easier to set up workflows for getting data in and out of carts during development, some new serial() channels are available. You can turn a file on your host machine into a binary stream, or drag and drop it into the running cartridge to do the same. From the manual:
Additional channels are available for bytestreams to and from the host operating system. These are intended to be most useful for UNIX-like environments while developing toolchains, and are not available while running a BBS or exported cart. Maximum transfer rate in all cases is 64k/sec (blocks cpu). 0x800 dropped file // stat(120) returns TRUE when data available 0x802 dropped image // stat(121) returns TRUE when data available 0x804 stdin 0x805 stdout 0x806 file specifed with: pico8 -i filename 0x807 file specifed with: pico8 -o filename Image files dropped into PICO-8 show up on channel 0x802 as a bytestream: The first 4 bytes are the image's width and height (2 bytes each little-endian, like PEEK2), followed by the image in reading order, one byte per pixel, colour-fitted to the display palette at the time the file was dropped. |
Drag and Drop
On a related note, you can also now drop .p8.png cartridges into PICO-8 to open them. If there is a cartridge with unsaved changes, it will prompt before continuing. You can also drop .png files into the spritesheet, by first selecting the sprite that should be the top-left corner location.
API Changes
add() now comes with an optional 3rd parameter: an integer that specifies the location in the table that the new value should be inserted at. Similarly, a new variation of del() is available: deli(tbl, index) ("delete by index") allows deleting from a given location in the table rather than by value.
split() is also new. It complements the common strategy of storing data as strings. From the manual:
split str [separator] [convert_numbers] Split a string into a table of elements delimited by the given separator (defaults to ","). When convert_numbers is true, numerical tokens are stored as numbers (defaults to true). Empty elements are stored as empty strings. split("1,2,3") -- returns {1,2,3} split("one:two:3",":",false) -- returns {"one","two","3"} split("1,,2,") -- returns {1,"",2,""} |
Binary Storage
It is now also more efficient to store 8-bit binary data in the source code section, by encoding it as a binary string. The .p8.png format stores uncompressable sequences as a raw block of data, effectively allowing cart authors to choose how much of the code section to trade for raw binary storage.
Binary strings can be encoded by escaping characters that can't appear in the source code. For example:
0 should become "\000" (or "\0" when not followed by another number), etc. To make this easier, previously invisible characters C1..C15 have font entries, and also unicode replacements when copying and pasting. I'm working on a snippet for converting between data strings and raw binary data, to make this process easier. UPDATE: here's the snippet.
HTML Touch Support under iOS
Touch controls for HTML exports is now a little smoother, and works when running from inside an iFrame (including itch.io game pages). I removed the mobile buttons menu by default (the buttons along the top: fullscreen, sound, close) as they aren't very useful and are messy, but they can be turned back on in the options near the top of the exported html.
Changelog // added 0.2.1b
There are many other bug fixes in this update, but I haven't gotten around to replying to the BBS threads yet. For now, please check the complete changelog:
// Promo video by m7kenji with music by Kyohei Fujita
Shibuya Pixel Art 2020 is accepting submissions until the end of June, and this year there is a new category for 128x128 games! Lexaloffle is sponsoring the game category with a prize (a Picade Cabinet), and by offering a limited number of PICO-8 licenses to participants.
Similar to a game jam, entries should be based on one or more of the following themes: Shibuya, AI, Humanity, Game and/or Landscape. Unlike typical game jams, existing work can be adapted or reused, as long as it did not win a previous contest. You can find previous winning entries for 2018 and 2019.
To enter: simply post the image on twitter or instagram with the hashtag: #shibuyapixelart2020. It is possible to submit more than one entry. For game submissions, post an image of the titlescreen along with a link to the playable game (this BBS / itch.io etc). I suppose for images you'd also want to post a link to the original non-compressed version if needed. Also note that previous years' selections also included gifs/mp4s that also work as still images.
Apart from a Grand Prize (300k yen + a Wacom tablet), there are also 4 special category awards for: Limited Pixel Art, Analogue Pixel Art, Beyond Pixel Art and Pixel Art Game. Winning entries are announced in early August, with an exhibition and awards ceremony in September. But you don't need to be in Japan to enter!
For more information, and to read the full terms & conditions of entries, please visit the official contest homepage: https://pixel-art.jp/ (there is an automatic English translation button near the top of the main content).
UPDATE: I'm not sure if there are restrictions on team projects yet, but will update this thread with any news.

SHIBUYA GIRLS by @yacoyon 2019

生まれ変わる町 by @m7kenji 2019
Back to 2016! This is a demo @castpixel (also on twitter ) and I made in the weeks leading up to Tokyo Demo Fest 2016, as newly formed group: POD. Because it was made in a hurry, I felt I should tidy up the code before posting it. But that's never going to happen, so here's an even messier post-compo version with a few extra details added instead! The rotating orbycube effect can now be found in /demos though if you'd like to see roughly how it works. Also, if you're curious you can find the compo version with: load #orbys_compo

Download 0.2.0i at lexaloffle or via Humble, or for PocketCHIP.
Alright, let's do this! PICO-8's core specification is complete, and it appears to do what it says on the tin. So I'm calling it:
PICO-8 is in Beta!
The main purpose of 0.2 is to finish freezing the core of PICO-8 -- the api, cpu counting, specs, cart format, memory layout, program behavior, backwards and future-compatibility should no longer change.
Earlier attempts at settling on a fixed core in 0.1.11 and 0.1.12 failed because of technical issues creeping in and also some design decisions that just didn't sit right. It has only been due to the ongoing process of users like @Felice, @electricgryphon, @jobe, @freds72, @Eniko, @samhocevar, and many others prodding at the boundary of what PICO-8 can do -- and what it should do -- that all of those nooks and corners finally took shape. I'm really happy with the way the last pieces of PICO-8 have snapped together, and I think it has reached a point where it feels not only like it should never need to change, but that it never could have been any other way.
To make this happen required some jolting changes and a string of patches to get right, and the last few weeks PICO-8 has been in an uncomfortably liquid state. My apologies to everyone who was riding that bumpy update train (but thanks so much for the bug reports!). There might be one or two emergency patches in the next weeks, but I think any left-over quirks and design flaws will simply become part of the machine.
New Features and Changes
Character Set
PICO-8 now has a full 8-bit character set that can be accessed with CHR() to get a character by index, and ORD() to get the index from a character.
> PRINT(ORD("A")) 97 > PRINT(CHR(97)) A |
All characters from 0..255 (0..15 are control characters and are not visible)

All of the new characters 16..255 can now be typed directly into the code editor. There are 3 modes that can be toggled on and off:
- Katakana (ctrl-k) // type in romanji: ka ki ku ke ko
- Hiragana (ctrl-j) // ditto
- Puny Font (ctrl-p) // shift-letter gives you regular font
Additional characters can be accessed in the 2 kana modes with shift-0..9
SFX / Music Organiser
These can be accessed in the music editor, and give you a cart-wide view of all of the patterns or SFXes in a cart. They can be selected by shift-clicking, copied and pasted, or moved around (with ctrl-x, ctrl-v), and can also be used to visualize which SFXes are being used while music is playing.


Operators
Bitwise functions can now instead be expressed with operators. The function versions are still useful if you want nil arguments to default to 0, or just as as matter of style. But the operator versions are a little faster and often more token-efficient.
BAND(A,B) A & B BOR(A,B) A | B BXOR(A,B) A ^^ B SHL(A,B) A << B SHR(A,B) A >> B LSHR(A,B) A >>> B ROTL(A,B) A <<> B ROTR(A,B) A >>< B BNOT(A) ~A |
There's also a handy integer divide, and operators to peek (but not poke)
FLR(A/B) A \ B PEEK(A) @A PEEK2(A) %A PEEK4(A) $A |
Capacity Adjustments
CPU
Bitwise functions (BAND, BOR..) and PEEK functions are now a little more expensive. They can be replaced with operators counterparts to improve speed, but even they are not as fast as the 0.1.11 bitwise functions, especially when used in deeply nested expressions.
This change was necessary because I badly miscalculated how much real-world CPU load would be required to run the most bitwise-heavy carts. Lua functions cost a lot of (real) CPU compared to vm operators, and the result was carts that could completely obliterate a web-browser or real-world CPU on an older machine. This is a problem because a central goal of PICO-8 is to allow authors to forget about real-world CPUs across platforms, and just focus on the PICO-8 one.
Unfortunately, another central goal is to not mess with or break existing carts! So this was a hard choice to make. I've tried to balance this change somewhat with the introduction of native operators, tline(), and by adjusting the vm costs in a way that feels natural but also frees up some extra cycles. Along with bitwise and peek operators, the add and subtract vm instructions now also cost half as much as other vm instructions. So if you consider PICO-8 to be running at 8MHz, they cost 1 cycle per instruction, while most vm instructions cost 2.
CPU: Coroutines
Previous versions of PICO-8 handled CPU counting inside coroutines very badly. It was easy to accidentally (or intentionally) get 'free' cpu cycles when running a coroutine over a frame boundary, and in some versions the opposite could sometimes happen -- a coroutine or garbage collection would incorrectly yield the whole program causing unnecessary frame skipping. 0.2 contains a much cleaner implementation of cpu counting -- you can wrap anything in coresume(cocreate(function() ... end)), and get exactly the same result (minus the overhead of the wrapping). As a nice by-product, this has also made better STOP() / RESUME behaviour possible (see below).
Tokens and Code Compression
There is still a 8192 token limit (of course!), but negative numbers now count as a single token. This seemingly small fix, along with the new character set and bitwise operators, ultimately resulted in the code compression also improving. The result is that you can squeeze in around 10% more code.
If you want to peek behind the curtain, here's the story behind that:
Also, and this is a little embarrassing, I found some unused space in the 32k cartridge format that has been sitting dormant since its creation in 2014. It has been given to the code section, which is now 0x3d00 bytes instead of 0x3c00.
TLINE
The tline() function ("Textured Line") is a mixture of line(), sspr(), and map().
You can use it to draw a line of pixels (same as line()), where each colour is sampled linearly from an arbitrary line on the map. It's not much use out of the box, but can be used as a low-level primitive for many purposes including polygon rendering, DOOM-style floors and walls, sprite rotation, map scaling, drawing gradients, customized gradients and fill pattern schemes. I've only played with it a little bit so far, but it's really fun, and I'm looking forward to seeing what it winds up being used for.
API Changes
RND(TBL)
Give rnd() a table as an argument, and it will return a random item in that table.
BTNP Custom Repeat Delays
From the manual:
Custom delays (in frames @ 30fps) can be set by poking the following memory addresses: POKE(0x5F5C, DELAY) -- set the initial delay before repeating. 255 means never repeat. POKE(0x5F5D, DELAY) -- set the repeating delay. In both cases, 0 can be used for the default behaviour (delays 15 and 4) |
Fill Patterns Constants
Use the glyphs (shift-a..z) with fillp() to get some pre-defined fill patterns.
fillp(★) circfill(64,64,16,0x7) -- transparent white |
They are defined with the transparency bit set. You can use flr(★) or ★\1 to get 2-colour patterns.
fillp(★\1) circfill(64,64,16,0x7c) -- white and blue |

Demo Carts
Most of the demos have been updated, including Jelpi which now has a few more monsters and tilesets to play with! Use INSTALL_DEMOS to get the new versions. 0.2 also features 2 extra pre-installed games: 8 Legs to Love by @bridgs, and Embrace by @TRASEVOL_DOG. You can install them with INSTALL_GAMES.

Tabs and Tabs
Tab characters are now optionally visible (but off by default). You can turn them on in config.txt
Press shift-enter to automatically add an END and indent.
Also, there are 8 more code tabs. Click the right or left-most visible tab to scroll.

Shape Drawing Tools
Both the map and sprite editors now have circle, line, and rectangle drawing tools. Click the tool button to cycle through those 3 modes, and hold ctrl to toggle filled vs. outline circles and rectangles.

Map Tile Moving
It's now a little easier to move sprites around that are referenced by the map. In the map editor, select the sprites you'd like to move, use ctrl-x and ctrl-v to move them, and the map cell data will also be updated to avoid broken references. This operation applies to the selected region on the map (ctrl-a to select half, and ctrl-a again to select the whole map including shared memory).
This operation is a little tricky, because it adds items to both the spritesheet undo stack and the map undo stack, so you need to manually undo both if desired. Back up first!

Splore
Every time you launch a BBS cartridge, PICO-8 will now ping the server to check for a newer version and prompt you to update if it exists. You can turn this off in config.txt
There's also a 'search thread' option in splore's cart menu, which will be useful for long jam-style threads in the future. And is already great for browsing the tweetjam thread! (You can go to the search tab in splore, and search for "thread:tweetjam")

Exporters
HTML
The HTML exports now run a lot smoother on older machines, and with more reliable page formatting and mobile controls.
.zip File Output
A common problem when exporting cross-platform binaries, is that the machine you're generating files from doesn't necessarily support the file attributes needed to run programs on other operating system. This was especially problematic for Mac and Linux binaries exported from Windows, which had no way to store the executable bit (and so end-users would have to manually fix that). To get around this problem, the EXPORT command now produces ready-to-distribute .zip files, that store the needed file attributes when unzipped on any other operating system. As a bonus, you also don't need to bother manually zipping up each platform folder! There's currently no way to add other files (e.g. documentation) though, so in that case you might need to zip the .zip along with any other desired files.
Options menu
Binary exports now come with an OPTIONS menu that shows up when a cart is paused, and includes the same settings available in HTML exports (sound, fullscreen, controls).
Activity Log
Have you ever wondered how much time you've spent in PICO-8 editors or carts? Or which carts you've played the most? 0.2 now logs your activity to activity_log.txt (in the same folder as config.txt) once every 3 seconds (unless the PICO-8 is left idle for 30 seconds). There aren't any tools to process this data yet, but it is human-readable. I should clarify: this information is not transmitted anywhere! You can turn this off in config.txt (record_activity_log 0)
Frame Advance
PICO-8 can now be resumed from exactly the point that code stopped running. For example, if you put a STOP() in your code, and then type RESUME from the commandline, the program will continue as if the STOP() had not occurred. It's possible to type in commands before resuming to modify the state of the program though, which is useful for debugging.
A common debugging tool is to slow a game down and advancing frame by frame. You can do this by stopping suspending a program with escape, and then typing . and pressing enter. This will run the program until the next flip() call and then stop again. You can get subsequent frames in the same way, or just keep pressing enter after the first one. To add additional debugging behaviour, you can use stat(110), which returns true when running in frame-by-frame mode.

That's all for now -- I hope you enjoy 0.2 and I'll catch you soon!
-- zep
Full Changelog: (scroll down to 0.2.0 for the main changes)
Merry Christmas and Happy Holidays everyone!
This is my cartridge for the 2019 Advent Calendar. It is a simple toy/game with no secret endings at all. Nope.
There are 26 (or more! wink wink) carts available now, and you can get the full experience by playing from @enargy's main cart. It is a truly joyful collection!
The BBS's media storage system has recently been updated, and image attachments are now sent to a cloud bucket. Let's stress-test it with some gifs!
Rules:
- No gif, no post!
- No explanation of the gif is required.
- No quality required.
To save these, I used "CONFIG GIF_SCALE 3" from the PICO-8 command prompt, but any size is ok.
Some of these are doodles, some are unfinished carts, some are abandoned projects that will only live on as gifs.




View Older Posts