Log In  


Cart #digable_planetoids-0 | 2026-09-23 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
32

Another autumn, another PICO-1k Jam. I love this jam so much ~ the 1024 byte .p8.rom size limit hits a sweet spot where every little design and code choice carries so much weight, and you end up with so many overlapping meanings, but still with a sense of broad possibility. For me the jam is like a tiny hyper PICO-8 inside PICO-8 that always carries me along to new places. Thanks @Liquidream for hosting and greetings to everyone taking part! You can find entries on the jam's itch page or on the BBS via the pico1k tag. And if you'd like to join, there is still just under a week left of September!

Instructions

  • CTRL-R to restart (or ENTER to select reset from the pause menu)
  • [O] (Z/C) to jump and use jetpack
  • [X] to dig (LEFT and RIGHT while standing only!)
  • [X] + [DOWN] to drop an explosive (falls through topsoil)
  • Collect power gems for score (and +3 pixels of energy!)
  • Watch out for: falling sand, lava, and depleted energy
  • Fly up to warp to a new planet at any time.
  • There is no win state; perish with the glory of many gems collected.

It's up to you to decide if each gem is worth trying to collect, if each nook of a cave is worth exploring, and when it is worth flying up to warp to a new planet. Using the jetpack and digging use energy at the same rate (around 1 pixel of energy per second), and dropping explosives costs half a pixel of energy each. Running out of energy means instant doom and is a dishonorable way to die, comparable to falling into a tiny pool of lava. Good luck!

How it Works

// note: click "Code ▽" under the playable cartridge above to view commented src code!

This jam I wanted to see how far I could get with a procedurally generated world. In the past I've experimented with hand-designed coarse structures that are embellished with hard-coded details (Squiddy) and also using P8SCII for finer control over single tiles (Blue and Lefty). The problem with forgoing hand-designed elements with such a small code size is that it is very easy to end up with 10000 bowls of oatmeal. You really want generated structures to have a visual hierarchy, to play different roles at different scales, and for those structures to meaningfully interact with the game rules.

World Generation

The key discovery I made for this to happen in 1k was that the generation and world simulation could be closely intertwined. All of the world state (apart from the player) is encoded as pixel data in the spritesheet; things like falling explosives and blast sparkles along with the more obvious falling sand and lava, are all driven by tile-based rules (a variety of Cellular Automata). So the world generation is a combination of two passes: an initial state that the player never actually sees, and the result of running the core game rules (sand piles up, lava-falls and rivers form).

The world is generated by starting with horizontal bands of the desired tile indexes (with a layer of zero for caves), and then distorting them by drawing many circles using the colour at the center of the circle before it is drawn. Finally, pockets of sand and lava are added along with the collectable power gems. The sand is index 7 (white) and lava is index 3 (green):

The size of circles drawn, and the number of circles can be used to control the general lumpiness and scale of distortion. Both of these factors are derived from the planet seed sd:

-- ye olde tweetcart decay effect
for i=1,sd do
   x,y=rnd(128),33+rnd(88)
   c(x,y,y/(18+sd%18),pget(x,y))
end

After the planet finishes generating you can see the lava and sand (near the player x position) forming piles and rivers. The indexes of the three special tiles (1 gems, 3 lava, 7 sand) are chosen to correspond with the desired range of pocket sizes, and so that pass is simply:

for x=2,222 do -- range chosen to compress well
    k=rnd{1,3,7} -- gem,lava,sand
    circfill(x,120-rnd(7)^2,rnd(k),k)  --  rnd(7)^2 to spawn mostly deeper
end

The first row of sprites is used as graphics, and the rest of it as 4-bit tile indexes into that row (the map memory is not used at all). This allows using gfx operations like circfill() to draw the world, P8SCII to generate the sprite gfx, and then memcpy()ing from 0x6000 (display) to 0x0 (spritesheet) once each time a planet is entered. This means that if flying up to new planets is disabled, you can jetpack up and fly around the spritesheet:

I was unable to hide the few frames of world generation drawn to the screen without spending many compressed bytes, so leaned into it and treated it as an inter-planet warp effect. It is still quite jarring, but it helped to design the starting sound to complement that, and to throw the player into the world with a large dx,dy (starting values that also compressed better, as it turned out).

Sprite Generation

The gfx is drawn using P8SCII print statements, with the starting warp sound tacked on to the end with the \a control character:

?"c0-b  a◆●\#89-_\-hb\#02o"..sub(sd,1,3)..",'d\#3o!"..sub(sd,-2).."ˇ\#42-o.`\#f9▒\#6d◝◜◝░a-o\#0░░\0"
?"\|je|\-f|\-h,\|j|js0hg9\*2:0000000303030000gc\*2:3c3e3e3e3c3c0000gdod6b - -\ax6[cegb]<cegb]<cegb"

The display colour for 10 (yellow) is updated every frame to a rainbow cycle, and colours 3 (topsoil) and 11 (background) are also remapped based on the planet seed.

The planet seed is always in the range 11111..32222 and is used to define (among other things) both the background tile (4) and topsoil texture (5) using outline bitfields. The sub() function is grabbing the first 3 character, and last 2 character for background and topsoil tiles respectively. The background tile colours should always be quite dark, so the range of the seed is chosen so that the first character is always either 1 (blue) or 2 (dark purple), and occasionally 3 (dark green). This also fits nicely into the signed int16 range (maximum: 32767).

Hold up... what's going on with this guy's long leg?

The legs are made out of a comma and pipe | characters hidden by the body. But in the last frame, the body does not hide the top half of the |, so I shifted it two pixels down and it gets clipped out by spr().

Automaton Rules

The rules are applied to 25% of tiles near the player each frame, so each visible tile updates at ~7.5 frames per second:

  • blast sparkles disappear
  • lava, sand, and explosives fall into empty tiles (value < 6)
  • lava that is on a solid tile can randomly pool sideways into empty tiles

To create explosions, the spritesheet is copied to the screen, 3 black circles are drawn (at -128, +0, +128 to handle the world looping), and then copied back to the spritesheet. The frame is then skipped and redrawn to avoid showing any of that process during play.

for x=-22,22 do
-- (draw tiles here)
x=(x+px)&127 -- new local copy x in looped world space
for y=12+tt%4,120,4 do
    if(sget(x,y)>11)sset(x,y,0) -- 12 blast sparkles
    -- 2 bomb 3 lava 7 sand
    --> check of tiles is in this set using bits 0b10001100 (140)
    b1=sget(x,y)
    if(1<<b1)&140>0 then
        if sget(x,y+1)<6 then
            -- empty space or lava below -> fall
            sset(x,y+1,b1)
            sset(x,y,0)
        elseif b1<3 then
            -- explosive moving in to a solid tile
            memcpy(0x6000,0,0x2000)
            c(x,y,3,12)
            c(x+128,y,3,12) 
            c(x-128,y,3,12) 
            memcpy(0,0x6000,0x2000)
            ?"\as▥i6x5[a"
            goto _ 
        elseif b1<4 then
            -- lava: pool sideways
            x2=(x-1+rnd(3))%128
            if(sget(x2,y)<4)sset(x2,y,b1)
        end
    end
end
end

Platformer Physics

Very tiny but serviceable platformer logic following ideas from Blue and Lefty and other previous pico-1k games. The ? print statements are for playing sounds with P8SCII commands:

-- move horizontally
x1 = (px + dx) % 128 -- candidate x in looping world
-- move to x1 if not solid at top and bottom of player
if (sget(x1, py) | sget(x1, py+.75) < 5) px = x1 

-- move vertically
if(jj > 0) then -- jumping / falling 
    py += dy -- speculative; if hit solid, roll back
    if(sget(px, py) > 4) py-=dy dy=0 py\=1  --  indexes more than 4 are all solid; roll back and snap to top.
    if(sget(px, py+1) > 4) jj=0 py\=1 ?"\as2[[[g"   -- land on something solid
else
    if (sget(px, py+1) < 5) jj=1 dy=0  -- start falling when there is nothing solid below feet
    if (btn(4)) dy=-.6 jj=1 ?"\as2[[ga" -- jump
end

Finally, the resulting p8.rom cartride bytes in xxd -p format, after being minified and compressed with @thisismypassword's Shrinko-8:

00707861064b040017af1f5cdc7fd03541734571f4e94f905cde9c1fdc149c5e
042f9047771c727c7f8f418ec95fe0faf024a794c79db23732935c74fd2dc1f0
c8fbbccbe471d55eb938fa00755de6e546b5397157f00aef900eedcdee15b78d
95411a5dd6cf4d84030357197b67c337afd45d27171065f91b54e3f5d8a34417
4603dfc3f750bc4cb4ea896e2f0a5e21593a653a39e321d223de647824498617
c3b0da4b86f3a62b56e3a54316ab3342838d3ac800be38c03745514c3828490a
5f8e658bc9dcf401a34571ca83540f91c61be54b5ceb72cc0d2a9d1c3b37b2ef
930312e182c99543569a53b682324cd2a132bce59a5b56cee8e33638a6778cd1
f692e4d6816a236d077ab9b0ac5d6ac4022e68e656d452b2f696381b0894939a
a059d93046550e346537915f1a79e39e66e3394c536b08a92de90e0a0cf44a02
efa123f226420afdd6a34c0c488f4cedaa8954e973b4a551da2849448b662e58
bb7727530b132d0956ce49ea64a189a4851e64a5581e7a130193f0e691f5b46e
4f585919cefa62a3e88bd5e28862b8b862b69f8a2c500f24030ebd21a88b60ba
3c60b4991c995bd95e19dfe8f240c660636b61646a745100a1901a1133a85ccf
a22a848c86462a050525a3d536ba79291cbea59aee6b8d80a659eb2a8da0b6ae
17b406028d8089d19d85461225de9e9c5259699bc1b1be9fa8dab46f2706d299
1933e55d6e965ad928dd100b54601cc917eb019d94854251a02b05059b22bf5a
bb6024309b3ec10193b5b5c36e70e484201b6aeab5b8900deabcbb50e5e58636
49efd78158465c4383512217d06483ab4b32ab226bb9cc5a2114d494b1956a4d
08bb67c9eed168ff8eb9e18cfcad86268e18bfe3a0726e7862bcd8da2b148705
4e9e64514444616a672e989413903c8d740dc616a5cabca05a10eba5787473a9
dcf2895cc0dcecfa6ab4b43a39649724bd6a57cc29996f0521e667076a758676
66b19c88ab3277c9902a41beb9954e5c512d06d5e2d2397da622b03434b0910d
775a0c678ca54a2e520cd5d240994809347a525b7351b4dfccd5622163895cc0
8e9a485a0a40b49e9af0cc80e72ce194bdbd81c9f591fdf9b016375f256e30e3
d466717179642549cab6d411886b09a7b5b61a947feee45f16ca6cd333d95673
81bc88b7355edc911a4f7245d760717a724969a5d709a7cc40558e6908449247
a392124dab8aa6b4d42f8c2957e58a2e526245d9ca9b69ac0c4862ecac4532c3
fd6a26cb74532d54ac1550148156652c32755add2c744d23bc022d4126111592
e6b8616fab576ed9ddbc6178463458a66235d325e996f4c496e43126bac805fd
6219ce0a18acce4e6cf59b26b8628d7a029bdd6dcd82e3e6c2606d7ac601d343
0bc1e0e6013818a2696188a170a9c0bfa0c97b32fda1d9b1bd604c97aa95a704

// pico-1k entry page on itch: https://itch.io/jam/pico-1k-2026/rate/5047251

Lexaloffle News

My blog feed has been mostly just jam games lately, so here is a quick rundown of things going on behind the scenes:

  • Picotron is going into beta at 0.3.1 later this year, and finally get its own BBS announcement post! (but I have also been detailing various Picotron developments on bsky and mastodon). 0.3.1 includes scoresub() improvements, websockets support and extra math functionality for supporting 3d renderers written in userland.

  • Doodlemud is returning as a Picotron cartridge (it started as a javascript thing before Picotron existed) for stress-testing scoresub() and the more general LXO ("Lexaloffle Online") system that will be eventually be rolled out to PICO-8 and Voxatron. You can read more about LXO here, and there is a twitch stream of a talk I did last week at Tokyo Indies that goes a bit into scoresub(): https://www.twitch.tv/videos/2878326421 . There's also a nice talk at the end by Shoma on Carrot Kingdom and its WIP Picotron edition.

  • Earlier this year I released p8rt_ios, which is a wasm module that can be used to create iOS companion apps like Pocket8 and P8Launcher. I'm also working on a .apk build targeted at playing carts on android handhelds, and later my own companion app for Android. For now there is also: Macs75/pico8-android).

  • We will be at Pixel Art Park in Yokohama 2026-10-11 (as Pico Pico Cafe) selling miscellaneous rare Lexaloffle merch including machine-embroidered jelpi socks :3

  • The first postcart exhibition is happening at Pico Pico Cafe during the last week of November! There will be a separate announcement and call for postcarts, each of which will be viewable both in their regular animated form and as printed postcard-sized stills with their source code on the back.
32


1

It's actually possible to go below what I assume to be the intended bottom of the world if the player lands directly on top of the bottom layer and uses downward explosives. They don't explode it, but each one destroys one block of the bottom layer and stays there.


spoiler: at the end, you die :)


I think I found a bug... or a unique ground/element? I don't know how I managed to do this (bombing lava generally does not this), nor if touching it breaks the game.

Anyway, very fun 1k game. Impressive work as usual, Zep. :)


This is SO good, @zep! 🤩
The quality of the mechanics and playability​ in this tiny 1K entry are very impressive 🤯
Found myself progressing, dying and repeating several times (very addictive!).
I also kinda love how this sits in perfectly alongside other procgen-driven games like LMS (in fact, I originally wanted the planet layer to be platform/side-on, but I have NO idea how to squeeze it in back in the day - you just proved it IS possible! 👏)
Superb entry (...and thx for taking part! 😉)
P.S. How do u enable the cool zoomed-out/map view!? 😯

P.P.S. If u want any guinea pigs for your Android app - please do feel free to reach out! 😉 #ExcitingTimes


@zep, Thanks for the detailed writeup on this! I was wondering, though, about the raw hex format you showed. Can you paste that text into a file and run it in Pico-8, or is there a way to convert it back to plain text code that can be pasted and run?



[Please log in to post a comment]