Log In  

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:


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)

P#111906 2022-05-16 22:38 ( Edited 2022-06-04 19:26)

Cart #imhungry-0 | 2022-04-26 | Code ▽ | Embed ▽ | Forks ▽ | License: CC4-BY-NC-SA

> Vous contrôlez un petit rennes qui doit attraper le plus de nourriture possible, il s'agit de pain et de mûres. Plus vous attrapez de, plus la nourriture va vite et plus il est compliqué de l'attraper. En haut à droite, vous verrez qu'il y a votre tableau de bord, chaque aliment pêché vaut un point. Et en haut à gauche, il y a un panneau qui vous montre combien d'aliments vous n'avez pas attrapés. Attention ! au bout d'une dizaine d'aliments non attrapés vous perdez et le jeu affiche alors « GAME OVER ! », alors il faut appuyer sur enter pour recommencer.

> Les commandes sont : la flèche droite pour se déplacer vers la droite, la flèche gauche pour se déplacer vers la gauche et la flèche vers le haut pour sauter. C'est si simple !

(for more info, see https://itch.io/jam/im-hungry / https://pancelor.itch.io/im-hungry)

P#110904 2022-04-26 21:12 ( Edited 2022-06-01 14:24)

I hit this bug while working on a tweetcart:

?"\*6a"  -- prints 6 'a's    (expected)
?"\*6\"" -- prints 6 quotes  (expected)
?"\*6\n" -- prints 1 newline (unexpected!)
?"\n"    -- prints 1 newline (expected)
P#109613 2022-04-03 02:06

here's a demo cart showing off some different ways to handle input in grid-based games:

Cart #hojohiyomu-1 | 2022-02-24 | Code ▽ | Embed ▽ | Forks ▽ | License: CC4-BY-NC-SA


  • move around with the arrow keys
  • change "chapters" in the pause menu (enter + arrow keys)
  • slow down the game speed (in the later chapters) in the pause menu

I made this cart as a companion to a blog post about input buffering

P#107587 2022-02-24 09:19 ( Edited 2022-02-24 09:55)

@zep o/

A weird bug has been messing with me recently: pico-8 keeps telling me I have "unsaved changes" when I'm pretty sure I don't. I caught the bug on camera this time:

I don't know how to reproduce it; I tried adding a new tab and messing with the text cursor position (since I wondered if this had something to do with the fix for https://www.lexaloffle.com/bbs/?tid=39379 ) and soon after I was able to trigger the bug. But I was able to trigger the bug without even opening up the code editor -- all I did was load cart1 load cart2 load cart1 over and over again until it said "unsaved changes". strange

Jump to 1:12 and 1:24 in the video to see me trigger the bug two separate times. (the video description has a few other timestamps too)

I'm worried I'll stop trusting that message and accidentally lose real changes!

I think this bug is new as of 0.2.4b; I don't remember it happening beforehand.

P#107008 2022-02-16 05:04 ( Edited 2022-02-16 05:09)

Cart #lasal-1 | 2022-01-19 | Code ▽ | Embed ▽ | Forks ▽ | No License

A short celeste map mod. It's built as a bit of a puzzle, meant to teach you one specific thing about the game's mechanics.

This doesn't require any advanced speedrunning tech (spike clips, corner jumps, etc) -- executing the intended solutions should be possible for anyone who's beaten celeste classic once or twice.

If you're trying something that seems too hard or only barely possible, try looking for alternatives!


  • arrow keys / Z / X: move / jump / dash
  • E: toggle screenshake


celeste classic: maddy thorson + noel berry

smalleste: a token-optimized version of classic celeste that I used as a starting point

playtesting: cryss, sharkwithlasers, James, meep

berry follow code: meep's Terra Australis

map editor

I made this map to see how my custom map editor felt to use. I think it's pretty neat -- check it out! It lets you build much larger maps than the built-in pico-8 map editor.

P#105290 2022-01-19 00:23 ( Edited 2022-01-19 06:31)

demo cart

Cart #bigmap_demo-1 | 2022-01-18 | Code ▽ | Embed ▽ | Forks ▽ | No License


The recent 0.2.4 release added support for larger 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.

Larger maps are now possible, but it's difficult to get them into memory -- the built-in map editor only works with vanilla-sized maps.

This cart is a full map editor that makes it easy to make these larger maps!


  • tight iteration loop - play a level in your game, jump into the map editor to make a small tweak, and return to the game with minimal friction
  • change map size at any time
    • max width: 256 tiles
    • max height: none
    • max total size: 32K tiles (e.g. 128*256, or 32*1024)
  • easy copy-paste (right mouse + drag to copy, left mouse to paste)
  • zooming in/out
  • large brushes - place multiple tiles at a time
  • show 16x16 "room" outlines (useful for carts like celeste that are made of many 16x16 rooms)
  • "transparency" - optionally treat sprite 0 in large brushes as "transparent"
  • compressed maps using PX9
  • autosaving
  • the map editor uses 0 of your tokens -- it's a completely separate cart that you only use during development
    (well, it costs ~300 tokens to load the map string and run the decompressor)
  • your game will still be splore-compatible
  • your game can call map(), mget(), tline() etc without any extra work


The editor currently has no undo/redo functionality. That's not ideal! I'm hoping to get it working soon.

To undo all changes since your last save (probably the end of your last session using bigmap), use the "discard changes" button in the top-right.

If you make a large mistake, replace map.p8l with your an autosaved version (inside mygame/autosave/) and reload bigmap.


Setting the bigmap editor up takes a bit of work. This is mainly necessary to enable a tight map iteration loop, and also to work around the restrictions of pico-8 (for example, it's impossible to read a file from disk without user interaction, and asking the user to drag-and-drop their map file every time they wanted to edit is way too much friction for my tastes)

To start, make sure you have a folder (e.g. mygame/) with your game cart inside (e.g. mygame/mygame.p8)

  1. cd mygame (navigate into the folder containing your game)
  2. mkdir autosave (create a folder to store backups/autosaves)
  3. printh("","map.p8l") (this creates an empty map.p8l file)
  4. Save this file (https://gist.github.com/pancelor/f933286f244c6b85b7720dbe6f809143) as px9_decomp.lua (inside the mygame/ directory)
  5. load #bigmap (note: this is different from the bigmap_demo cart)
  6. Uncomment the two #include lines in the second tab (tab 1)
  7. save bigmap.p8
  8. Paste this snippet into mygame.p8: (inside _init(), or at top-level; either works)

    menuitem(1,"▒ edit map",function()
      -- pass spritesheet through upper memory,
      -- avoiding an extra second of load time
      local focusx,focusy=0,0
      poke(0x5500,1,focusx,focusy) --signal
      load("bigmap.p8","discard changes","mygame.p8")

    (make sure you change "mygame.p8" to the actual filename of your game)

    This snippet adds the menu option to enter bigmap while playing your game. If you set focusx and focusy, bigmap will start focused on that map coordinate.

  9. Paste this snippet into mygame.p8 at top-level:
    #include map.p8l
    #include px9_decomp.lua
    if map_import then map_import() end
  10. Save mygame.p8

You should now be good to go!

test+edit iteration loop

  1. Save mygame.p8. any unsaved changes will be lost every time you launch bigmap. (I wish this was avoidable but I couldn't find a way around it that preserved the quick test+edit loop I wanted)
  2. Run mygame.p8
  3. Pause the game (with P or Enter)
  4. Choose "edit map"
  5. Edit your map!
    If you accidentally press escape and exit the map editor, type r or resume into the console to resume the map editor.
  6. Return to your game with P, Enter, or the clickable "Play" button in the top-right

I advise setting up mygame.p8 to jump you to the room you were editing when it starts - this can be done by reading the focusx and focusy global variables that are set inside the map_import() function (inside map.p8l)

See my celeste mod or the getting started video for an example of how to do this.

technical details

When you press the save or play button, bigmap uses printh to write a text file called map.p8l into the current directory. This text file happens to be valid lua code that defines a function called map_import(), so when mygame.p8 executes #include map.p8l and map_import(), it runs the code generated by bigmap.

Here's an example of what map.p8l looks like:

-- this file was auto-generated by bigmap.p8
function map_import()
 local function vget(x,y) return @(0x8000+x+y*mapw) end
 local function vset(x,y,v) return poke(0x8000+x+y*mapw,v) end
 px9_sdecomp(0,0,vget,vset,"◝◝◝ユ◝7な◝◝✽,ゃf\0★)&るちP;](♥KねF ... many many more chars here ... X▤ミヘラ⬇️⬇️Bれん")

This has 3 main parts:

  1. Set up some helpful global vars - mapw and maph are the width and height of the map, in tiles. focusx and focusy are the tile coordinate of the tile that was in the center of the screen when map.p8l was saved -- you can use this info to jump directly to that room to let you make small tweaks and test them very quickly
  2. poke(0x5f56,0x80,mapw) -- this tells pico-8 to use mapdata stored at 0x8000, with map width mapw
  3. The rest of the function uses PX9 to decompress the binary data stored in that long string. The decompressed data gets stored starting at 0x8000 and takes up mapw*maph bytes.

That compressed data string is created using PX9 and this snippet for encoding binary data in strings.


stuff I used:

alternative map editors you might consider using instead:


happy map editing!

I'd like to see what you make -- let me know if you use this in your projects! And if you want to credit me I'd appreciate it :)

Is bigmap helpful? Is it too confusing to set up? Find any bugs? Let me know what you think!

P#105301 2022-01-19 00:12 ( Edited 2022-03-10 00:04)

what is this?

The wiki is nice for checking exactly how time-expensive various operations are, but it's a bit out of date. Also, it'd be nice to just be able to directly test two implementations against each other, rather than adding up how long each individual operation takes.

The wiki also has a code listing for a cpu profiler, but it's a bit hard to find if you don't know it exists. Plus, it was fun for me to dive in and double-check the math myself.

My profiler is pretty similar to the one on the wiki, although IMO mine has a nicer/simpler interface. Additionally, I've commented exactly how the cycle calculation works, which might be useful for other people to see:

-- slightly simplified from the version in the cart
function profile_one(func)
  local n = 0x1000

  -- n must be larger than 256, or m will overflow

  -- we want to type
  --   local m = 0x80_0000/n
  -- but 8𝘮𝘩z is too large a number to handle in pico-8,
  -- so we do (0x80_0000>>16)/(n>>16) instead
  -- (n is always an integer, so n>>16 won't lose any bits)
  local m = 0x80/(n>>16)

  local function cycles(t0,t1,t2) return (t0+t2-2*t1)*m/30 end
  -- given three timestamps (pre-calibration, middle, post-measurement),
  --   calculate how many more 𝘤𝘱𝘶 cycles func() took compared to nop()
  -- derivation:
  --   𝘵 := ((t2-t1)-(t1-t0))/n (frames)
  --     this is the extra time for each func call, compared to nop
  --     this is measured in #-of-frames (at 30fps) -- it will be a small fraction for most ops
  --   𝘧 := 1/30 (seconds/frame)
  --     this is just the framerate that the tests run at, not the framerate of your game
  --     can get this programmatically with stat(8) if you really wanted to
  --   𝘮 := 256*256*128 = 8𝘮𝘩z (cycles/second)
  --     (𝘱𝘪𝘤𝘰-8 runs at 8𝘮𝘩z; source: https://www.lexaloffle.com/bbs/?tid=37695)
  --   cycles := 𝘵 frames * 𝘧 seconds/frame * 𝘮 cycles/second
  -- optimization / working around pico-8's fixed point numbers:
  --   𝘵2 := 𝘵*n = (t2-t1)-(t1-t0)
  --   𝘮2 := 𝘮/n := m (e.g. when n is 0x1000, m is 0x800)
  --   cycles := 𝘵2*𝘮2*𝘧

  -- calibrate, then measure
  local nop=function() end -- this must be local, because func is local
  local atot,asys=stat(1),stat(2)
  for i=1,n do nop() end
  local btot,bsys=stat(1),stat(2)
  for i=1,n do func() end
  local ctot,csys=stat(1),stat(2)

  -- report
  local lua=cycles(atot-asys,btot-bsys,ctot-csys)
  local sys=cycles(asys,bsys,csys)
  local tot=lua+sys
  return {

how do I use it?

You can try it here online, but to really use it you'll want to download it yourself and edit the body of the analyze() function. There are instructions embedded in the cart with more details:

Cart #cyclecounter-2 | 2022-01-16 | Code ▽ | Embed ▽ | Forks ▽ | License: CC4-BY-NC-SA

misc results

poke4 v. memcopy

  profile("memcpy     ", function() memcpy(0,0x200,64)       end)
  profile("poke4/poke4", function() poke4(0,peek4(0x200,16)) end)

> memcpy : 7 +64 = 71 (lua+sys)
> poke4/poke4 : 7 +60 = 67 (lua+sys)

Copying 64 bytes of memory is very slightly faster if you use poke4 instead of memcpy -- interesting!
(iirc this is true for other data sizes... find out for yourself for sure by downloading and running the cart!)

edit: this has changed in 0.2.4b! the memcpy in this example now takes 7 +32 cycles

constant folding

I thought lua code was not optimized by the lua compiler/JIT at all, but it turns out there are some very specific optimizations it will do.

  profile("     +", function() return 2+2 end)
  profile("   +++", function() return 2+2+2+2+2+2+2+2 end)

These functions both take a single cycle! That long addition gets optimized by lua, apparently. @luchak found these explanations:

> Since Lua often compiles source code into byte code on the fly, it is designed to be a fast single-pass compiler. It does do some constant folding

A No Frills Introduction to Lua 5.1 VM Instructions (book)
> As of Lua 5.1, the parser and code generator can perform limited constant expression folding or evaluation. Constant folding only works for binary arithmetic operators and the unary minus operator (UNM, which will be covered next.) There is no equivalent optimization for relational, boolean or string operators.

constant folding...?

One further test case:

  profile("tail add x3", function() local a=2 return 2+2+2+2+2+2+2+a end)
  profile("head add x3", function() local a=2 return a+2+2+2+2+2+2+2 end)

> tail add x3 : 2 + 0 = 2 (lua+sys)
> head add x3 : 8 + 0 = 8 (lua+sys)

These cost different amounts! Constant-folding only seems to work at the start of expressions. (This is all highly impractical code anyway, but it's fun to dig in and figure out this sort of thing)

update the wiki?

I have not updated the CPU page on the wiki; it's a bit hard to pin down exactly which operations take cycles, and I would personally rather use a tool like this to compare two potential implementations.

But, just so you're aware, the wiki is definitely out of date; when I ran the wiki's cpu profiler on pico-8 0.2.4, it produced different results. (I put a summary of the raw differences here)

edit: thisismypassword updated the wiki -- thank you!


Cart by pancelor.

Thanks to @samhocevar for the initial snippet that I used as a basis for this profiler!

Thanks to @freds72 and @luchak for discussing an earlier version of this with me!



  • added: press X to copy to clipboard
  • added: can pass args; e.g. profile("lerp", lerp, {args={1,4,0.3}})


  • intial release
P#104795 2022-01-11 03:31 ( Edited 2022-08-13 22:46)

Cart #linecook-2 | 2022-06-01 | Code ▽ | Embed ▽ | Forks ▽ | No License

these busy birds will eat almost anything that falls into their gullet -- what will you feed them? they have their preferences, but people food beats bird food any day of the week!

a difficult, chaotic arcade game. now with local multiplayer support! also available on itch.


  • left / right: move
  • x / up: grab
  • ESDF: movement keys for player 2


  • 4 difficulty modes: "easy", medium, hard, and practice
  • 3 different control schemes:
    • solo
    • local multiplayer
    • two-handed singleplayer
  • 4 challenging maps for 4 different flavors of gameplay


this was made for the #ChainLetterJam! Patrick nominated me; I had fun playing Arithmetic Bounce (my high score on hard mode is 24), so I decided to try making a game that would feel similarly chaotic.

I took inspiration from the fact that none of the target numbers in Arithmetic Bounce were inherently good or bad; their value changed depending on the current goal. This became the ingredient/recipe idea in linecook: specific ingredients are sometimes good and sometimes bad, depending on the current recipe. I also liked the chaos and time pressure caused by gravity in Arithmetic Bounce; I've gone for a slightly different but still chaotic spin by giving the player two grabber-claws that are tricky to aim.

the continuation of this chain is: https://tallywinkle.itch.io/the-witchs-almanac


I wrote about this game's design here. tl;dr: if you allow your game systems to play out as physical processes in the game world, there's a lot more opportunities for the player to interrupt and cause surprising interactions

hope you enjoy it!

P#103294 2021-12-22 02:03 ( Edited 2022-06-01 19:37)

0.2.4 has been released, and we now have an extra segment of memory to play with from 0x8000 to 0xffff. That's 32K, or 0x8000 bytes. A spritesheet takes up 0x2000 bytes... so we could stuff 4 extra spritesheets in there!

I've created a system where you can call my custom function cspr the same way you would normally call spr, and everything "just works". The difference is, cspr can handle up to 1024 sprites instead of the standard 256-sprite-limit of spr. (also, cspr is a bit slower (but not much!) than spr, because it has to manage a cache)

Here's a demo that uses 4 full spritesheets; search the code for "cspr" to see how easy it is to use, once you've set it up!

Cart #hefafanino-4 | 2021-12-23 | Code ▽ | Embed ▽ | Forks ▽ | License: CC4-BY-NC-SA

(the games are all by me: #linecook, #remains, #ocelotsafari, and #escalatorworld)

technical details

The core ideas are the blit and cspr functions:

  • blit: this is used to move sprites between the 4 upper spritesheets and the sprite cache (located at 0x0000, where the spritesheet normally is)
  • cspr: cspr_simple gets the core idea of the sprite caching across; the full cspr in the cart can deal with any arguments you would normally pass to spr (like the width/height/flip parameters)

These are the simple/readable versions; the versions in the cart have some optimizations such as replacing y0*64 with y0<<6 that may make it more difficult to follow:

-- copies a sprite from x0,y0
--   (from spritesheet based at
--   memory address base0)
--   into x1,y1 on the spritesheet
--   based at address base1
-- set base1 to 0x6000 to use the
--   screen as the destination
-- all coordinates are measured
--   in pixels
-- note! odd x-coordinates will
--   be rounded down
-- by pancelor
function blit(base1,x1,y1,base0,x0,y0, w,h)
 local a0=base0+y0*64+x0\2 --source
 local a1=base1+y1*64+x1\2 --destination
 local w2=w and w\2 or 4   --half-width
 for da=0,(h or 8)*64-1,64 do

-- "cached sprite" by pancelor
--  uses a direct-mapped cache
--  up to 4 spritesheets are
--   stored in 0x8000+
--  they are numbered 0-1023
--  using any sprite s will write
--   it to spritesheet slot s%256
--   and then use it from there
_cspr_bank={} -- maps slots (0-255) to which bank the sprite comes from (0-3)
function cspr_simple(sbig,x,y)
 local bank,s=sbig>>8&3,sbig&0xff
 -- bank is 0-3, sbig is 0-255 (inclusive)
 if _cspr_bank[s]~=bank then
  -- cache miss!
  -- blit sprite s from its bank into the cache:
  local sx,sy=s%16*8,s\16*8


An earlier version of this demo used something called upspr instead of cspr and had many drawbacks (do load #hefafanino-1 in your local pico-8 to see that old version). I've updated the demo cart to a way better version that "just works", using cspr.

  1. sprites can only be blitted on even x-pixel values. e.g., upspr(290,11,11) will draw sprite 290 to 10,11 instead
  2. you can't store anything inside upper memory until runtime, so you'll probably want to use PX9 or something similar to store your extra spritesheets (as code strings? inside 0x0000-0x2000?) and then decompress them at startup
  3. flipping sprites takes extra work
  4. palette and transparency will not be respected
  5. sprite editing is more difficult (due to 2)
  6. the upper memory is being used by spritesheets, which may get in the way of other uses for the upper memory (such as larger maps). But you don't need to completely fill the upper memory with spritesheets; you could have, say, 512 sprites (2x normal) and still have 0x4000 bytes leftover for a 128x128 map. we've got more room for either, but we still have to make a tradeoff between the two!
  7. map() no longer works -- you'll need to write your own version of map() that calls spr() repeatedly. luckily this is about as fast as calling map() directly. note that mget() and mset() will need to be rewritten too, because they only handle 1-byte entries.

extension ideas:

  1. if you stored all of your sprites in upper memory and used the spritesheet at 0x0000 as a cache for the sprites you're actively using, you can fix drawbacks 1, 3, and 4 (above) "for free". See "Virtual Sprites" in @freds72's POOM devlog for an idea of how to do this
    • Done! I used a direct-mapping cache instead of an LRU cache, which might cause performance problems if you repeatedly draw sprite x and then sprite x+256*n (because those sprites both map to the same slot, x). For example, sprites 17, 256+17,512+17, and 768+17 all get stored in slot 17 of my direct-mapped sprite cache.
    • However, cspr/blit is quite fast, so you might not even see performance problems. (see my next post for performance details)


Someone on discord asked: "should people draw all sprites from bank 1, then swap for bank 2, etc?"

My recommendation: That would help, but I don't recommend it -- there are more effective ways to improve the performance, I think:

  • If you're okay with needing to manage which sprite banks are currently loaded, you could avoid all of the overhead of calling cspr() and just call spr() directly. This lets you use map(), too. You would of course need to manually memcpy the sprite banks into the 0x0000 region when appropriate.
  • If you want better performance but want a low-maintenance cache that's easy to use, you should probably write an LRU cache instead of my simple direct-mapped cache. (I may do this myself soon)

is cspr good enough to just use?

I think so! it's fast enough; it uses up to 10~15% of a frame (in my limited testing) and you get 4x the sprite space, without needing to think about the cache at all. An LRU cache might make this number way better, but I haven't tried that yet.

Keep in mind that map() does not work with cspr -- this may be a dealbreaker for some. (you'll need to roll your own implementation of map/mget/mset)

If you are willing to give up the "without needing to think about the cache at all" requirement, you should maybe manually move pages of sprites (128x32? 128x64? 128x128?) around instead -- it'd be mostly pretty simple, and very token-efficient. (thanks to merwok for the suggestion!)

P#103168 2021-12-20 12:38 ( Edited 2021-12-23 08:37)

I've hit a really bizarre error; my code has no coroutines, but it fails with "attempt to yield from outside a coroutine"

I've simplified it down as much as I could; while removing unrelated cruft (e.g. the sort function from my new project template that I wasn't actually using anywhere) the bug would arbitrarily appear or disappear.

The bug will either get triggered 100% of the time or 0% of the time when you run the cart, but its presence or disappearance arbitrarily changes depending on what other unrelated code there is in the cart.

For example, tab 0 is 15K of function foo() end repeated over and over again. They're commented out right now, and the bug is present. If you comment them back in, the bug disappears.

Commenting out the body of pqb (which prints many many u64 objects) seems to remove the bug for good (no matter how many foos I comment in or out). I'm not too experienced with metatables; I think I may be at fault for something I'm doing inside u64.__tostring? sometime variation of this code give a slightly differently-formatted error message that jumps me into the middle of my __tostring method. or maybe printh is having issues dumping so much text to the console? I dunno

I'm using pico-8 0.2.4 for windows 7.

Cart #pbomdme-0 | 2021-12-12 | Code ▽ | Embed ▽ | Forks ▽ | License: CC4-BY-NC-SA

the more common error screen:

the more rare error screen (can be triggered if there are exactly 144 foos in tab 0)

P#102444 2021-12-12 04:06

This is #bubblecat-0

  • It doesn't reload properly (launch it and then press ctrl-R; an "attempt to call a string value" error message appears, and is very different from the normal error messages. pico-8 is unresponsive after this)
  • It does reload fine through the menu (press P; choose "reset cart")

Cart #bubblecat-0 | 2021-11-20 | Code ▽ | Embed ▽ | Forks ▽ | No License

This is #bubblecat-2

  • It reloads just fine (using both methods)
  • However, reloading with ctrl-R causes a strange "loaded external changes" message

Cart #bubblecat-2 | 2021-11-20 | Code ▽ | Embed ▽ | Forks ▽ | No License

The difference between the two carts is a single newline between the end of the __lua__ section and the __gfx__ section:

These errors only happen in the web player or exported binaries; they do not happen inside pico8.exe.

I'm 90% sure I created #bubblecat inside pico8 itself; I didn't edit bubblecat.p8 using my external text editor.

Expected behavior:

  • a cart without a final newline in the last tab should work fine in the web player and in binary exports
  • no "loaded external changes" message should be shown when reloading

I'm running pico8 0.2.4; I noticed this issue on 0.2.3 but I believe the web player is running 0.2.4, and I confirmed the issue is present in binary exports for windows from 0.2.4

P#101396 2021-12-04 01:10 ( Edited 2021-12-04 01:18)

Cart #bubblecat-2 | 2021-11-20 | Code ▽ | Embed ▽ | Forks ▽ | No License

welcome back, bubble cat. we have another situation, and this time you've only got 60 seconds

how to play:

  • arrow keys: move
  • ctrl-m: mute
  • X/Z: continue to next level

you don't have to full-clear every level! skipping a level without clearing it just gives you a points penalty (-5 per remaining bubble)


this game was made to fit inside two tweets; i.e. <560 characters of code and no sprites! here's the full code:

x=3y=3o={}m=0n=0p=circfill::_::z={}for j=1,13do
while t()<60do?"⁶1⁶c"
if(n~=m)m+=d d+=6
if(not r)r,s=s
for j=#z,1,-1do
i=z[j]d=q and"-5"b=i\7*9a=i%7*9p(a+4,b+4,3,j|8)
for a in all(o)do
end?q or""
if(q)goto _
::e::goto e

(that's only 548 characters, but some of them (like the cat face) cost two characters on twitter. remember to ctrl-p to enter puny-text mode before pasting this into your local console!)

some code highlights:

  • convert btnp() bitfield into movement: b=btnp()if(b>0)s=b*.5938&.75if(r)x+=cos(r)y+=sin(r)
  • buffered input: if(not r)r,s=s
  • out-of-bounds check: w=x\7+y\7~=0
  • animated score display: d=sgn(n-m) if(n~=m)m+=d
  • collision checking: if(y*7+x-x\7==i)
  • draw player: ?"★⁵8d🐱",x*9+1,y*9+3,7
  • animated score floaters: for a in all(o)do a[3]-=1?unpack(a) end

full code history:


high score:

my high score is 207! 240! what's yours?

P#100464 2021-11-20 03:23 ( Edited 2022-01-02 20:31)

I was messing around with the new p8scii memset command, and it sometimes crashes my console at weird times. once, it crashed when I did this:


(i.e. using memset with an address but no actual arguments)

Another time I did a similar command and then did "reboot", and it crashed then

system info: pico-8 0.2.3 / windows 7

the error message:
> Microsoft Visual C++ Runtime Library
> This application has requested the Runtime to terminate it in an unusual way.
> Please contact the application's support team for more information.

P#99291 2021-10-28 23:27

Cart #firroref-0 | 2021-10-15 | Code ▽ | Embed ▽ | Forks ▽ | License: CC4-BY-NC-SA

Here's some code:

poke(0x5f5e,0x11) --only enable bitplane 1
sset(0,0,15) --edit sprite 0

I would expect the sset() call to set the spritesheet's corner to color 1 (dark blue) because of the bitplane setting. However, this instead sets the corner to 15 (tan).

I can workaround this for now by using pset and then memcopying the screen to the spritesheet, but that means my decompression code (which wants to call sset with bitplanes active) will need to either take less than a frame to run, or show artifacts onscreen while it runs.

P#98668 2021-10-15 01:19

I just found out that you can turn a specific sprite into the icon for the binary export: https://www.lexaloffle.com/dl/docs/pico-8_manual.html#Binary_Applications_ and whoa, this is really great! But afaict it's restricted to the standard palette. Is there some way to set a custom palette for the icon in the export? If not, adding some sort of palette flag might be a nice feature:

EXPORT -I 32 -C 12 -Z 0,132,4,140,134,6,135,7,8,137,139,11,138,130,13,131 MYGAME.BIN

or maybe reuse the -C flag, and a -1 entry means transparent?

EXPORT -I 32 -C 0,132,4,140,134,6,135,7,8,137,139,11,-1,130,13,131 MYGAME.BIN

(although that might be a bit awkward because using pal(12,-1,1) ingame means to map 12->0x8F, not 12->transparent...)

P#98352 2021-10-07 20:06

Cart #freecell1k-0 | 2021-09-28 | Code ▽ | Embed ▽ | Forks ▽ | License: CC4-BY-NC-SA

Free Cell, in 1022 characters of code, and no sprites! twitter | itch


  • click and drag cards around
    • you cannot move stacks of cards; one at a time only!
  • stack descending cards of alternating colors in the main play area
    • any card may be placed in an empty column of the main area
  • store any card in the four "free cells" at the top left
  • make a stack of each suit A->K in the top right to win
  • reset the cart to start a new game


You can only move one card at a time; if you want to move a stack of cards you have to take it apart and put it back together manually. This is different from "standard" solitaire, and it makes Free Cell particularly interesting! It also makes the implementation a bit easier to fit into the tiny code-size constraint ;)


Congrats! Enjoy the win animation here: https://pancelor.itch.io/solitaire-win-animation (I wanted to add a "you win" animation to this game, but I didn't have the room to fit it in... so I made it as a separate cartridge)


The source is in the cart, of course, but it's here too. Remember to enable puny text mode (cmd-p) before pasting this into your local PICO-8 console:

function F(i)S=s[i]J=i\W I=i\8O=1-I U=I*(i-8+J)*14+O*i*Z+2V=O*max(#S*6+22,28)+5end
for i=0,51do s[i]={m=i\W}A(B,{x=i,y=400,k=i+i\w*3},rnd(i+1)+1)end
q(-15-😐,264,2043,4,3843)D=rectfill::_::L=T%8T+=1K=B[T]N=not btn(5)X=stat(32)-6Y=stat(33)-8C=fillp
for i=15,0,-1do
C(▒)a=5+O*28D(U,a,U+W,a+Z,2)C()for _ENV in all(S)do x=u+3*x+.5>>2y=v+3*y+.5>>2end
if(H and N and(J+G+#S<1or Z-I|k+G==Z|1+H.k^^32or H.k==J+k|G))K=H L=i
for r in all(B)do
q(63-😐,244)D(x-1,y-1,x+W,y+Z,4)q(61-😐,-1,-1)D(x,y,x+W,y+Z,3)a=r.k%Z+1?(a==10and"³f|³f0 ³b"or sub("a23456789|jqk",a,a).." ³d")..split("♥,◆,◆⁵8f..³aᶜ3.,◆⁵8fニ")[r.k\Z+1],x+1,y+1,r.k\32
for i=0,77do
if(M< Q/⧗)Q=0?"⁷ceg4"
goto _

(there's an extra space in there near the end because the BBS text editor seems to choke on the < symbol)

An earlier, more readable, and much longer version of this code can be found here


Some of the more bizarre tricks I used to squeeze every bit of functionality out of my 1024-character budget:

  • set the palette with poke2(-15-😐,264,2043,4,3843)
  • update card positions with for _ENV in all(S)do x=u+3*x+.5>>2y=v+3*y+.5>>2end
  • dynamically cast shadows with a very particular palette and poke2(63-😐,244)rectfill(x-1,y-1,x+W,y+Z,4)poke2(61-😐,-1,-1)
  • draw the 4 suit icons with split("♥,◆,◆⁵8f..³aᶜ3.,◆⁵8fニ")[suit_id]
  • check if you can drop your held card with i\12+G+#S<1or 16-i\8|k+G==16|1+H.k^^32or H.k==i\12+k|G
  • wait until the next frame and clear the screen with ?"⁶1⁶c6"

    • (thanks to zep for pointing out that \^ can be written as ⁶!)
  • auto-move cards to the top right by tracking the minimum stack height with bitshifting (search for M (and m) to see the relevant code)
  • check whether the game is won by taking advantage of the fact that 2^12<⧗ and ⧗<2^13

I'M SORRY, "poke2(63-😐,244)"???

Yeah! 63-😐 is 24414.5, which is the address I need to poke to get those slick shadows! Check out this post for more info.

P#97939 2021-09-28 22:10 ( Edited 2021-09-30 04:04)

Cart #constantcompanion-8 | 2022-09-02 | Code ▽ | Embed ▽ | Forks ▽ | License: CC4-BY-NC-SA


This is a niche tool to help you save characters when writing carts that are codesize-constrained.

  • type in a number; press enter
  • then press up/down and ctrl-c/enter to copy a code
  • navigate back up (or press backspace) to start typing a new number

Example: 0x6000 can be written as 0x6000 (6 chars), 24576 (5 chars), 6^13 (4 chars) or ⌂-🐱 (3 chars!)

The tool sorts the results by character count (on twitter), so the top results are your best bet.

Update: The latest versions of pico8 have special P8SCII codes for poking which are often fewer characters than calling poke directly (even after using this tool). e.g. ?"\^!5f10249?" instead of poke(0x5f10,50,52,57,63). Consider using that instead! But this tool might still be useful sometimes, and at the very least, it's an interesting artifact.


While I was making Free Cell 1K (itch | twitter | bbs) for the #Pico1K jam, I realized I could save characters by taking advantage of the built-in constants. I needed to poke a few things to various addresses (for setting the palette, drawing dynamic shadows with bitplanes, and enabling the mouse) so I had code that looked like this:

-- 94 chars (not including comments or newlines)
poke(0x5F10,8,1,251,7,4,0,3,15) --palette
poke(0x5F2D,3) --mouse
poke(0x5F5C,-1) --disable btnp repeat
poke(0x5F5E,0xF4) --shadows on
poke(0x5F5E,0xFF) --shadows off

Each of those poke addresses uses 6 characters; we can do better! The first thing I did to save characters was this:

-- 84 chars (not including comments or newlines)

But then I saw @zep tweet a trick for writing sqrt(x) as x^█ instead (because █ (shift+A) is defined to have a value of 0.5), and I realized I could do even better: the 😐 (shift+M) character is defined to have a value of -24351.5, so I did this:

-- 95 chars (not including comments or newlines)

95 chars is not an improvement... yet! However, poke() ignores fractional addresses, so those .5s aren't necessary. Also, I was able to combine the 0x5F5C and 0x5F5E pokes into a single poke, which nullifies some of the relative advantage of the A=24365 technique. In the end, this was the shortest code I could find:

-- 64 chars (not including comments or newlines)

This saves 2 characters over the equivalent version that uses A=24365 instead of the moon face. (or maybe just 1 character, if the newline after q=poke2 can't be removed)

how did you know moon face was the one to use?!

I didn't! I wrote a program to brute-force try all the built-in symbols and see if any were useful for my needs. Check out tab 5 of the cart ("analysis") to see the brute-force algorithm I used.

I've cleaned that program up and posted it here for you. It might be less useful for tweetcarts (because stuff like 😐 takes up 2 characters on twitter) but it saved me 1~2 entire characters (genuinely very helpful!) during the Pico1K jam, and I hope it helps you too.

Leave a message here or tag me on twitter if you found it useful; I'd like to see what you make!

how does it work?

  1. build a list of all 150 "symbols" under consideration:
    • █▒🐱░✽●♥☉웃⌂😐♪◆…★⧗ˇ∧▤▥ (most symbols)
    • the numbers 0-9
    • and the negated version of everything above
    • the numbers 10-99 (but not their negated versions)
  2. try combining every pair of symbols a,b with these operations:
  3. if any result is between target and target+1, display the result. this was chosen because poke/memset/etc round their inputs down to the nearest integer. custom checks are easy to hack in yourself; search for "function near" in the code


  • v8:
    • add support for new pico-8 syntax a~b for bitwise not (1 char shorter than the old a^^b)
    • sort results by twitter character count instead of naive character count. thanks to jadelombax for the handy reference table here: https://www.lexaloffle.com/bbs/?tid=44375
  • v7: fix typo in v6
  • v6:
    • fix exponentiation parse order (-a^b gets parsed by pico8 as -(a^b), unlike any other operation on the list)
    • increase list scroll speed
    • clean up output list results a bit (e.g. no more -a+b, since b-a is shorter)
    • remove ~x from consideration; it's very similar mathematically to -x, so calculating it was making all searches slower for hardly any benefit. (the code is commented out, so you can re-add it yourself pretty easily if you download the cart and search for ~)
    • make it easier to do custom "is close enough" checking; search for "function near" in the code
  • v5: show options (and allow them to be copied) as soon as they're found. (no longer need to wait for the entire search to complete)
P#97937 2021-09-28 21:48 ( Edited 2022-09-02 22:49)

Cart #ocelotsafari-0 | 2021-04-29 | Code ▽ | Embed ▽ | Forks ▽ | No License

welcome to the ocelot safari!

enjoy the ocelots, and do let us know if you find any long-lost relics deep in the jungle :)


  • hold Z to drag things
  • arrow keys to move
  • retrieve the lost gemstone of Tezcatlipoca! some say it’s as far as fvkgl-sbhe meters deep in the jungle!


  • we'll leave you some new tools at the initial drop point, if the ocelots steal your gear
  • ocelots can crawl through vines and trees -- they're tricksters!
  • be sure to bring some matches; the nights are long and dark, and who knows what lurks in the jungle...


I didn't make the time to make an interactive tutorial, so here's a video instead:

And here's a gif showing how to use each tool:

(light a fire by bumping matches into wood)

good luck in there!


  • This was made for the Ludum Dare 48 compo in 48 hours. (plus minor bugfixes; read changelog here and here). Rate my entry! https://ldjam.com/events/ludum-dare/48/ocelot-safari
  • My initial goal was to make a game exploring how items feel if you have no inventory system or "use item" button, and I'm happy with the results. Sometimes movement can feel a bit awkward, especially at the start, but that's what the whole game is built around, so I think it's fine.
  • I really like how the nighttime and the ocelots make it a visceral struggle to advance deeper and deeper into the jungle.
P#91318 2021-04-29 22:01

In the 0.2.2 release, zep made a small tool for creating custom fonts:
https://www.lexaloffle.com/bbs/?tid=41544 (search for "Custom Fonts")

It's a great tool, but if you only have a font snippet (and no longer have the spritesheet), it's hard to edit that font again. So, I extended the tool to also support font importing!

You can get my font tool from the pico-8 console:


Instructions on how to use the tool are in tab 0 of the cart. happy fonting!

P#90878 2021-04-21 09:29

View Older Posts
Follow Lexaloffle:        
Generated 2022-10-04 07:55:54 | 0.092s | Q:79