Code for combining simple animations together to create more complex animations using + and * operators.
The example uses four simple animations—each one just a different coloured circle starting in a different corner and moving to the diagonally opposite corner—and combines them in different ways to create the final animation.
For simple animations A and B:
- The + operators first runs A and, once it's finished, runs B.
- The * operator runs both A and B at the same time.
As with normal addition and multiplication you can string together as many animations as you want and use parentheses to indicate a particular ordering.
To create animations use new_animation()
and then give the animation object an init
method. The init
method should return a table containing a draw
and an update
function; update
should return true when the animation has finished.
a = new_animation() a.init = function() local offset = 0 return { update=function() offset += 5 if offset > 128 then return true end end, draw=function() circfill(offset, offset, 2, 7) end } end |
The animation object then needs to be instantiated to use it.
anim = a() -- or a.init(), same thing function _update() anim.update() end function _draw() cls() anim.draw() end |
Having the init
function means you can use the same animation multiple times since any internal values, like offset
, will be re-initialized each time. You don't need to initialize the animation each time when combining animations, that's handled internally, you only need to initialize the combined animation as a whole.
-- creates a compound animation which will play the simple animation -- three times in a row. anim = (a + a + a)() -- or (a + a + a).init(). Again, same thing. |
Lua Code (indented 3 spaces)
Lua Code (indented 1 space)
If I view a post on the BBS and scroll down to the bottom to post a comment, there are two buttons: Submit, on the left and Preview, on the right.
If I click Preview I'm taken to a new screen. The first issue is that I've just clicked a Preview button but I'm not shown a preview just a larger text box. To actually see the preview I have to scroll down and click the Preview button again.
Which brings me to the second issue: The Preview button is now on the left and the submit button, now called Publish Changes, is on the right. I would expect the buttons to still be in the same place and the number of times I've almost posted half a comment because I almost clicked Publish when what I wanted was Preview are many.
Minor issues. Not the end of the world if it never gets fixed—at this point fixing it might cause as many or even more mistakes than not fixing it so maybe it's just not worth doing—but thought I'd point it out anyway.
This is just something I've been playing with in between working on other things.
I didn't know about sprite stacking until this old post popped up a couple of months ago. Although that example rotates the sprites themselves it turns out you can get some interesting results even without it. I haven't bothered with collision detection so you can walk right through the trees and the rocks. Waking through the water was intentional though. I wanted it to look like actually walking in water.
The space is procedurally generated but persistent so it's the same every time the cart is run. And it's effectively infinite in all directions. Not actually infinite obviously but pretty darn big for no particularly good reason.
This cart uses devkit mode so you'll need a mouse and keyboard to use it.


Update:
This started off as a level editor for the Space Taxi Remake I'm working on. At some point I realized I was going a bit overboard and instead of doing the sensible thing and saying, "well that's good enough for my purposes I guess I'll stop now," I just kinda leaned into it.
Obviously any images you make with this are yours to do with as you please. If you use it and like it credit is, of course, appreciated but not required.
I've posted a new comment in the Space Taxi thread with a few very rough screens made with this if anyone wants to check that out.
Saving and Loading
Saving doesn't work from the BBS embedded player or on Education edition so you'll need to run the cart locally to do that. Files are saved as .p8l files. They're just plain text with a lisp-like syntax describing the points and shapes, etc. of the drawing.
By default files save to the desktop and are overwritten if they have the same name. You can change this behaviour by modifying the variables overwrite
and save_to_desktop
at the very top of the code (Tab 0.)
Loading is done via dragging the file into the program and that does work on the BBS player! This sample save file loads the drawing of the words "Drafting Table" as seen on the cart image. Just save it locally as a text file then drag it in to load.
You can load more than one file and it'll just keep adding in the objects from each. This is so you can have, for instance, a file with a bunch of different platform types and one with different obstacles, etc. and combine them without having to redraw them every time.
Exporting and Importing
To compress and export (to the clipboard) the image as a string of binary data press the button at the bottom right of the group in the center or press '8'.
![]() |
[8x8] |
To import the image into your own cart, paste the binary string into the cart and copy and uncomment the code from tab 0 of this cart. There are only three functions you need to know about:
store_image(dest, str) converts the binary data string 'str' to bytes and stores them in memory beginning at memory address 'dest'. This data is still compressed and not the actual image. Returns the memory address immediately after the data which you can use to store additional images or other data. next = store_image(0x8000, img1) -- where my img1 is a string exported by Drafting Table store_image(next, img2) -- as is img2. load_image_from_memory(dest, src) used in conjunction with 'store_image'. Reads the compressed data from memory address 'src' and writes the decompressed images data to memory address 'dest'. This can then be drawn to the screen with 'memcpy'. load_image_from_memory(0x1000, 0x8000) memcpy(0x6000, 0x1000, 0x2000) -- draw the image to the screen load_image_from_string(dest, str) you can instead decompress the image directly from 'str' to memory address 'dest' without storing the compressed data to memory. load_image_from_string(0x1000, img1) memcpy(0x6000, 0x1000, 0x2000) |
The rotation tool in action and "Drafting Table" exported as collision demo "level geometry" for my Space Taxi remake.
Some caveats
Available drawing area
The UI at the bottom of Drafting Table and the UI at the bottom of Space Taxi are the same size leaving me exactly enough drawing space for a single level and that's all that gets exported. If anyone thinks that they'd find this useful it shouldn't be too hard to add a shortcut to hide the UI and allow editing and exporting the full screen. Unless/until someone asks though it will stay as it is because it suits my purpose.
Positive and negative rotations
When using numerical input for rotations I've gone with the convention in mathematics: positive angles rotate counterclockwise and negative angles rotate clockwise. Additionally, angles are given as a number between 0 and 1 as in Pico-8 itself. So 0.25 is a 90 degree rotation counterclockwise while -0.125 is a 45 degree rotation clockwise.
All objects rotate around their own center point even if multiple objects are selected at once unless grouped together with 'g' in which case all grouped objects rotate as a unit around their common center point.
Moving objects ignores snap
This is entirely because my sleep deprived brain was having trouble getting it to work. It'll be fixed eventually. For now the workaround is to just get it in approximately the right spot then use the arrow keys to place it precisely.
Polygons are a bit weird
Due to the way I'm drawing/filling/detecting collisions with the various shapes, the free polygon tool sometimes gives unexpected results like not filling the whole thing or, sometimes, making it completely un-selectable except via the 'select all' shortcut. This only happens when the polygon in not a convex polygon.
The "same" non-convex polygon in outline and fill mode.
So to make sure you're getting what you expect, stick to convex polygons (all interior angles less than 180 degrees) and for complex shapes, draw separate parts and group them together.
Same shape as above but constructed from two shapes grouped together.
The original Space Taxi divided up its 24 levels into three "shifts" of eight levels each corresponding to the easy, medium and hard levels. And it turns out eight levels is about what I'm able to fit on a single cart so I've decided to release the game by shift and then merge them all together into a single multi-cart game when I'm done. It's actually already multi-cart with one for displaying the menu and loading the level data into upper memory and the other for actually running the game.
The first one, Morning Shift, is not quite finished but it is fully playable. The first level is a copy of the first level from the original game as sort of an homage to it but the rest of the levels are original. And they could use some play testing. If anyone is willing I'd be happy to hear any feedback.
In particular I'm interested in hearing:
- Are the blinking platform markers helpful?
- Would they be more helpful if they blinked a different colour?
- Keeping in mind that these are the easy levels—with 16 more levels to come at some point in the future—how does the progression feel?
- Would you change the order of any of the levels?
- Was there anything that you particularly liked or disliked about the game?
Don't feel like you have to address all (or any) of those and it's by no means an exhaustive list so any other comments are also appreciated.
Things to do
- Level art :: You'll see that I've spent more time on some of the levels than others. The layouts are, I think, pretty much set but I'm still working on making some of the levels more visually interesting.
- SFX and Music :: The original had voice synthesis which I don't but I'd like to add some sound effects when passengers appear, get knocked over, etc. as well as other sound effects generally. And while the original didn't have music I'll probably add some.
- Fix how money (aka: score) tips, etc. work
- Add saved high scores
- Add an actual win screen
- Various tweaks (refuel faster, etc.)
Original Post Below
I don't know if anyone else remembers Space Taxi from the C64 era but I used to love playing it as a kid. I'm not sure why it popped into my mind a while back but I thought I'd take a shot at implementing a Pico-8 version.
This is a work in progress so it only has a few very boring levels for testing out the mechanics. I'm a bit too close to it and have gotten used to the controls so if anybody who isn't me felt like giving it a shot and letting me know how the controls feel I'd be grateful.
There will likely be a second screen under the "Controls" menu option which explains the UI but I haven't implemented it yet. So briefly:
- The money indicator on the left is your "Earned Money" aka, your score.
- Above that is your lives
- The money indicator on the right is your "Tip." Deliver passengers faster, get a bigger tip.
- The "clock" on the right is the level indicator. (1:00 is level 1, 12:00 is level 12, etc.)
- The black box at bottom center is where passenger instructions are displayed (where to take them)
- Above that is the fuel gauge. Fuel usage is tied to how long you're in the air not how much you use the thrusters and fuel resets between levels. The third (and last) sample level has a fuel platform to test out the refuelling mechanic but not all levels have fuel platforms.
- The gray checked bar above the other UI elements is the landing indicator and flashes either green, yellow or red. Green for a soft landing, yellow for a harder landing and red when you crash. Your tip is reduced (well not yet but it will be) for "yellow" landings.
Once I've got the mechanics dialed in I'll probably throw together a quick level editor and start making some actual levels. Hopefully I've got room for a full 24. I've got a little under 3000 tokens left and about half my character space and I haven't tried to optimize it yet but I suspect I may end up using a second cart for the level data.
Thanks to anybody who tries it out!
This function creates simple extensible 'types' with a few additional (potentially) handy properties. If you find it useful, help yourself to it. Credit appreciated but not required.
You use it like this:
-- a 2d point object point = new_type('x y') -- argument names separated by spaces p = point(1, 2) print(p.x) -- prints 1 print(p.y) -- prints 2 |
As for the handy properties, the first is that the object's data is stored both by key and as an array. Which means you can access the properties using the dot notation, numerical indexes, or even unpacking the object, all of which can come in handy in different situations.
p = point(1, 2) x, y = unpack(p) -- extract all the data in order -- all of these print 1 print(p.x) print(p[1]) print(x) -- and all of these print 2 print(p.y) print(p[2]) print(y) |
The downside of this is that each object is storing the data twice and, therefore, requires twice as much space. So if you have a lot of active objects in your program at once you'll run out of memory about twice as fast. But you can always modify new_type()
to only use one or the other of these methods instead of both if you prefer.
The second handy property is that functions created with new_type()
aren't actually functions, they're objects which you can add methods to. Those methods are then available to every object of that 'type'. For instance:
point = new_type('x y') point.add = function(a, b) return point(a.x + b.x, a.y + b.y) end p = point(1, 2) q = point(2, 3) r = p:add(q) print(r.x) -- prints 3 print(r.y) -- prints 5 |
It even works if the methods are defined after you've already created the objects:
point.dot_product = function(a, b) return a.x * b.x + a.y * b.y end print(p:dot_product(r)) -- prints 13 |
Not sure if this is actually a bug or expected behaviour. Seems like a bug to me. That said, don't know if it's a problem with PICO-8 or just with Lua in general.
Here's the problem. If I define a metatable using the __metatable
property then the __tostring
method doesn't get called automatically (by print for example) when expected.
m = {} x = setmetatable( {}, { __index=m, __metatable=m, __tostring=function(self) return 'blah blah blah' end } ) print(x) print(tostring(x)) |
Output:
[table] blah blah blah |
So, I can call the tostring() function explicitly but it's not called automatically by print as I would expect. If I leave out the __metatable
property though it works as expected:
m = {} x = setmetatable( {}, { __index=m, __tostring=function(self) return 'blah blah blah' end } ) print(x) print(tostring(x)) |
Output:
blah blah blah blah blah blah |
Putting the __tostring
function in m also doesn't work:
m = { __tostring=function(self) return 'blah blah blah' end } x = setmetatable( {}, { __index=m, __metatable=m } ) print(x) print(tostring(x)) |
Output:
table: 0x30bb9cc table: 0x30bb9cc |
Nor does giving m its own metatable with a __tostring
method:
m = setmetatable({}, { __tostring=function(self) return 'blah blah blah' end }) x = setmetatable( {}, { __index=m, __metatable=m } ) print(x) print(tostring(x)) |
Output:
[table] table: 0x30bb9cc |
The __metatable
property doesn't seem to interfere with other metamethods. Haven't checked them all though. Example:
m = {} x = setmetatable( {}, { __index=m, __metatable=m, __add=function(a, b) return 1 end, __tostring=function(self) return 'blah blah blah' end } ) print(x) print(x + x) |
Output:
[table] 1 |
This isn't really a major problem, I'm mostly just messing around but it seems weird so might be worth taking a look at at some point.
I don't know if people are still having problems with the inline music player but here's a cart with the song just in case.
I've made no attempt to optimize space and used pretty much all the sfx slots. Though some are duplicated just so if you happen to listen on the editor music/sfx tab it's kind of satisfying to watch. To me at least.

Just a little weekend "I need a break from my projects" project.
Uses the marching squares algorithm to determine tile placement at each step and 3 dimensional value noise to animate it.
This isn't really anything special, just a little quality of life improvement I've found handy and keep in a file of utility functions I use frequently.
Overloads sub()
so it works on arrays as well as strings. I find it useful to be able to slice and dice arrays in the same way as strings and especially now that we can index strings with square bracket notation the difference between them isn't always important. I've got something with a length and I want a sub-section of it. Makes sense to use the same function for both, in my mind at least.
_sub = sub function sub(lst, i, j) if type(lst) == 'string' then return _sub(lst, i, j) else local r = {} for n=i,j or #lst do add(r, lst[n]) end return r end end |
Creates a new array without modifying the original. Doesn't handle negative indices because I've not really needed them so far but it would be easy enough to add.
Potentially a feature suggestion @zep? I don't mind writing the function myself but I'd also be happy not to. But maybe I'm the only one who finds it useful.
I'm working on a game using procedural generation and although the game is nowhere near done I thought the generation technique itself was interesting enough on its own to post about.
Rather than try to explain it myself I'll just send you to someone who already has, better than I likely could. BorisTheBrave: Graph Rewriting for Procedural Level Generation
Instead, I'll just share some of my initial results. The general idea is to generate the "big picture" details first and then successively refine them to build a fleshed out so the levels generated here are shown at 1/8 scale. Eventually every pixel in these maps will expand to be one tile at full scale with additional generation filling in the fine details along the way.
The rules I'm using a pretty simple for now; they were mostly intended to help me shake out any bugs in the graph rewriting system itself. The actual rules I end up using will probably be somewhat more complex but even so I think I'm already getting some decent results.
The Cycle
The first thing I do is generate a cycle with a start (green square) and an end (red square.) The idea for each level is you go in, get whatever is waiting for you at the end—treasure, a boss fight, whatever—and then have to get back out. Having a cycle gives you multiple potential routes to accomplish that.
At the moment the cycles go one-way—although it's not shown—with one path leading toward the end and the other path leading from the end back to the start. But you could have two paths heading towards the start where one might be shorter but full of enemies and the other is longer but relatively safe, and all sorts of other variations.
Terrain
Next up is terrain. For this demo I just have two terrain types, stone and water. You can have both types on a single tile giving you wet stone. The rules are written so that stone and water will never touch directly but will always have wet stone in between. This also results in the occasional patch of wet stone bordered by either stone on both sides or water on both sides which helps to vary things up a bit more.
For the actual game I'm thinking I'll just have abstract terrain types: terrain-1, terrain-2, etc. Then each level will have a randomly chosen theme and that theme will give me a terrain palette which will assign actual terrain types. So you could have fire level and ice levels and so on.
Enemies
The enemy rules are fairly straight forward. There are four types:
- Large enemies (big X)
- Small enemies (small x)
- Swarms (clusters of small enemies that attack together)
- Guardians (in a white box with a border. When you come into their
area everything closes off and you can't continue until you defeat
them. Basically mini-bosses.)
The red ones will be visible as you approach and the yellow ones will be hidden and ambush you. You'll never get two large enemies right next to each other but any other combination of enemies is possible.
Obstacles
Obstacles show up on the map as locks and keys but they may not be actual locked doors requiring keys. They're just anything which requires you to do or obtain something in one part of the level in order to progress in another part. The "key" could be an actual key but it could also be a lever or a puzzle or a new ability. Maybe you need to drain water in one area to reveal a door in another. Whatever.
This, in particular, is an area where I think graph rewriting shines. Since at this stage the whole level is represented as a graph and the path from start to end and back to start is a directed sub-graph, it's relatively easy to ensure that keys never end up behind their associated lock. The lock and key are initially spawned on the same tile with keys optionally then being moved backwards along the path but never past the start.
At the moment there's a 50% chance of a key moving on each iteration of the rules so, on average, keys are found fairly close to their locks but I can tweak the probabilities to have them move either more or less.
Rooms
Rooms are added to empty areas near the main path. Rooms are either big or small and once placed one or more doors are added to the room connecting it to other rooms and/or the main path.
The dark gray bars connecting the rooms to the path are just to show roughly where doors are and how they connect up to the main path. There won't be literal corridors to every room. Either the rooms will be moved closer to the main path or the surrounding terrain will be expanded to the edge of the room.
At the moment the rooms are all just kind of plopped down on top of each other. Whether overlapping rooms merge into a bigger room or where exactly the walls between them will be will be figured out at a later stage.
Next steps
Next is to take this simple set of rules and and basic level layouts and turn them into full-fledged level maps to get the basic infrastructure in place to handle that process.
After that I'll probably take a break to work on actual game mechanics before really diving into the level generation rules.
A few days back @dw817 posted a thread about the number of possible combinations in 256 bytes in which they asked:
> Is there a way to calculate 256^256 ?
And knowing there are all sorts of tools and languages which can handle such huge numbers I cheekily posted the answer and caused a bit of a derail.
Anyway, I had some time on my hands so I figured I'd implement large number support for Pico-8.
Edit: Fixed a bug in divide()
Is this likely to be of use to anybody? Probably not. Is it major overkill? You betcha! Though in fairness, at 706 tokens it came out smaller than I was expecting.
Features/Functions
-
bignum(n)
: Create a 'bignum' with valuen
which is just a regular Pico-8 number. Most operations require both numbers to be bignums. Sobignum(16000)*bignum(1254)
but notbignum(16000)*1254
.bignum()
is equivalent tobignum(0)
-
divide(bn)
: You can use the division and mod operators (/, %) on bignums if you only need one or the other. Thedivide
function returns both the quotient and remainder at once.q, r = divide(bignum(3)^16, bignum(2)^15)
show(bn)
: Converts a bignum into its decimal representation as a string.factorial(n)
: Calculates the factorial ofn
and returns the result as a bignum. Factorials get very big, very fast so the input tofactorial
is just a regular number.+ - / % ^
: Arithmetic operators behave how you would expect. All require both arguments to be bignums with the exception of^
where only the first argument is a bignum and the second is a regular number. Division is integer division only sobignum(3)/bignum(4)==bignum(0)
.> >= <= < ==
: Comparisons similarly require both arguments to be bignums.
Issues
- The arithmetic operators are all fairly efficent but probably not optimally so.
- I wasn't taking negative numbers into account at all so I make no promises about what will happen if you try to use them. Maybe it will work! (It probably won't work.)
show
is extremely inefficient. I'm sure there are better ways to convert very long binary strings to decimal but I'm not familiar with them so this is what I've got. Large numbers (like 52!) may take a bit of time and very large numbers will crash the program as it runs out of memory. (bignum(256)^256 does this, sadly. It calculates the number fine but crashes when you try to convert it.)
I know a lot of people, myself included, usually write their pico~8 code a little off the cuff tinkering with it until it works. Which tends to be more fun in my experience. But it can also be incredibly frustrating if I'm working on sometime more complex where every time I change something I break something else. And in those cases planning out some formal tests helps me maintain my sanity and get the thing actually working much faster than I probably would otherwise. And since I'm working on something fairly complex at the moment, I took a bit of a detour and put together a little test framework and thought I'd make it available for anybody else who might find it useful.
The code is on github: https://github.com/jasondelaat/pico8-tools/tree/release/testo-8
Or you can just copy it here:
And here's a cart with some simple functions containing errors and a few tests, most of which fail just to give you an idea of how it works.
Testo-8
The framework is pretty simple. It's just a single function test
which returns an object exposing methods— given
, when
, result
and cleanup
—for defining the test.
Testo-8 defines an _init
function which automatically runs the tests. Just #include testo-8.lua
, write tests and run the cart. If you've defined your own _init
you'll probably need to comment it out to get the tests to run.
A very simple test might look something like this:
test('some simple addition') :given('the number one', function() return 1 end) :when('adding 2', function(n) return n+2 end) :result('should equal 3', function(r) return r == 3 end) |
The methods given
, when
and result
—which I'll call clauses—all take a string as their first argument and a function as their second, while test
takes a string argument only. The strings are used to build the output message if the test fails.
The function arguments taken by the other methods serve different purposes:
given
should return the object(s) being tested. (1 in the example)when
takes the object(s) being tested as input and does something with it returning the result(s) (add 2)result
takes the result(s) and should return a boolean, true if the test passes and false if it fails. (does 1+2 == 3?)
Each test has exactly one given
clause below which will be one or more when
clauses. Each when
clause contains one or more result
clauses and can optionally be ended with a cleanup
clause. More on that later. So an actual test might look something like this:
-- the ...'s are just a placeholders for some appropriate function test('some test') :given('an object to test', ...) :when('1st when', ...) :result('result 1', ...) :result('result 2', ...) :result('result 3', ...) :when('2nd when', ...) :result('result 4', ...) :result('result 5', ...) :result('result 6', ...) :cleanup(...) :when('3rd when', ...) :result('result 7', ...) :result('result 8', ...) |
The number of result
clauses is the actual number of tests that will be run so the above example would be eight tests. Each result
clause is executed as follows: The given
clause is executed to generate the object(s) to test. The test object(s) are passed to the when
clause which appears above the result
and finally the results are passed to the result
clause which determines whether the test passes or fails.
So in the above example the given
clause will run eight times, once for every result
. The first when
clause will be called three times and so will the second while the third when
clause will only be called twice.
cleanup
takes a single function as its argument and is used to clean up after a test if, for instance, the test modifies some global state which needs to be reset in-between tests. The cleanup
clause is optional but if it exists will be called after each result
clause inside the same when
. The cleanup
in the above example would therefore be called three times, once after each of result
s 4, 5 and 6.
For anyone interested in seeing actual tests, here are some I wrote for a simple s-expression parser.
Modules and Testing
I rely heavily on an external editor and spreading all my code around a bunch of files. If that's not how you work this may not be super practical. But here's a quick run-down of how I (currently) work on a project.
Even though Pico-8 Lua doesn't technically have modules I generally try to write things in a modular way and #include
with the help of do...end
gives me something module-like.
A vastly oversimplified example would be something like this:
-- player.lua local pos = {x=64, y=64} local s = 1 local function move(var, dist) return function() pos[var] += dist end end move_left = move('x', -2) move_right = move('x', 2) move_up = move('y', -2) move_down = move('y', 2) function draw_player() spr(s, pos.x, pos.y) end |
Which I include inside of a do...end
block like so:

Writing modules like this doesn't really cost much extra because:
- These are all functions I'd write anyway
- The
local
keyword doesn't use any tokens - The
do...end
costs just a single token - The added encapsulation given module local variables means I can't accidentally mess of things like the player position from other parts of my code because
pos
doesn't exist outside of the module.
Importantly, I don't put the surrounding do...end
in the module file itself. Because when it come to writing the actual tests, I'll put those in another separate file and then include it inside the same do...end
block as before.

This makes the tests part of the same module so they can access and test all the local data and functions. Once I'm sure everything is working properly I can just comment out the #include
for the test file and free up all those tokens.
Issues
- Since Lua doesn't have exception handling capabilities like
try...catch
or similar, I'm not able to intercept certain errors and report them as test failures. So things like attempting to index a nil value, etc. will still cause the cart to crash and you'll have to fix those problems before the test will run. - The above can also lead to occasionally cryptic error messages saying that there's an error with testo-8 itself. This is certainly possible but usually it means you've passed nil, or something else, where testo-8 is expecting a function. If you're frequently commenting out parts of your code make sure you haven't commented out a function which you're using in a test.
So I've started working on this. It's still very early and I've only got some of the UI elements and the very beginnings of procedural generation in place but I think there's enough to make it worth sharing.
Controls
While moving around:
- 🅾️ to switch to the ability selector. (Coloured boxed bottom-middle)
In ability selector:
- 🅾️ to select highlighted item
- ❎ to cancel
Choosing 'END TURN' pulls up a 'Dice Selector' menu which doesn't do anything yet.
Press either ❎ or 🅾️ to dismiss it.
Notes
- Actual movement in the game will be tile based but I wanted people to be able to zoom around the big empty levels fairly quickly for now because it's not like there's much to see.
- At the moment the levels are just a big loop of straight, boring grey hallways surrounded by blackness but eventually room will be able to expand into the available empty spaces, there will be locks, keys, enemies, puzzles, etc.
- The mini-map may or may not be in the actual game but I wanted to give an overall idea of the shape of the generated levels.
- The green square in-level is the level entrance and the red one the level exit. Nothing happens yet when you get to them.
- The stats on the bottom left are hard-coded and don't do/mean anything yet.
I'm trying my hand at procedural generation using graph re-writing. There's a fairly straight-forward summary of the process here for anyone interested: https://www.boristhebrave.com/2021/04/02/graph-rewriting/
This version isn't actually using re-writing yet which is why all the interesting stuff is missing from the level. It's just a graph with a basic loop. I've got the system written and—I think—working as expected so next steps are to start testing some rules for slightly more interesting levels to shake out all the bugs and then take it from there. Oh yeah, and all the actual game mechanics. Those will probably help too.
So maybe this is old news to everybody but me but I just discovered it by accident. Turns out you can define local functions! It makes sense but it somehow never occurred to me to try before.
Here's a quick example:
do local function a() print('in a') end function b() print('in b: calling a') a() end print('in local scope') print(a) print(b) a() b() print('leaving local scope\n\n') end print('in global scope') print(a) print(b) b() |
Output:
in local scope [function] [function] in a in b: calling a in a leaving local scope in global scope [nil] [function] in b: calling a in a |
That's it. Just thought it was interesting.
I had a bit of time to tinker yesterday. I'm not sure what made me think of the old Infocom text adventures but here we are.
If you're familiar with Text Adventures (or Interactive Fiction) you may know that one of—maybe the—most popular tool for creating them has been the Inform language. The current version is Inform 7 but waaaaay back when I was first learning, it is was Inform 6.
Anyway, I decided to throw together a quick little IF authoring API loosely based on Inform 6. It is by no means complete or particularly advanced. Basically, I followed the first tutorial for the game Heidi from The Inform Beginner's Guide and implemented just enough to make it work. But work it does! Mostly. I think...
The command parser is extremely simple so don't expect too much from it. All commands should be in the form:
verb [noun] [noun] |
Where the nouns are optional depending on what you're trying to do. So east
or e
to walk to the east; take object
to pick up an object; and put object target
to put an object on/in something else. There are enough verbs, including synonyms, to complete this game and the API even lets you add new ones but don't try to be too creative with your commands. It was mostly just something to do on a Saturday afternoon so I may or may not develop it further.
The API clocks in at 926 tokens while the example game itself is only 173.
For simplicity's sake here's the code for just the game itself if you're interested in how it works. You can find the original in Appendix B of The Inform Beginner's Guide linked above if you want to see how the two versions compare.
Walkthrough
Made for the midilib custom SFX instrument project
And in action:
The animation is taken from one of my other projects. The palm tree image is adapted from the tutorial The basics of Painting with Maths by Inigo Quilez.
I mean, who doesn't compose for helicopter, right?
Made for the midilib custom SFX instrument project
Credit's always nice but not required.
Here it is in action. Please forgive the poorly and hastily drawn helicopter.
So this isn't strictly Pico-8 related but I just came across a video series on Youtube which I thought others here might enjoy.
The Animation of Final Fantasy
The intention seems to be to follow the evolution of FF animation all the way from FF-I up to FF-whatever-number-they're-up-to-by-the-time-he-finishes-the-series.
So far he's only up to FF-IV (the first Super Famicon/SNES release) but I think the first three (the Famicon/NES releases) are probably most interesting to Pico-8 developers. I thought it was an interesting look into how the developers managed with so few assets and—especially in FF-II—how they were able to use those assets to tell a story and convey at least some very basic sense of character.
PICO Composer v0.3
Changes
PICO Composer main menu
When you start the cart you'll be prompted to load an album or create a new one. Use the up/down arrow keys to navigate the menu and enter
to select. Either, or both, of the tempo and time signature can be left blank and you'll be given the defaults of 120 bpm and a 4/4 time signature. Any time signature that can't be parsed properly will default to 4/4.
When creating a new album the album name will be the name of the file—no need to specify .p8 extension—where your work will be saved. At the moment each "album" can only have a single song, with a single voice, on instrument 0. The idea though is to have each album contain multiple songs. Space constraints may end up making that infeasible but that's the plan for now at least.
Controls:
PICO Composer is intended to be run locally with a full keyboard and will save your work to a separate .p8 file.

Demo Song/Album
Download this cart and save it in the same directory as picomposer.p8.png
Load and run picomposer and when prompted select load album
, type song6
and press enter. It should load and after a couple of seconds you'll see the last few notes of the song. Press space
to play the song from the beginning.
When you export an album (press e
) a new file album_name.music.p8
will be created. This file can't be edited in Pico Composer and is intended to be imported for use into your own carts. To do so copy/paste the code from the example below and use the load_music()
and play()
functions.
(Note: The song data for this cart is in the spritesheet and it's "importing" from itself into upper memory. Usually you'll be importing from the generated .music.p8 file but I wanted a self contained example.)
The importer is currently pretty heavy sitting somewhere around 600 tokens. Once I've got a few more features implemented I'll revist and try to bring the size down.
Future Development/Feature Wishlist
- Select/edit note(s)
- Allow all the pico-8 instruments and custom instruments
- Multiple songs per album
- Dynamics: volume pp, p, mp, mf, f, ff, fff
- Allow up to 4 voices and ability to switch around between them
- Clefs. As is the staff is in treble clef. Adding, at least, bass clef would be useful
- Bar lines
- crescendo, decrecendo
- Repeat symbols and voltas
- Effects/articulations (tremolo, etc.)
- Maybe a mode specifically for notating drum parts
- Possibly the ability to convert to/from the pico-8 tracker format
- Joining notes when possible (connected 8th notes, for instance)
- Ties and slurs
- key signatures
- Draw leger lines
- Start screen with nicer menus
- Reduce importer code size.
Proof of concept version:
View Older Posts