Log In  
Follow
drakeblue

Solo Indie Dev - author of PICO Space, a 2D space sim featuring small animals in space; Halloween Horrors; P8C-BUN; The Pico Mermaid and Demystifying the Christmas Tree.
WIP Dungeon Apprentice (a pico version of FTL's Dungeon Master which is proving a bit tricky...).
I tweet and itch and sometimes even face. Maybe one day I'll even steam.

Demystifying the Christmas Tree
by drakeblue
PICO Space
by drakeblue
Halloween Horrors
by drakeblue

aka writing down how my cart works before I forget.

This is about my game, Halloween Horrors, which is here.

This game was written very much based on previous work, with functions dating all the way back to P8C-BUN, my first game. I started from the Demystify the Christmas Tree code because I wanted the kittens. It's not very token efficient at all, but I didn't come close to running out of tokens in the end (6447/8192). Originally, I was making another Christmas game - it was only very late on that my gf persuaded me to make it into something for Halloween - thus it's a little bit messy and rushed.

The unminified source is at the bottom of this post in a spoiler - it's too big to make into a cart without minification. Even after minifying, I did run out of space: v1.0.1 takes up 99.8% of the compressed cart size(!).

General Notes

Sprite sheets

There are 3 sprite sheets:

  • "game" the built-in PICO-8 one which has the kittens, the breakable objects, candles and monsters.
  • "house" the map tiles for the house background
  • "title" the letters for the intro titles

The latter two are encoded in "dbi" format - i.e. the output of my Python image downsampling and compression code (dbi=="drake blue image"; I needed a name of some sort...). This will encode 4 bit PICO-8 images as 1,2 or 3 bit data if few enough colours are used, apply a couple of run length encoding schemes and encode the smallest result as 8 bit unicode for pasting into source code as a string. I wrote it for my attempt at a Dungeon Master clone at the end of last year and tweaked it since then (one day I'll finish that game too). The PICO-8 code for it unpacks this format to lua memory as a table with palette and image data. Once unpacked, the palette for the image can be set by a simple pal call, the data by using poke4(dest,unpack(data)) - very simple and, surprisingly, faster than a memcpy. I tried unpacking to the "upper memory" above 0x8000, but it was slower.

I drew the letters sheet using only 8 colours for the Christmas game which lets that image be encoded as 3 bits per pixel. I think I could have used 4 colours (i.e. 2 bit) and it would have looked fine for this game and saved up to a third of the data size.

At the end of the project I found that I'd run over the compressed size limit, but also that I hadn't used all of these two extra sprite sheets. I cropped them to the portions I was using, re-encoded and that was enough to get under the bar no code changes necessary :)

Editor

I knew I would use a separate sprite sheet for the house graphics (as opposed to the characters, decorations) so I began by making the house in a separate cart. I started off using PICO-8's sprite and map editor, but I swapped the latter for Tiled using the plugin here. This worked very well. I use Aseprite for most sprite work, but for some reason I did all drawing in PICO-8 for this game.

For the last game I'd generated the platforms by hand: add some values, run, look at a debug rectangle, repeat. That was okay for one Christmas tree, but didn't appeal for a whole house. I considered having no separate platforms and using impassable tiles, but having experimented a little with drawing the house interior already I was having enough trouble making it look as nice as I wanted without adding any more limitations. Besides, I wanted to see if it would work "off-grid".
I was already working in a separate cart to the game so I started adding code there for editing where the platforms and walls went. Then I added a simple ball sprite in position 0 (so it wouldn't be drawn by the map) so I could test the platforms. I considered adding the kitten code and encoding their graphics for it, but never got round to it.

Eventually I moved more and more into the editor as, even though it is clunky, it's still easier to work with for me than entering numbers by hand.

I liked adding the rectfill areas that paint the walls so much that I used that to draw the "paintings" instead of using more sprites. I considered adding a bunch of shadows this way, but ran out of time/patience.
PICO-8 draws rectangles so fast that this didn't seem to dent performance at all.

In retrospect, I would have liked to do one of two things:

  1. implement the editor in another environment eg. Love2D

Primarily so I could take advantage of a bigger resolution window and make the interface a bit nicer.
Incidentally, I did make a double display version of the game in PICO-8 very early on so that the two kittens could play split-screen. It worked very well, but I couldn't get the html output to work nicely with the two displays, despite a few attempted hacks (I hate web-programming) so I parked it. Might have another look at that soon especially if someone can help me hack the css to display it nicely.

  1. make the editor and game one cart

Getting the platform data to save so that it could be used by the editor and the game was a bit annoying. To begin with I used printh to output to console then copied and pasted into the editor and game code separately. Yep, pretty annoying.
I worked out that I could printh to a file and then #include that file from both carts. Problem solved! Except PICO-8 didn't seem to like writing to a lua file that already existed (even though I told it to) and wrote out a .p8l file instead. I'd rename the file back to .lua to update the data. Annoying.
So I wrote a little bash script to run between saves. Less annoying.
It's kinda funny, but I only just worked out I could import the p8l file, instead of .lua and it would Just Work. Ah well, next time...
This has also made me wonder about making user-definable levels for a future game...

The big problem with putting the editor in the game would be that the editor functions obviously use tokens. It's another reason (along with test and debug code) that I would appreciate a "dev mode" in PICO-8 that blocked exporting to png, html, bin etc. but would work from .p8 files during dev time. Even if it put in a "dev mode only" banner or "over token limit" that flashes up every ten seconds or something when run like that...

Anyway, here's the editor if you're interested, with data baked in so it runs - it will still output, I guess, but I don't think it will update to use the new data. I've not tested it on here much at all and be warned: it's really not intended for use by anyone but me and even I could barely use it.

Cart #hh_editor-0 | 2021-11-01 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
3

X shows platforms, walls, floors and highlights current selection for those (and the "paints").
N shows monsters, candles, dec(orations).
There's a bunch of other obscure key commands.

Worst bit: delete (-) has no "are you sure?" and there's no undo.

Interface weirdness aside, it's still been a much nicer way of working than typing the code in by hand so I would definitely use this approach again.

Initialisation

I pull in the two "extra" sprite sheets first ("house", "title") then store the cart's one ("game") so I can restore it from lua memory when needed. There's a few palettes for fading to black, lightning and the darker palette that's used for most of the game.

Because the game never really restarts and when you finish it the cart is just run again, the state of the music is pulled in from one of the stat values. This is the downside of that approach. The upside is that there's no need to write a reset routine and test it. It's worked okay for other game for me.

Writing this post has made me remember that switching the storm off doesn't survive restarting the game so maybe I should fix that if I find space in future.

Most of the rest is run of the mill initialising of variables and tables, except for setting up the majority of the entities in the game: the platforms, monsters, items etc.

During this project I wrote about how I was processing data stored in strings into tables: see here. I improved this a few times, but it always relied on feeding to the same constructor function that meant every entity had the same elements: x,y,r,b,c (if I remember correctly).
x and y were fine - position. But r and b (right, bottom), for instance, only made sense for the platforms and meant different things for, um, different things e.g. velocity for the bats.

I wanted to change that so I considered different constructor functions and triggering those depending on the data - that might have worked, but they'd have cost a lot of tokens and I would have needed to put those functions in a table (or mess about with _ENV) anyway and it occurred to me that I could implement what's now in the code:

function unpack_data(raw_data)
 local raw_data=split(raw_data)
 local num_elements,data,names,item=raw_data[1],{},{},{}
 for p=2,num_elements+2 do
  add(names,raw_data[p])
 end
 for p=num_elements+2,#raw_data do
  item[names[(p-2)%num_elements+1]]=raw_data[p]
  if p%num_elements==1 then
   add(data,item)
   item={}
  end
 end
 return data
end

This lets me describe and initialise a table of tables (or entities as I think of them) with the element names that I want like so:

g_things=unpack_data'5,x,y,vx,vy,col,10,10,0,0,12,20,20,0,0,11'

The first value is the number of elements for each item, then a list of the element names and then the values themselves; for two items in this case. Add more entities by adding more numbers to the end of the string. It works for string values too of course.
That line costs 5 tokens and each table subsequently costs 4 more if you put them on the same line. You can see that the vx, and vy values burn a lot of characters - there's nothing to stop you adding those values later. I needed those elements to be non-nil so I put them in there. I might try adding them later if I need to extend the game as I'm in the weird position where I've run out of space before tokens. Of course, this string data, and the encoded images, are probably why it's got like that.

Having an editor to automatically output the large strings involved in this approach is quite helpful.

I guess the next step may be to encode the values as unicode (assuming they fit in a byte or two) to save even more space, but I haven't tried that yet and I don't know how the PICO-8 compression would react; it might not result in much of a saving.

Game modes, _draw and _update

There are three game modes: title, start and play. I re-used a set_fade function to move between these which deals with fading the palette in and out and changing the mode. You can pass it various functions and arguments to execute as soon as it's called, when it's faded to black and when it's faded up. I think. I can't remember how it works, but it seems to do what I want it to...

As with most of my carts, in the _update and _draw functions there are a few things that get done every frame, whatever the mode.
_update60: pulse and fr variables that control animation and periodic effects. I've used these since P8C-BUN, they work very well for little PICO-8 games (when trying to write bigger things e.g. using Love2D they don't and they are a pain to remove, but that's a whole different story).

Screen shake is updated here as well, inline since there's no need to call it in a function (and some token-saving habits are hard to shake ;) ):

-- update_ss()
 g_ss.x+=g_ss.vx
 g_ss.y+=g_ss.vy

 g_ss.r,g_ss.b=g_ss.vx*0.6-g_ss.x/5,g_ss.vy*0.6-g_ss.y/5
 if abs(g_ss.vx)+abs(g_ss.vy)<0.1 then
  g_ss.x,g_ss.y=0,0
 end

There's no real reason not to make this separate variables instead of a table (g_ss_s vs g_ss.vx), but I never needed to do it. The conditional at the end of this snippet makes sure that the camera doesn't get "stuck" slightly off centre.
My "cam" function that wraps the PICO=8 camera() native function adds the screen shake values any time I change drawing origin and ensure that everything shakes.
There's a "shake" function that actually kicks it off.

Then it calls whatever update function is currently set.

_draw()
This resets the palette and camera and calls the current draw function. It also does the lightning - which is the same in every mode.
Randomly, a lightning strike is triggered, which consists of the screen being cleared to white, sfx triggered and countdown set. For each frame the countdown hasn't reached zero, the screen palette is set to a pre-determined set of lighter colours. Very simple, but works quite nicely. Looking at it now, I'm tempted to see if I could make the lightning fade more gradual by having more stages. I could feed the palette through the lighter map more than once, just like how the fade to black works or set up a table with gradually lighter palettes.

Most of the time, the palette is darkened to make the lightning more of a contrast and attempt to give the game a slightly more spooky atmosphere. Yellow is the exception to this (the orange is fairly bright too) so I had to be careful where I used that - it's supposed to highlight the candles that the kittens need to find and make the monster's eyes (and the pumpkin) a bit more scary.

In the menu, you can switch the storm off and that restores the normal palette and stops the lightning.

Title

As mentioned above, I started with the DtCT code and was writing a Christmas game, so for ages this section was untouched from that. When I embarked on the Halloween project this was the first thing I played with though.

Snow to rain

I altered the snow code to update multiple times per iteration. This makes two things happen: the raindrops appear to fall faster and makes them appear to be elongated. I tweaked the number of drops to make sure that it still ran fast enough and noticed that they were getting "caught" on the letters to the extent that the rain appeared to stop. So I hijacked the "simpler snow" (which also became rain) I'd added to the play game mode to fall constantly in the background and not interact with the letters. Speeding up the letters moving helps to hide this problem too.

Titles

There are some pal calls to make the letters a more spooky colour - I didn't change the image asset at all apart from making a K into an H and the dots on the 'i's into skulls.
For ages it still talked about Christmas after that until I could face updating the title data to be the right words...
For the previous game I'd entered this all by hand with a lot of trial and error, but having used an editor for the rest of the game I couldn't be bothered doing it by hand again. I also couldn't be bothered adding all the code to my editor that I'd need to draw the titles so instead I added it straight into the game mode and commented out the lines that move the titles about. It's pretty crude, but got the job done far faster than doing it by hand.

The code is still there, commented out.

This mode also uses the screen fade routine with one of the variants described here for added wibbliness. This is the same effect as used in PICO Space.

Start

I pulled in the simpler rain code and screen fade for this screen too. Writing text and getting it to fit on the screen takes ages - I really should find a nicer way to edit it e.g. in PICO-8 itself. P8SCII means that the whole description is a single string. I added the control descriptions, ghosts and kitten heads at the end when I knew I wasn't short of tokens.

Play

I implemented this constantly watching out for the moment when performance would drop enough that I'd need to do some optimisation, most likely to draw/update portions of the house at a time, instead of just updating and drawing everything every frame.
I considered various strategies to bucket entities, update on alternating frames etc.

The moment never came. The game draws and updates everything, every single frame and only rarely breaks 90% CPU, even now I stopped worrying about it.

I was disappointed and relieved at the same time :)

Update

  • move enemies: note the pumpkin and the skulls are all considered to be pumpkins. I probably didn't need to do that in the end, but that's the way it is.
  • move kittens and check how they're interacting with platforms, floors etc.
    -- platforms are only impassible from above if the player isn't pushing down 'Mario'-style (I think, never played Mario much as I never had a Nintendo as a kid).
    -- floors are impassible from above and below
    -- walls are impassible from left or right
    There are many, many platforms and not many floors or walls
  • check if kittens have hit anything with their bops and react
    -- spawns some particles
    -- converts decorations to "fallers"
    I've just noticed I still have some of the bonus code in there even though it isn't used in this game...

Also, you can see where I hacked the collision routine for the kittens (in_rect) with an unexpected "+8" because for some reason the kittens are drawn 8 pixels lower than you'd expect and I had to add that to make the collisions with the monsters work(!).

Draw

The trickiest bit of this was deciding when to swap between the house and game sprites. In the end it uses something like the following sequence, starting with the house sprites loaded into PICO-8 sprite memory:

  • draw background
  • load game sprites
  • draw decorations, candles
  • draw monsters
  • draw kittens
  • draw HUD
  • load house sprites
  • draw stairs and parts of doorways in foreground

This means there's only two loads, mid-frame. If you look closely you can see that the scores go behind the stairs, but to fix that I'd need two more loads and it'd never run at 60fps. Maybe for Picotron...

When the game ends, I use the title sprites for the "Happy Halloween" message so they need to be loaded in and that pushes the CPU just over the 60fps limit into 30fps too :( It's not too bad - you can still run about the house okay, I think - and I didn't have time to fix it. I'm not sure that I even can.

Conclusion

On the whole it works and people seem to like it. I could improve the code a lot and if I could work out a way to save compressed size and take advantage of the spare tokens I suspect I could add some more - maybe more options for more varied gameplay would be nice. I think I need to work on gameplay and prototyping that earlier and worry less about coding, squeezing in assets and graphics until I have that a bit more "down".

The editor worked really well and I was pleased with the data (un)packing functions.

Now I have to decide if I still want to make the Christmas game with the kittens and how I could make it better. I guess I could mahe the house/playing area even bigger. Only the background graphics are restricted by the map grid - just saying...

Unminified Code

Warning: this is pretty messy and not v efficient. I haven't really done any token optimisation on it at all.
If you have any questions about it, please comment and we can puzzle out what on earth it does together :)

For reference only...


-- halloween_horrors
-- by drakeblue


--returns random integer (vs float from rnd)
function rint(...) return rndr(...)&-1 end


-- rndr(t,r)==rnd(t)+r
function rndr(t,r)
return rnd(t)+(r and r or 0)
end


-- generates a random direction
function rnd_dir(m)
local angle,d=rnd(),rnd(m)
return dsin(angle),dcos(angle)
end


function buttonp(b)
local p=btnp(b)
if b==❎ and p then
sfx(62,3,10,2)
elseif b==🅾️ and p then
sfx(62,3,7,2)
elseif p then
sfx(62,3,11,1)
end
return p
end


function use_sprites(sprites)
if curr_spr!=sprites then
curr_spr=sprites
local data=sprites.data -- v slightly faster
poke4(0,unpack(data))
end
pal(sprites.pl)
end


decode={

function(rle)
out={}
for i=1,#rle\2 do
for j=0,rle[i2-1] do
add(out,rle[i
2])
end
end
return out
end,
-- decodes data where 0s are rle only
function(rle0)
local out,i,len={},1,#rle0
while i<=len do
local data=rle0[i]
if data==0 then
local rep=rle0[i+1]
for j=0,rep do
add(out,0)
end
i+=2
else
add(out,data)
i+=1
end
end
return out
end
}


-- take list of raw pixel colour values
-- and pack into 4byte chunks for poke4
function pack_sprite_data(data)
local out,val={},0
for m,d in pairs(data) do
val=(val>>>4)|(d<<12)
if m%8==0 then
-- note horrendous swizzle to get pixels in correct order for p8
-- isn't needed below since built into decoding
-- out[(m>>3)-1]=(band(val,0xf0f0.f0f0)>>4) + (band(val,0x0f0f.0f0f)<<4)
add(out,val)
val=0
end
end
return out
end


expand8bit={
--expands 8bit data already decoded from str
[2]=function(data,ptr,d_len)
local out={}
for i=ptr,#data do
local val=data[i]
for j=3,0,-1 do
add(out,(val>>>(j2))&0x3)
d_len-=1
if d_len==0 then return out end
end
end
return out
end,
--expands 8bit data already decoded from str
[3]=function(data,ptr,d_len)
local out,val,md={},0,(ptr+2)%3
for i=ptr,#data do
val=(val<<8)+(data[i]>>8)
if i%3==md then
for j=0,7 do
add(out,(val>>>((7-j)
3-8))&7)
d_len-=1
if(d_len==0) return out
end
end
end
return out
end,
--expands 8bit data already decoded from str
[4]=function(data,ptr,d_len)
local out={}
for i=ptr,#data do
add(out,(data[i]>>>4)&0xf)
add(out,data[i]&0xf)
end
return out
end
}


function unpack_dbi(dbi_data)
-- read first value to get trans
local trans, raw=ord(sub(dbi_data,1,1)),{}
for r=2,#dbi_data do
raw[r-2]=(ord(sub(dbi_data,r,r))-trans)&0xff
end
local width,height,cols,pl,ptr=raw[0],raw[1],raw[2],{},3
local bpp=(cols-8>0) and 4 or (cols-4>0) and 3 or (cols-2>0) and 2 or 1
-- printh("w:"..width.." h:"..height.." cols:"..cols)
for i=0,cols-1 do
pl[i]=raw[ptr]
ptr+=1
-- printh(pl[i])
end
local comp=raw[ptr]
ptr+=1
--16bit value for num values to extract from 8bit data
local d_len=(raw[ptr]<<8)+raw[ptr+1]
ptr+=2
-- print("d_len:"..d_len)
local d_comp = expand8bitbpp
-- debug expand 8bit
-- local d_str=''
--for d=1,#d_rle0 do
-- d_str=d_str..d_rle0[d]
--end
--printh(d_str)

-- debug rle decode
--local d_raw=decode_rle(d_rle0)
--local d_str=''
--for d=1,#d_raw do
--d_str=d_str..d_raw[d]
--end
--printh(d_str)
local sprites = {data=pack_sprite_data(decodecomp),pl=pl,w=width,h=height}
--use_sprites(sprites)
--cls()
--spr(0,0,0,16,16)
--while not btnp(❎) do end
return sprites
end

-- resets the draw palette
-- without touching the screen palette
-- like pal() would

function reset_dr_pal()
poke4(0x5f00,0x0302.0110,0x0706.0504,0x0b0a.0908,0x0f0e.0d0c)
end


function wait(w)
for i=0,w do flip() end
end


-- called from fade, try not to call directly
function set_upd_func(new_upd_func)
upd_func=new_upd_func
end


-- called from fade, try not to call directly
function set_dr_func(new_dr_func)
dr_func=new_dr_func
end


function set_fade(speed,
down_funcs,down_params,
up_funcs,up_params)
set_upd_func(blank)
set_dr_func(blank)
fd={
val=1,
dir=speed,
down_funcs=down_funcs,
down_params=down_params,
up_funcs=up_funcs,
up_params=up_params
}
end


function fade()
if fd.val==-10 then
return --not fading
else
if fd.val<=0 then -- end of fade
-- do changes for end of fade
if fd.up_funcs then for i in pairs(fd.up_funcs) do fd.up_funcsi end end
fd.val=-10
return
--todo: restore buttonp(❎) or --skip
elseif fd.val>8 then
-- faded to black
-- so do fade down change while invisible
-- and start fade up
if fd.down_funcs then for i in pairs(fd.down_funcs) do fd.down_funcsi end end
fd.dir=-fd.dir
fd.val=8 -- make sure won't get hit again
end
-- set the colours (actual fade)
fade_pal(fd.val)
fd.val+=fd.dir
end
end


-- fades screen palette for light levels (global)
function fade_pal(lvl)
local ad=0x5f10
for c=0,15 do
-- for all colours
for i=1,lvl do
-- for requested levels
poke(ad,scpal_map[@ad])
end
ad+=1
end
end


-- gets length of printed string
-- takes into account special chars etc
-- doesn't work over multiple lines
-- or with strings that print longer than
-- 256 pixels...
-- assumes y=128 is off screen
-- be careful of camera()
function len_print(s)
print(s..'\0',0,128)
return @0x5f26
end


-- doesn't use replaced m and w for speed
function print_centre(s,y,c,out)
fun=out and pout or print
fun(s,63-len_print(s)\2,y,c)
end


function unpack_split(s)
return unpack(split(s))
end


function pout(s,x,y,c,o) -- 30 tokens, 5.7 seconds
color(o)
?'-f'..s..'\^g-h'..s..'\^g|f'..s..'\^g|h'..s,x,y
?s,x,y,c
end


-- checks if two points are in a rect with sides t and s
-- if t is not passed then treated as square

-- note the "-8" for the y axis is an enormous hack to get round
-- whatever the hell you were doing drawing the kittens
-- like you did, nowhere near their origin. This func is only
-- used for kitten collision atm so you can get away with it.

-- todo - remove the many adjustments for kitten position
function in_rect(a,b,s,t)
return abs(a.x-b.x)<s and abs(a.y-b.y-8)<(t or s)
end


--returns random integer (vs float from rnd)
function rint(...) return rndr(...)&-1 end


-- rndr(t,r)==rnd(t)+r
function rndr(t,r)
return rnd(t)+(r or 0)
end

function _update60()
pulse=(pulse+1)%192
fr=pulse\8%3

-- update_ss()
g_ss.x+=g_ss.vx
g_ss.y+=g_ss.vy

g_ss.r,g_ss.b=g_ss.vx0.6-g_ss.x/5,g_ss.vy0.6-g_ss.y/5
if abs(g_ss.vx)+abs(g_ss.vy)<0.1 then
g_ss.x,g_ss.y=0,0
end
upd_func()

end

function blank()
end

-- screen shake

function shake(d)
-- r=vx, b==vy
local x,y=rnd_dir(d)
g_ss.r+=x
g_ss.b+=y
end

function cam(x,y)
camera((x or 0)+g_ss.x,(y or 0)+g_ss.y)
end

function kit_flinch(k,e)
if k.flinch==0 then
sfx(60,3,rnd{20,24,28},4)
k.flinch=45
k.face=k.x<e.x
k.vx=k.face and -1.5 or 1.5
shake(0.2)
end
end

-- has kitten hit something
function kit_hit(k,t,w,h)
return abs(k.y-t.y-8)<(h or 16) and abs(k.x-t.x+(k.face and 18 or -10))<(w or 16)
end

function up_play()

if not fin then
-- move baddies
for g,ghost in pairs(g_ghosts) do
local newx=ghost.x+ghost.vx
if ghost.x>1000 then
ghost.vx=-1
elseif (ghost.y<128 and ghost.x<260) or ghost.x<128 then
ghost.vx=1
end
ghost.x=ghost.x+ghost.vx
ghost.y+=sin(pulse/192)/7
end

for s,spid in pairs(g_spids) do
local smin, smax
if spid.y<128 then -- upstairs spid
smin,smax=16,108
else -- downstairs spid
smin,smax=144,240
end

if spid.y<smin then
 spid.vy=1
 spid.y=smin
elseif spid.y>smax then
 spid.vy=-1
 spid.y=smax
else
 spid.y+=spid.vy
end

end

-- bats
local bat=g_bats[pulse&15]
if bat and rint(2)>0 and bat.y>20 then
bat.vy=-rint(3,1)
end
for b,bat in pairs(g_bats) do
bat.vy=min(1.5,bat.vy+0.1)
bat.y=mid(2,bat.vy0.8+bat.y,100)
bat.vx=mid(-0.8,bat.vx
0.8+rndr(0.2,-0.1),0.8)
bat.x+=bat.vx
if bat.x<260 then bat.x=260 bat.vx=1 end
if bat.x>604 then bat.x=604 bat.vx=-1 end
end
end -- not fin

-- pumpkins and skulls
for p,pumpkin in pairs(g_pumpkins) do
local dist,dx,dy,kit,best_dist=32767
if not fin and abs(g_cam.x-pumpkin.hx)<100 and abs(g_cam.y-pumpkin.hy)<100 then
for pl=st_pl,end_pl do
-- find closest kitten
local f=kits[pl]
local this_dx,this_dy=f.x-pumpkin.x,f.y-pumpkin.y
local this_dist=this_dxthis_dx+this_dythis_dy
if this_dist<dist then
kit,dist,dx,dy=f,this_dist,this_dx,this_dy
end
end

if pumpkin.c>0 then
 dx,dy=-dx,-dy
end
pumpkin.c=max(pumpkin.c-1)

-- fly at closest kitten
dist=sqrt(dist)
pumpkin.vx=mid(1,0.2*dx/dist+pumpkin.vx*0.8,-1)
pumpkin.vy=mid(1,0.2*dy/dist+pumpkin.vy*0.8,-1)

local an=pulse+p*60
pumpkin.x+=pumpkin.vx+cos(an/192)
pumpkin.y=min(236,pumpkin.y+pumpkin.vy+sin(an/96))

add(parts,{x=pumpkin.x+pumpkin.w\2,y=pumpkin.y+5,vx=rndr(4,-2),vy=rndr(4,-4),life=rndr(30,30),
  c=rnd(p>1 and {10,8,7,6} or {9,11})})

else
-- send back to home
pumpkin.x+=(pumpkin.hx-pumpkin.x)0.05
pumpkin.y+=(pumpkin.hy-pumpkin.y)
0.05
end
end

-- for each player's kitten 'f' for frankie
for pl=st_pl,end_pl do
-- get kitten and get 0 or 1 for current player
local f,player=kits[pl],(st_pl==end_pl) and 0 or pl-1

-- collisions with platforms
f.gr=false
if f.vy>=0 then
for pl,plat in pairs(g_plats) do
if plat.x<f.x+14 and plat.r>f.x+6 and plat.y<f.y+4 and plat.b>f.y+4 then
if btn(⬇️,player) and f.flinch<30 then
f.gr=false
f.vy=0.2
f.y+=2
else
f.gr=true
f.vy=0
f.y=plat.y-4
end
end
end
end

-- collisions with floors (including stairs)
-- can't go through floors from bottom
for pl=#g_floors,1,-1 do
-- for pl=4,1,-1 do
local plat=g_floors[pl]
plat.draw=nil
if plat.x<f.x+14 and plat.r>f.x+6 and plat.y<f.y+4 and plat.b>f.y+2 then
local dl,dr,dt,db=f.x+14-plat.x,plat.r-f.x+6,f.y+4-plat.y,plat.b-f.y+2
if dt<db then -- dt<db
f.gr=true
f.floor=1
f.vy=0
f.y=plat.y-4
plat.draw=1
else
f.vy=0.3
f.y=max(plat.b,f.y)
end
end
end

-- special line for stairs
-- 648,232,860,128
local sx,sy,ex,ey=unpack(g_stairs_line)
-- grad=(ey-sy)/(ex-sx)(f.x-sx)+sy
if sx<f.x+14 and ex>f.x+6 then
-- check if under line
-- todo: simplify maths
local ly=(ey-sy)/(ex-sx)
(f.x-sx)+sy
-- if 634<f.x and 851>f.x then
-- local ly=-0.49760*(f.x-648)+232
if f.y<ly+4 and f.y>ly then
f.y=ly+5
f.vy=0.3
-- f.above=1
else
-- f.above=0
end
end

-- collisions with walls
for pl,plat in pairs(g_walls) do
if plat.y<f.y+4 and plat.b>f.y then
if plat.x<f.x+24 and plat.r>f.x then
f.vx=0
f.vy=max(f.vy)
if f.x-plat.x<plat.r-f.x then
f.x=plat.x-24
else
f.x=plat.r
end
end
end
end

-- "input"
-- x
local traction=(f.y>240 or not f.gr) and 0.05 or 0.2

if btn(⬅️,player) and f.flinch<30 then
-- run/move left
f.vx=max(-2,f.vx-traction)
f.face=false
f.sit=0
elseif btn(➡️,player) and f.flinch<30 then
-- run/move right
f.vx=min(2,f.vx+traction)
f.face=true
f.sit=0
else
f.vx*=0.9
f.sit=min(100,f.sit+1)
end

-- y
if btnp(⬆️,player) or btnp(🅾️,player) and f.flinch<30 then
f.sit=0
if f.gr then
f.vy=-3.49
sfx(62,3,28,4)
else
f.vy+=0.2
end
elseif btnp(⬇️,player) and f.flinch<30 and f.gr and not f.floor then
f.sit=0
else
f.vy=min(3,f.vy+0.2)
end

-- hitting
if btnp(❎,player) and not fin and f.flinch<30 and f.hit==0 then
f.hit=20
f.vx=0
f.sit=0
sfx(62,3,rnd{0,3},4)
end

f.hit=max(f.hit-1)

-- kitten "physics"

if st_pl==end_pl then
f.x+=f.vx
f.y+=f.vy
else
-- resolve kittens getting too far from each other
local other=pl==st_pl and kits[end_pl] or kits[st_pl]
local dx=abs(f.x+f.vx-other.x)
local dy=abs(f.y+f.vy-other.y)
if dx<120 then
f.x+=f.vx
end
if dy>120 then
f.x=other.x
f.y=other.y
else
f.y+=f.vy
end
end

-- check if kitten has hit anything
local hit

if f.hit>0 then
for c,candle in pairs(g_candles) do
if not candle.out then
if kit_hit(f,candle) then
candle.out=1
g_num_candles-=1
f.score+=10
if g_num_candles==0 then
fin=60
end
end
end
end

for s,sweet in pairs(g_sweets) do
if not sweet.hit and not sweet.dead and kit_hit(f,sweet,8,8) then
sweet.hit,hit=1,1
if sweet.spr>111 then -- double height
add(g_sweets,{x=sweet.x,y=sweet.y-8,vx=rndr(2,f.face and 0 or -2),vy=rndr(2,-1),spr=sweet.spr+48,hit=1})
end
sweet.spr+=64
sweet.vx,sweet.vy=rndr(2,f.face and 0 or -2),rndr(2,-1)
f.score+=1
g_num_broken+=1
end
end

for s,spid in pairs(g_spids) do
if kit_hit(f,spid) then
spid.vy=f.y>spid.y+6 and -2 or 2
spid.y+=spid.vy*3
hit=1

end

end

for g,ghost in pairs(g_ghosts) do
if kit_hit(f,ghost) then
ghost.vx=f.x>ghost.x and -1 or 1
ghost.x+=ghost.vx*5
hit=1
end
end

local function bat_hit(f,bat,w)
if kit_hit(f,bat,w) then
-- get direction away from kit
local dx,dy=bat.x-f.x,bat.y-f.y
local n,hit=sqrt(dxdx+dydy)0.1,1
bat.vx=dx/n
bat.vy=dy/n
bat.x+=bat.vx
2
bat.y+=bat.vy*2
return 1
end
end

for b,bat in pairs(g_bats) do
hit=bat_hit(f,bat) or hit
end
for p,pumpkin in pairs(g_pumpkins) do
if bat_hit(f,pumpkin,pumpkin.w) then
hit=1
pumpkin.c=40
end
end
end

if hit then
sfx(62,2,rnd{12,16},4)
local px=f.face and (f.x+24) or (f.x-7)
for i=0,15 do
add(parts,{x=px+rndr(6,-3),y=f.y+rnd(5),vx=rndr(4,-2),vy=rndr(4,-3),life=rndr(30,30),
c=rnd(15)})
end
end

f.bon_cool=max(f.bon_cool-1)
if(f.bon_cool==0)f.bonus=1

-- collisions with monsters
if g_end_cool<1 then
if buttonp(❎) then
set_fade(0.2,{run},{mus and 1 or 0})
end
else -- not fin
-- bats
for s,spid in pairs(g_bats) do
if in_rect(f,spid,16) then
kit_flinch(f,spid)
end
end

-- ghosts
for g,ghost in pairs(g_ghosts) do
if in_rect(f,ghost,16,12) then
kit_flinch(f,ghost)
end
end

-- spids
for s,spid in pairs(g_spids) do
if in_rect(f,spid,16) then
kit_flinch(f,spid)
end
end

for p,pumpkin in pairs(g_pumpkins) do
if in_rect(f,pumpkin,pumpkin.w) then
kit_flinch(f,pumpkin)
end
end
end -- if fin
end -- kits

-- decorations that are falling
for s,sweet in pairs(g_sweets) do
if sweet.hit then
local hit_floor
for pl,plat in pairs(g_floors) do
if plat.x-4<sweet.x and plat.r+4>sweet.x then
if plat.y-4<sweet.y and plat.b+4>sweet.y then
hit_floor=1
end
end
end

if hit_floor then
sweet.vy=-sweet.vy0.5
sfx(62,3,27,1)
if abs(sweet.vy)<0.5 then sweet.dead,sweet.hit=1 end
else
sweet.vy=min(sweet.vy+0.2,4)
end
sweet.vx
=0.9
sweet.y+=sweet.vy
sweet.x+=sweet.vx
end
-- removing fallers hasn't been required so far
-- faller.life-=1
-- if faller.life<=0 then
-- -- overwrite with last faller
-- fallers[f]=fallers[#fallers]
-- fallers[#fallers]=nil
-- end
end

if fin==1 then
g_end_time=time()-g_time
end

-- bonus
-- for b=#bons,1,-1 do
-- local bon=bons[b]
-- bon.life-=1
-- if bon.life==0 then
-- bons[b]=bons[#bons]
-- bons[#bons]=nil
-- else
-- bon.y+=bon.vy
-- end
-- end

end

--function break_dec(k,baub)
-- -- spawn particles, bonuses and falling dec here
-- local decx,decy=baub.dec.x,baub.dec.y
-- if not fin then
-- k.score+=k.bonus
-- k.bon_cool=60
-- add(bons,{x=decx,y=decy,vy=-0.2,life=30,sp=65+k.bonus})
-- k.bonus=min(k.bonus+1,5)
-- end
-- local faller=add(fallers,{x=decx,y=decy,vx=k.face and rnd(2) or -rnd(2),vy=0,spr=baub.spr+80,live=true})
-- for i=0,20 do
-- add(parts,{x=decx,y=decy,vx=faller.vx+rnd(4)-2,vy=rnd(4)-2,life=rnd(30)+30,
-- c=sget((faller.spr%16)8+2+rint(4),(faller.spr\16)8+2+rint(4))})
-- end
--
--end

function _draw()
pal()

-- lightning?
dr_func()
if rint(300)==1 and not g_storm then
lightning=rint(20)+5
cls(7)
shake(1)
if not mus then sfx(rnd{61,63},0) end
elseif lightning>0 then
local lightning_cols=g_lightning_cols
for l,col in pairs(lightning_cols) do
pal(l-1,col,1)
end
else
fade()
if not g_storm and fd.val<1 then
local dark_cols=g_dark_cols
for l,col in pairs(dark_cols) do
pal(l-1,col,1)
end
if dr_func~=dr_play then pal(2,2,1) pal(8,8,1) end
end
end
lightning=max(lightning-1)
cam()
-- print(stat(0),0,112,7)
end

function dr_start()
scr_fade(pulse)
local wind=sin(pulse/433)*sin(pulse/192)/6
for i=0,2 do
for s,sno in pairs(snowback) do
sno[2]=(sno[2]+1)%128
sno[1]=(sno[1]+wind)%128
pset(sno[1],sno[2],rnd{1,5,6})
end
end

palt(15)
palt(0,1)
spr(15,pulse<20 and 1 or 0,119,1,1,pulse<20)
pal(7,6)
spr((pulse\8)%2==1 and 192 or 194,pulse-16,60+sin(pulse/192)6,2,2)
spr((pulse\8)%2==1 and 194 or 192,144-pulse,30+cos(pulse/96)
6,2,2,1)
pal(7,7)
pal(5,4)
pal(6,15)
spr(15,pulse>170 and 121 or 120,119,1,1,pulse>170)
pal(5,5)
pal(6,6)

print("the fan-enily have decorated the\nhouse for hallov-eve'en tonight\nbut then gone out for food.\n|jalas, the \faspooky candles \f7they\nbought are genuinely n-enagical\nand have n-enade \f8creepy n-enonsters\f7\nappear nov-ev that it's dark.\n|jput out the candles and banish\nthe n-enonsters.\n|jand if son-ene things get broken\nit serves then-en right for\nleaving you at hon-ene alone!",3,3,7)

rectfill(54,106,54,104,13)
rectfill(55,106,70,106)
rectfill(71,104,71,106)
pout("hit",58,104,7)

rectfill(36,93,36,96,13)
rectfill(20,92,107,92)
rectfill(91,93,91,96)
line(19,93,16,94)
line(108,93,111,94)
pout("jun-enp",52,90,7)

pout('p1',1,88,7)
pout('p2',120,88,10)

print("⬅️-m➡️|d-1⬆️-8|m⬇️",1,98,6)
print(pulse<96 and "\f7+\f6🅾️(z)\f7+\f6❎" or "\f7+\f6🅾️(n)\f7+\f6n-en",23,98)
--p2 instructions
print(pulse<96 and "⬅️-m➡️|d-1⬆️-8|m⬇️" or "s-sf|d-5e-c|md",106,98,15)
print(pulse<96 and "\ff❎\f7+\ff🅾️tab\f7+" or "\ffq\f7+\ff🅾️(v-ev)\f7+",70,98)

print('⬅️ \f6play frankie \ffplay philly',1,112,7+fr,1)
print('➡️',120,112,10-fr,1)
print_centre('❎ for a 2 kitten game',122,11+fr, 1)

for i=0,3 do
for s=1,#snowfront\2 do
local sno=snowfront[s]
sno[2]=(sno[2]+1)%128
sno[1]=(sno[1]+wind)%128
pset(sno[1],sno[2],rnd{5,13,12})
end
end

if buttonp(⬅️) then st_pl=1 end_pl=1 set_fade(0.2,{set_dr_func,music,decorate_house},{dr_play,mus and 22 or -1},{set_upd_func},{up_play}) end
if buttonp(➡️) then st_pl=2 end_pl=2 set_fade(0.2,{set_dr_func,music,decorate_house},{dr_play,mus and 22 or -1},{set_upd_func},{up_play}) end
if buttonp(❎) then st_pl=1 end_pl=2 set_fade(0.2,{set_dr_func,music,decorate_house},{dr_play,mus and 22 or -1},{set_upd_func},{up_play}) end

end

function dr_title()
--sc=4
--if btnp(⬅️,0) then if btn(❎) then ls[sc][g_letter][4]-=1 else ls[sc][g_letter][2]-=1 end end
--if btnp(➡️,0) then if btn(❎) then ls[sc][g_letter][4]+=1 else ls[sc][g_letter][2]+=1 end end
--if btnp(⬆️,0) then if btn(❎) then ls[sc][g_letter][5]-=1 else ls[sc][g_letter][3]-=1 end end
--if btnp(⬇️,0) then if btn(❎) then ls[sc][g_letter][5]+=1 else ls[sc][g_letter][3]+=1 end end

--if btnp(⬅️,1) then ls[sc][g_letter][1]-=1 end
--if btnp(➡️,1) then ls[sc][g_letter][1]+=1 end
--if btnp(⬆️,1) then g_letter=max(1,g_letter-1) end
--if btnp(⬇️,1) then if g_letter<#ls[sc] then g_letter+=1 else add(ls[sc],{0,0,0,2,2}) g_letter+=1 end end

scr_fade(pulse)
cool-=2.5
if cool<0 then sc=sc%(#ls-1)+1 cool=512 end
pal(title.pl)
local offset=abs(2560/(256-(cool-256)%512))/2 - 5
-- local offset=0

local wind=sin(pulse/433)*sin(pulse/192)/6
for i=0,2 do
for s,sno in pairs(snowback) do
sno[2]=(sno[2]+1)%128
sno[1]=(sno[1]+wind)%128
pset(sno[1],sno[2],6)
end
end

for lnum=1,#ls[sc] do
pal(1,2)
pal(3,8)
pal(2,1)
pal(4,1)
local l=ls[sc][lnum]
spr(l[1],l[2],l[3]+offset,l[4],l[5])
end

for i=0,2 do

for p=#snow,1,-1 do
local s=snow[p]
if s.fall then
local olds={x=s.x,y=s.y}
s.x+=wind
local pix=pget(s.x,s.y+1)
if s.y>126 then
-- fallen off bottom
-- pset(olds.x,olds.y,0)
snow[p].x=rnd(128)
snow[p].y=0
elseif pix>0 then
-- hit something
pix=pget(s.x-1,s.y+1)
if pix>0 then
local pix=pget(s.x+1,s.y+1)
if pix>0 then
s.fall=false
else
s.x+=1
snow_fall(s,olds)
end
else
s.x-=1
snow_fall(s,olds)
end
else
-- falling
snow_fall(s,olds)
end
else
-- pset(s.x,s.y,12)
if pget(s.x,s.y+1)<1 then
s.fall=true
end
end
end
end

if pulse\32%2==0 then
pout("press ❎ ",48,122,8,2)
end
if buttonp(❎) then set_fade(0.2,{set_dr_func,use_sprites},{dr_start,game},{set_upd_func},{blank}) end
--if buttonp(🅾️) then
-- local out=''
-- for l in all(ls[sc]) do
-- out..=l[1]..','..l[2]..','..l[3]..','..l[4]..','..l[5]..','
-- end
-- printh(out)
--end
end

function snow_fall(s,olds)
-- pal(5,5)
-- pset(olds.x,olds.y,5)
s.y+=(rint(1.4,2))
pset(s.x,s.y,rnd{12,13})
end

-- formats a passed time as m:ss
function time_string(t)
local t=flr(t)
return t\60 ..":"..(t%60<10 and 0 or '')..t%60
end

function dr_play()

local ball={x=(kits[st_pl].x+kits[end_pl].x+20)\2,
y=(kits[st_pl].y+kits[end_pl].y+20)\2}

g_cam.x+=(ball.x-g_cam.x)\4
if ball.x>700 and ball.x<900 then
-- stairwell
g_cam.y=mid(62,g_cam.y+(ball.y-g_cam.y)/28,192)
else
if ball.y>129 then
-- downstairs
g_cam.x=mid(60,g_cam.x,964)
g_cam.y+=mid(-2,(193-g_cam.y)/28,2)
else
-- upstairs
g_cam.x=mid(318,g_cam.x,964)
g_cam.y+=mid(-2,(62-g_cam.y)/28,2)
end
end

cam(g_cam.x-64,g_cam.y-64)

cls()

-- rain
local wind=sin(pulse/192)/8
for i=0,2 do
for _,s in pairs(snowback) do
s[1],s[2]=(s[1]+wind)%128,(s[2]+1)%120
pset(s[1],s[2]+128,13)
end
end

-- paint walls
for p,paint in pairs(g_paints) do
rectfill(paint.x,paint.y,paint.r,paint.b,paint.c)
end

use_sprites(house)
-- floor sprites
for i=44,124 do
-- upper floor skirting boards
spr(1,i8,112,1,1)
end
for i=33,126 do
-- upper floor carpet
spr(40,i
8,120,1,1)
end
for i=91,93 do
-- lower coving
spr(1,i8,136,1,1)
end
for i=104,105 do
-- lower coving
spr(1,i
8,136,1,1)
end

for i=33,124 do
-- lower skirting boards
spr(1,i8,240,1,1)
end
for i=14,126 do
-- lower floorboards
spr(17,i
8,248,1,1)
end

-- outlines for floors
-- rectfill(256,125,750,126,6)--1st floor
-- rectfill(840,125,1028,126)--1st floor
-- rectfill(-4,256,1028,264,6)--ground

-- background (pretty much everything apart from stairs and doorframes)
map(0,0,0,0,128,32,1)

rectfill(256,128,750,128,13)--1st floor shadow
rectfill(840,128,1028,128)--1st floor shadow

-- walls
-- rectfill(-4,-4,0,260,6)-- left wall of garden
-- rectfill(252,-4,256,128,6)-- left wall of house
-- rectfill(1024,-4,1028,255,6)--right wall

reset_dr_pal()

use_sprites(game)

-- bonus indicators
for b=1,#bons do
local bon=bons[b]
if bon.life\4%3==1 then
spr(bon.sp,bon.x,bon.y)
end
end

-- draw candles
-- draw monsters
palt(15,1)
palt(0,false)

-- draw decorations

for s,sweet in pairs(g_sweets) do
local sp=sweet.spr
if sp>111 and sp<128 then
spr(sp-16,sweet.x,sweet.y-8)
end
spr(sp,sweet.x,sweet.y)
end

for c,candle in pairs(g_candles) do
if candle.out then
pal(10,9)
pal(9,8)
end
if not candle.out then
spr(236,candle.x,candle.y,2,2)
local p=(pulse+c*7)\4
spr(46+p%2,candle.x+5,candle.y-4,1,1,fr%2==0)
else
spr(234,candle.x,candle.y,2,2)
pal(10,10)
pal(9,9)
end
end

local dim=1
if fin then
dim=flr(fin/60)
end

for s,spid in pairs(g_spids) do
local weby=spid.y<120 and 16 or 144
spr(202,spid.x-8,spid.y<120 and 16 or 144,4,2,s&1==0)-- web
rectfill(spid.x+7,weby+4,spid.x+7,spid.y+1,7)--thread
spr(230+(fin and 0 or 2(pulse\4%8<3 and 1 or 0)),spid.x,fin and (spid.y-weby)dim+weby or spid.y,2,2)-- spid
if not fin and rint(30)==0 then
add(parts,{x=spid.x+(rint(2)==0 and 6 or 9),y=spid.y+10,vx=0,vy=1,life=spid.y<128 and (128-spid.y)\3.5 or (250-spid.y)\3.5,c=11})
end
--pout(s,spid.x,spid.y,11)
end
for g,ghost in pairs(g_ghosts) do
spr(fin and 192 or 192+2(pulse\8%2),ghost.x,ghost.y,2,2,ghost.vx<0)
--rect(ghost.x,ghost.y,ghost.x+16,ghost.y+12,9)
--pout(g,ghost.x,ghost.y,7)
end
for b,bat in pairs(g_bats) do
local s=224+2
((pulse\8+b)%4)
if s==230 then s=226 end
spr(fin and 226 or s,bat.x,bat.y,2,2)
--pout(b,bat.x,bat.y,8)
if not fin and rint(30)==0 then
add(parts,{x=bat.x+(rint(2)==0 and 6 or 9),y=bat.y+10,vx=0,vy=1,life=bat.y<128 and (128-bat.y)\3.5 or (250-spid.y)\3.5,c=8})
end
end

-- pumpkins and skulls
for p,pumpkin in pairs(g_pumpkins) do
spr((fin or fr%3~=1) and pumpkin.sp1 or pumpkin.sp2,pumpkin.x,pumpkin.y,pumpkin.w\8,2)
end

-- end draw monsters

if not fin or fin>0 then
-- statuses
cam()

-- candle for status
spr(236,52,11,2,2)
spr(46+fr%2,57,7,1,1,(pulse\4)%2==0)
local prog=100-flr((#g_decs-g_num_broken)/#g_decs*100)
if prog<100 then
if prog<10 then
prog=' '..prog
else
prog=' '..prog
end
end
pout((g_num_candles<10 and ' ' or '') .. g_num_candles.." "..prog.."%",47,15,fr%2==0 and 10 or 11)
-- show time elapsed
local t=time()-g_time
print_centre(time_string(t),2,10,7)
for pl=st_pl,end_pl do
local f=kits[pl]
if pl==2 then
pal(6,15)
pal(5,4)
end

-- kitten heads
cam(pl==1 and -1 or -118)
spr(15)
if pulse\8==pl*5 then
-- shifty eyes
pset(2,4,3)
pset(5,4)
pset(3,4,1)
pset(6,4)
end
rectfill(8,1,8,4,1)
cam()
pout(f.name,pl==1 and 12 or 93,2,6)
pout("score "..(f.score\100)..((f.score%100)\10)..(f.score%10),pl==1 and 2 or 91,10,f.bon_cool>1 and fr!=1 and 7 or 6)

-- local str
-- if f.above==1 then str='above' else str='below' end
-- print(str,0,100,8)
-- circ(f.x,grad,2,10)
end -- draw kittens (for pl)

pal(5,5)
pal(6,6)
cam(g_cam.x-64,g_cam.y-64)
end

-- draw kittens
for pl=st_pl,end_pl do
if pl==2 then
-- alt colours for philly
pal(5,4)
pal(6,15)
end

local f=kits[pl]
if f.flinch>0 then
f.flinch-=1
if fr!=0 then spr(38,f.x-(f.face and -4 or 8),f.y-10,3,2,f.face) end
elseif f.hit>0 then
spr(f.hit>12 and 38 or 41,f.x-(f.face and -4 or 8),f.y-10,3,2,f.face)
elseif f.gr then
if abs(f.vx)<0.1 and f.sit>10 then
-- sit
spr(9+(((pulse<48 and pl==1)or(pulse>144 and pl==2)) and fr2 or 0),f.x+(f.face and 3 or 0),f.y-8,2,2,f.face)
else
-- run
spr(fr
3,f.x,f.y-10,3,2,f.face)
end
else
if f.vy<0 then
-- jump
spr(32,f.x,f.y-10,3,2,f.face)
else
-- fall
spr(35,f.x,f.y-12,3,2,f.face)
end
end
--pset(f.x,f.y,10)
end

-- dark blue->black for outlines
-- pal(1,0)

-- put palette back
pal(5,5)
pal(6,6)

palt(0,1)
palt(15,false)

for p=#parts,1,-1 do
part=parts[p]
part.life-=1
if part.life<0 then
parts[p]=parts[#parts]
parts[#parts]=nil
end
part.x+=part.vx
part.y+=part.vy
part.vy+=.2
pset(part.x,part.y,part.c)
end

-- draw stairs
use_sprites(house)

-- foreground map items
-- mostly stairs and doorframes
map(0,0,0,0,128,32,2)

reset_drpal()
-- snow
for i=0,3 do
-- local ymax=120+rint(10)
for
,s in pairs(snowfront) do
s[1],s[2]=(s[1]+wind)%112,(s[2]+1)%128
pset(s[1],s[2]+128,rnd{12,13})
end
end

-- show progress in destroying decorations
cam()

if fin==0 then
if g_end_cool<1 then
rectfill(0,118,127,124,2)
print_centre(" press ❎ to play again",119,8+fr)
end

-- show scores
local str=''
if st_pl==end_pl and st_pl==1 then
str="frankie scores "..kits[1].score.."!"
elseif st_pl==end_pl and st_pl==2 then
str="philly scores "..kits[2].score.."!"
elseif kits[1].score>kits[2].score then
str="frankie v-evins: "..kits[1].score.." to "..kits[2].score.."!"
elseif kits[2].score>kits[1].score then
str="philly v-evins: "..kits[2].score.." to "..kits[1].score.."!"
else
str="both kittens drav-ev v-evith: "..kits[1].score.."!"
end
rectfill(0,8,127,14,9)
print_centre(str,9,10+fr)

-- show time
rectfill(0,16,127,22,11)
print_centre("your tin-ene: "..time_string(g_end_time),17,13+fr)

-- happy halloween
use_sprites(title)

pal(drpal_map)
for l in all(ls[#ls]) do
spr(l[1],l[2]+3,l[3]+3,l[4],l[5])
end
pal(title.pl)
for l,letter in pairs(ls[#ls]) do
pal(1,g_title_cols[(l+pulse\16)%3+1])
spr(letter[1],letter[2],letter[3],letter[4],letter[5])
end

g_end_cool=max(g_end_cool-1)

elseif fin then
fin-=1
end -- if fin

-- line(g_stairs_line[1],g_stairs_line[2],g_stairs_line[3],g_stairs_line[4],8)
cam()
-- print(grad,0,108,8)

end -- dr_play


-- fades a quarter of the screen at a time
-- scan line by scan line, left pixel then right pixel byte by byte
-- which line, side of pair is dictated by p
function scr_fade(p)
-- local tables seem to be faster.
-- change start line based on oddness value
local d,m=0x6000+p%2*64,p&2==0 and g80 or g81

-- for half of the 128 lines on the screen
for j=0,0x1f80,128 do
-- for every 4bytes of this line
for a=d+j,j+d+60,4 do

-- map every pair of pixels to a mapped pair
-- 4 bytes at a time
-- shorter and quicker
poke(a,m[@a],m[@(a+1)],m[@(a+2)],m[@(a+3)])
end
end
end


-- fills house with decorations for kittens to destroy
-- also init function for game
function decorate_house()
-- dump g_sweets into dec locations for the moment
g_sweets={}
for d,dec in pairs(g_decs) do
local sweet=add(g_sweets,{x=dec.x,y=dec.y,vx=0,vy=0,spr=64+rint(48)})
if sweet.spr>95 then sweet.spr+=16 end
end

g_time=time()
end

-- new unpack function
function unpack_data(raw_data)
local raw_data=split(raw_data)
local num_elements,data,names,item=raw_data[1],{},{},{}
for p=2,num_elements+2 do
add(names,raw_data[p])
end
for p=num_elements+2,#raw_data do
item[names[(p-2)%num_elements+1]]=raw_data[p]
if p%num_elements==1 then
add(data,item)
item={}
end
end
return data
end


-- init

house_dbi="し%%ふしたとてつそせぬのはなねひちすにすっAふすほすふ「▥▥%そたそたそたし¥ã‚ã™ã¤Eてちせやゆにてちわゆし ンや▮としすりなせつし)のこリ)%そたそたそたしゅてちてむわすつわ◀てちせやゆにてちせふゆに セやふ▮とすねまねなせつ「たのこリ)%ヘたしちてそもちてちわすつわ•ã¦ã¡ã›ã‚„ゆにてちせやゆに みやコ▮とせねそりなせつ「たのこリ)Eみわちそチむわすつレたのわてちせやゆにてちせやゆに やレ▮とすなりゆせは「たのこリ\tやEそたわちもちてちてちわすつレたのわてちせやゆにてちせやゆに ‖▮とつせウせすは「たのこリホやeそたわちムむわすつレたはわてちせやゆにてちせやゆに!‖!しつんすはし「たのこリゃやコ「わそたわむアちてちサつレとはわ◀てちせやゆに「‖「ふはつれふ)のこリたやレ」わそたわ¥ãƒŸE•ã¦ã¡ã›ã‚„ゆに」‖」‖ヨたりたのこリ)レまわそたレたのレすつレすつをこ#ちつなす゛すつはの‖カみねたねたのこリ)コまみわそたレたのレすつレすつをれうちてむれのつへ゛すつはの‖ねみニみのこリ)ふまみハそたレたはレすつレすつをはもちのちのちのちのちのちのちのちのちのむはのつすなす◜へつはの◀た■たのこリ)まゃレそたレとはレすつレすつをはてちのちのちのちのちのちのちのちのちのちのちはのつへモすなへつれ•ã­ãŸ¹ãŸã®ã“リIしそたレそたuすつレすつわはてのちのちのちのちのちのちのちのちのちのむれつへテすなへつすれ‖りたヨたのこリ)わそたレそたuすつレすつわはむのちのちのちのちのちのちのちのちのちのちれつ&つすつれ‖カたニたのこリ)わそたレそたuヒわすつわはてのちのちのちのちのちのちのちのちのちのむれめすつすつすャれ‖ニたカたのこリ)わそたレそたuすタわすつわはむのちのちのちのちのちのちのちのちのちのちはャはつはつはつはめは%すつすつすつしたのこリゃしそたuそたわねな¹Vつわはちのちのちのちのちのちのちのちのちのちのむはˇ%すつすつすつしたのこリたわそたuそたわ!すめへめヒつわはむのちのちのちのちのちのちのちのちのちのちはすuす%ヒつし▤「わそたuそたわ¹ãªã­ã™ã¤ã‚µã¤ãƒ’つわはちのちのちのちのちのちのちのちのちのちのむはにすしてちつハてちつにすEめわ▥」わそたuそたわ!VつわはむのちのちのちのちのちのちのちのちのちのちはへつてちつヒてちつへEすつわ▤「わそた%まわそたわ!EすつわはちのちのちのちのちのちのちのちのちのちのむはへちてちャてちつへEすつわ▤「わそた⁵まみカそたりなりなヨEすつわはむのちのちのちのちのちのちのちのちのちのちはすつちてむはわはてちめ&わすつわえ。わそたハまみふ「!Eすつわはちのちのちのちのちのちのちのちのちのちのむはつはちてちるはしはのてむは+わすつˇuそたわまゃわ」ニなカEすつわはむのちのちのちのちのちのちのちのちのちのちはしはちもちヌちもちはし▤Hたスた⁵スたヲた「たHサスレへわすつEと▮はむもちるはむもちる▤Hたスた⁵スたヲた「たHすそにすスコへめわすつEと▮るもゅはのちもゅは▤Hたスた⁵スたヲた「たH◀ふへめハすつEと▮はむもちるゅもちるっ▥みヲン⁵ホヲた「たHすそよまにをょレすつEと▮るもゅはのちもゅはった▤たHた⁵Hた「たHすよをにすめしすつレすつ%へと▮はむもちるしのちものはしった▤たHた⁵Hた「たHサますそわすつレすつ⁵へめ!るもゅはふのちのはふった▤たHた⁵Hた「たHすよすスわすつレすつハへめふ「はむもちる‖った▤たHた⁵Hた「たHサスわすつレすつわへょわ」るもゅは‖ と█-ぬたぬたぬたぬたふツˇコツハたとハとたとのコたえとたのふのソのれ」 と█-たぬたぬたぬたぬふとそたのˇコとそたのハたとコみとるわたえイたのしのちもちるはヘゃ と█-ぬたぬたぬたぬたふとたとのˇコとたとのハたとわたとネのふ▥ホとしのちもむのはヘゃ と█-たぬたぬたぬたぬふるれˇコるれハたとわとれやのはのしたとけるたとしのちもちるはヘゃ と█-ぬたぬたぬたぬたわたとˇレたとハツふとはとキれしたとのはのはるはるはのはるはキたとしのちもむのはヘゃ と█-たぬたぬたぬたぬわたとIわ)とわみとそたのみしはキれふたとるはるはのはるはるはのはるたとしのちもちるはヘゃ と█-ぬたぬたぬたぬたわた]わた\rたとわイたとのやふはのれわたとけるたとしむもむるヘゃけ\"。たぬたぬたぬたぬわたとuたとレたとハるれEたとけるたとしのちもゅのヘゃわすつ∧ロつすわサつをわたとˇレたとレたとEみˇEをヘゃわすつロやvつすわサつをわたとˇレたとレたとEセˇレをょヘゃわすつヒとそたとfつすわサつをわたとっ」まセはっのたとレたとEンˇふへタれヘゃわすつヒのたとのつVつすわサつをわたと▤たはたまのたとレたとE」ˇょリヘゃわすつヒつるめVつすわサつをわたとた☉たはたそのはたとレたとE」‖みレ#ヘゃわすつロょfつすわサつをわたとた☉たはたそのはたとレたとわホハン‖セコ#ヘゃわすつ∧ロつすわサつをわたとたxみのたそのはたとレたとわメ⁵セヘたふンふ#ヘゃわすい•ã™ã‚•ã‚ãŸã¨ãŸxみのたそのはたとレたとレたと%みヘI#ヘゃ5にひにE⁸たコたとハたと▤たとたそのはたとわよそるトヌにへまuヘゃふラはヘゃレイつすにすつはとのはレ⁸たコたとハたと▤たのたそのはたとわにのそのトヌよすはみまUヘゃしのちてむのはヘゃハとまやょはやぬのはハったったコたとハたと☉みのたそのはたとわにのそトヌエすはまみま5ヘゃのちもゅのヘゃハとそぬとのとそとのとぬたとのハたまたそたそたわたやのコたとたxみのたそのはたとわるひエヌトれスみま‖ヘゃしのもちるはヘゃコとぬたぬるそたぬとのとらのはコまみったとたとたとたとたわたとセまホそホはみのはたとわるひよヌトのれヲみまレヘゃ□はヘゃわのとたぬとのぬたらやのとぬるはわ⁸た。わたとぬ▥はゃのたとわのにひにヌトるはす「みまコヘゃふヌれヘゃコのやるとらやるやのはコ⁸た◀わたときぬはオのたとわのにひヌトキはす8みまふヘみ⁵はしヘゃハひのはにのイのへるにハ」•ã‚ãŸã‘□たとわよてキトヌへXみ⁸Eヘゃコケにひ◝すつエˇコをハにヌトヌエ•ãµã‚­ã‚¨ã™ãƒŒã™ã¤ã—みXみヘみし¥Uそろにルエすエˇコょをふスノヌト%るそノスひにすつふみ▤そわ\nのEそろよろそケよすにひよˇわれタへキトヌトの%のそひエヌエすつコみ8‖マるEノよひそルにすにろよˇふリょるトヌトる‖るひエヌトれレみ「‖ゅるEろそろよケそノよすにひエひわたとたとたとわとたとし#のトヌトキ‖るひよヌトのれ‖みヲ‖ちるハとたとたとたとたしノよろそ⁴にすにひそろわ。ふイし#トヌトヌ‖のにひにヌトるはす5みス‖の⁵。わひエひそひそひそケにすにろわ∧#エヌトヌに‖のにひヌトキはすUみまˇ◀レ4エレい#よヌトヌよ‖よそキトヌへuみˇ•u▤▤まお‖つな⁶なサなつわたとわスサつすつへはへˇUまみxセxNねテすなをなへちつ^つのわたとわスすそにへつすのすはつすˇ5まみxみスみXお゛のつなねなりゆねウすつのわたとわ&るすつはすのˇ‖まみxみ「み8おなカなりなちつNすつのわたとわすまよそにす#「uまみxみXみ「ウすおテのつ>へつのわたとわすにをよすつのすのすつすは⁸ぬUまみxみ▤みヲ◀モす>ちつす◜すゆすめのふとたとのふそすまロめへつはぬそぬそぬそぬそ5まみxみ▤スみスすつすつすつすつ>すモのつすゆす◜すつはのとスみのスすよへはのへのすつ 8みxみ▤「みま•ãŠã¡ã¤ã™.すつはのとそホのスサsしれ9XみˇぬとにたとてちなEたとレたとEのちつすモをつはのちˇたのこリたXみˇふぬとにたとてちな⁵なわたとレたとEちのつすᵉすつはるˇたのこリた8みˇコぬとにたとてちなわたとふなわたとレたとEのちつすなすゆねゆすつはのちˇたのこリた「みˇレぬとにたとてちなわたとてちなわたとレたとEちのつすᵉすつはるやUやたのこリたヲみˇ‖ぬとにたとてちなぬとしたとてちなわたとレたとEのちつす◜すめはのちそたしてちのハてちのそみのこリたスみˇ5ぬとにたとてちなせつしたとてちなわ)とEちのつすテすなすめはるやのてちのホてちのやたのこリたまみUサつレつVぬとにたとてちなわ=Eのちつすᵉすつはのちるちてちのメてちキ▥9uミレkぬとにたとてちなˇ‖ちのつすᵉすつはるれちてむはわはてちのれ▥」ふのとのはふ「ˇˇˇわきぬのレくねのˇハとたとのふ「ˇˇˇわけるレけるˇコやたとるし「ˇˇˇわきぬのレくねのˇわイたとキ」ˇˇˇわ▮キ▮のレ■キ■のˇわイたとキ「ˇˇˇわ の のレ!の!のˇわイたとキそンそˇˇˇわきぬのレくねのˇわ(たスたそˇˇˇ∧∧∧ˇ」そンそˇˇˇいいいˇˇˇˇˇˇヒめれˇˇˇˇˇˇˇ⁵のへつすょはˇˇˇˇˇˇˇ⁵ロつれˇˇˇˇˇˇˇ⁵ロめれ%タれˇˇˇˇˇˇ⁵つへつすつへ[へネˇˇˇˇˇˇレサの∧すつクˇˇˇˇˇˇレロつヒのへつをのつすれつはˇˇˇˇˇˇレへのヒのfつクˇˇˇˇeつサね⁶ミはレ6つすつすつのつすつすめネˇˇˇˇeつfタはレすつへのサのすᵇすめすネˇˇˇˇeつ6めのタれハ◀ょのはつれつはつはめはつれˇˇˇˇeつをのをねすのすャれハすのヒめCしリˇˇˇˇuめFつすつすクハヒめのはつネˇˇˇˇˇ%つロねサタすはつはハヒつすリˇˇˇˇˇEつ&つへタすはレすつへめれのれˇˇˇˇˇUつをね⁶つすミレサょクˇˇuゅれるゅるソˇ✽つすのサのをつすミはレへつすめれつはˇˇuちてキはてちのむるゅˇ✽めFミはレサょクˇˇuてのむはゅのむのはのむˇ✽つヒねヒつすタはレをつすタはˇˇuむのはのてちてちのむのはのちˇ✽つ&つへミはレすつへタれˇˇuてむはマのゅのはのわむˇ5つすつ6のつすめはレサのつすめはˇˇuむるてマのゅれふちてのはˇ%つヒのロミはレサミはˇˇuもちるもちのむのゅのしちてちるはˇ‖つ6つへタはレへつへタはˇˇuるリてはるクのはしてゅのはˇ⁵めをねサねをょのつれハねをょのつれˇˇuもゅるてむのはるちヌむのはˇレつへの6ミれˇˇˇuるてむるちてちるはてむのはるむのはˇハつヒの◀タれˇˇˇeちてゅのはちてゅはゅのちのはのむのはˇハつすつ◀つへのタれˇˇˇeゅのちるてむのはのてちてちるれのむのはˇわめ⁶ねヒミれˇˇˇUちてゅるてちてむは\nのはのゅのはˇふつすつサねヒつすミれˇˇˇUむのちるてちてむるてゅのむのれのゅれˇし∧つすょれˇˇˇ5てゅるアちもちるもむのむのれのゅのは✽つすね&のをャれˇˇˇ‖てソのれキてるリてのれキ3eヒつFミれˇˇˇ%もちのちのちてソるてちキむるソるはˇつをの◀つをミのれˇˇˇ%キむるゅるはてむのはるむるソるは✽つへねめすのね6タクˇˇˇ⁵ちてゅのはてちてむのゅのちのはのむのはのソる✽サめVのへょれˇˇˇ⁵ゅのちるゅキてちてむのれのむのはのソのはeサつのつヒねサのは⁶れˇˇˇハちてゅるてちてむは\nるはゅのはるゅのはEつのつねミへょヒつれしめへつれˇˇˇコむのちるむてむるてソのちるはのゅクるむのれ‖つのすょネょはタへつすつクへつすつはˇˇˇふてゅるてちてむてちるもマるはのゅのクるむる⁵をねsャネすょはˇˇˇてソのれるゅるれるてちこリレサネふSめすふクめすˇˇˇ"
once_dbi="こ#⁙ちことしてすせさしろ⌂よ⁘lん5ミ⬇️5ムよわムを#ムろ5フん5ムを#ムん‖ムよ⁘gょY|を5~⁷⁙て▮Uケ⁸は|ろ9ル▮UホっYミoEタ●+ルに4,⁴Tl⁴4;そひてぬ#ルぬ5ろエゃミhひjよハ+⁷-ルよハ+⁸1さフハヘょ+lつEをんレ+●1ろフコヘょ1ろフわjせEろわ7ヘんわ+G-ルよ⁘gょ)ミHひgょ%ャ░ほフんレ+G'ルほ4メを1タ(はlほEタ(はなよコ+'-ルほ3-わ5アフ⁘jはEもわ7マはEャ✽ほフんレ+G'ルほ4メを1タ(はlほEタ(みタHひてほEアろほヘんレ+●1ろフコヘょ1ろフ⁘-わふアフレハょ-ミHひjよハ+⁷#ヒ⁷3ko5もよし+'#ヒょ+モて5ろエレ+●1ろフコムひ5ノん」ムせ4cょ)ムんコ+⁷Tもろふiょ4jよセミᶠ9ウん5jさ5ム⁶#lん5ュ⁴7ムんT<ん9テん1タ✽セを⁶Y~よゃ~⁶'~▮YすはE~ᶠ⁙ゆ▮1なᶠ⁘jよ⁘jよ⁘jはEタ●1タ●1タ●1タ●+ルよ⁘jよ⁘eん4gん5てん5ミ⬇️5ミ⁷⁘-を'ムん5ムん1タ●%mを)◜オ3もヘYk♥Ykフ1アフ⁙<ヘYル▮5タ●1てフ⁙kんふてフセケフゃひフ⁘とを)ルつEひを1タ⬇️7マに#ュこ%ルよコ+hひeれほマにEひフわマよ⁘dょ1ゆエたャっひfょ/ルこ1さっひfょ'ルつ4jよ⁙メを+kそ'ルよコ+G5ャ⬇️コモよコ+ヘは,よ⁘jせEタ(9ハょ1ろん3モんTe▮1ろフわ+フ1タ●%ルよホすさ'ルよン|れクとを1タ⁸は-ろふタ●1てフ⁘gょ1タせ'ルよ⁘fょ'ルつ4jよ⁙メを1ろフ⁘cんわ+●1もフわ+フ1タ●%ルよ⁘とを)ヒんわ+●1もフしさフしされひjよ‖+●+ルよコハんわ+●1もフ3とん#ムを1タ☉ひjひ5タᶠ5モつEタ●)lエたム³ã‚³ãƒ£â—1チフ⁘hᶠ⁘ゆ▮)ルよ⁘g▮'~てTjよ⁙とを1タ●-ルよ⁘jよ⁘jよコ+●1タ✽7ハを1タ●1もん5ムん1ルん5ホん5タん3,を#lせ4jは1タ✽ふ5アほ4をほ◜▮5ろ▮YマこEへん⁙ュんTjは#マよ⁙ムフ5ムんeヘょ+ミ/ハマょ)ミ░セ{●-ネ⁶1タせE▮Y~ム'+ルは4eん‖+⁷1タ♥3つせ'ムつ#ミ●'メゃ2ム')ルは4eん‖+⁷1タhUすオ3な⁷しすオ4jこ7ユよやヤエわ+'/ムん1ルに4jめ%ノれ3つんしてせ1タそふjに5ャヘひてほ3,よEもを1ケっ3つんたモんふてよ⁙てっ1も'S-わふろフわマょ)ミ●/~さ#d³Yす³Sャ●#メエ⁙o('ルは4とろふチフコホん3lん⁙lん5ムん5マよGャ✽めャヘひてはEひを7ヒん\tルぬ7マに7レヘほ4フ1ケフThっ'ルは3つヘは,こ-ルに4mわほマね5ムん5ムク4hん9タLリ-わふミヘは,んレ+フ4mわほマふ7ユムん4ク4gっみタ░Uフ⁷S<ん5モま5ムん1ルはEタG5c{てこgeマにGャ●+{P+~▮Yテ▮Yミ☉ひとを-メキ³ã‚“す³ã¦'1ひフTjよ⁘jよふマょ+ルよレ4•ã¦ã“{てて(1てんTjよ⁘jよふマょ+ルよレ,³ãƒŸãŸcミさんTdっみタ░ふムよハム⁵ふタせ1ルはEタG5♥す³ã‚“すめモよふ4⁶1ひフ5タフeャ⁷Uチを7フょ1アょ[たcミたiハjせEャ●'ムク4cんkャヘはめ♥1ルはEタHわc{てこ{し⁴⁶%ムエ⁘ム³5,'/メゃみひフわアを7フょ1アフ`#3ぬ#'5jせ7ャ✽めャっわヤんハルん9ひフわヒん1ルはEタG5⧗ふさSは;モよふ4⁶)⁷エふ,Geハっふ⁴エわ+ヤ5ムエ‖+(ひhっみせ#ᶠせゃへマつEャ░5モせ5\0ゃふさょ=⁷エわ+▮Y~よし+(ひhアふムん5ムシ⁙lっ1も'Sムヘ$ム'7ユこ;ャヘひjにEろフ⁘メっほ4ヘんᶠよコルエ⁙ム3Sメヘ&ム(へノん9さこEろん4dょ+ルん5マん5ムん5ムを-メっ1ヤゃ'ルエイメゃ'⁴エたムんわ◜⁷1さフハ.▮Uホっや5ムんヤん⁘ムフTム'slっ*ムシサろ³Uモれふハ⁷1ルはEろをほ.cミたcハ⁴を-メん5ムク=ろょTアEへなつEすこEもを7フょ+ミhふc3ぬ#3しム⁶-ヨ{●Yゃ-レエョアんS-ろほヒん‖+(ひてめ5◝#ᶠせ#⁶て⁶/ムん5ムFほモよヒて⁴ほハょ)ミ☉ひとろ5iっむこ{てこ{kャ●G5ムんチんTgコみひフわ+⁷1ルはEひエ‖4³á¶ ã›#ᶠさクTjよ⁙ムヘ1ろクS-ろほヒん‖+(はム⁶#ルキ³ã‚“す³ã‚ã‚“Tdん5ム⁴5ムんSメヘ1ろんS-ろほヒん‖+(はてん⁙てっ⬇️んす³ã‚ã‚¯9タんGルエコヤん9てフTgク9ひフわ+⁷1ルはEモん1メエてこ{つヤゃ1まん5ᵇ7<てDふモよヒろ⁴ほハょ)ミ☉は+そふc⁷3つ(わ♥す³ã‚ã‚¯=タ7Gャw;ャ⁷Ejは;ャせし+ヘは,を7ノんし.て5ミ⁷5ムん5ヤゃ1ケフTlっ)レエ⁘てっ#ムエゃムん5テん5c⁷+l⁴へ1{●Yク=タ♥5jク9もフTgク9すっ+~▮5タぬYeᶠンfん5ムん5ムF1さょTo()ムエ⁘に(%{░5タ●/5ムん5ムんタ⬇️7.よ;ャ⁷Ejは;ャ●5タ●1タ●1さフTlっ)レエ⁘てっ1チをふマよ⁙ムん5ムん⁘cん9チ'Smっ1ろ'Tjん⁶ろよ⁘lムほ-ミ4jっみチ3Slっ1ろ3Tjん\t░よ⁘,フ5ムんeマめGャ♥efっふjつ6なよしアを#ャ●-メゃ~▮W;ミ✽7モよ5ャ⁸わャ░ふム⁶#ミ⁷4nよ⁘ムフt😐ク4gん5ムん5⁴エコムエ⁙,んTc⁷5ム⁶Tjほ7ユよやヤエ⁙lフE5ょ<なぬん8よん5ワ⁙◜▮Yタ●1ルエ⁙lっ1もミ5ムん5⁴⁶1タ●1タ●)ムエ⁙oっ1もフ]5ムわム⁴5ムん5ムん1ムん5ムん5ムエわムん5ムん4lヘ1も3Tfん9チ'SlヘんルムほミgE5ょG-アわeっほ4ヘん⁴をほ.よヒなよコル⁶<なね5ムん5ムク4ᶜん5ムん5ムっ(ムん5ムん;ミHふjほ9タ⁸わjゃみっょ=5ムわ⁴わu,Lん5ムふャ▶E\rムん,'+ムエ⁘レF)ルエ‖ム⁵5モされんさへてほ5ャ░ふgん9cミたcゃふもょTjよハム⁶;モほ7ョネミしネへては7ャ░っフっみしネセしフeハアみタ●+メエ◀ろ⁵7.ネミしネミヤエコ4⁶1ひミW#ᶠせ#⁷edょ9タ●+レエ‖⁴⁵7モしネふしネてっ)ルエ⁘eょ9#ᶠせ#ᶠ5ャん5jよ⁘とっ1ムエレム‖こ3ふこ/っ)ムエ⁘eん9す³ã‚“す³ã‚‚なせ7ャ●1ろんToっ-メカこ3ふこ43Slヘ1タフE~されゆさふモせGャ●1ろょTo(-レエせ#3せ#ゃみもミTjつG◜されゆさわ⁴³7モよ⁘fん5ホんeヘょ93ふこ3は5ャ⁸ふjよわ,³ã‚“す³ã‚“ヤっ%ムエ⁵ムん5jせ?」っ)ムん5fん9#3せ#3eャ⁷5jよわムᶜこ{てへてDふ.めE4クTdん5ム⁴5ムんSlヘ$れてされたハfっみタ░ふ.ネセしフeᵇ⁸わiん5⁷エ⁙レムんKᶜん5ワコ4‖こ3ふこ,()レん5ムエ⁙-フ5ムんeᵇ(ふiんw⁴エ⁘jよコ,⁙せ#3せ,っ)ルょG-エ⁙-っほ4ホへヘん9コミふム⁶'ムれ5ミん4dん9しネミしネめモに5ムん5ャ░ふムん5ムシ⁵ルエ⁙ムクTdエEなょ%lよふルᶜされてさに()メスん4エ⁙,ホG5ムルcっふjんkャ⬇️Sとろほハん⁙メヘCんされんゃみもミTふF'レエ⁘eっふhゃめモよコ+ヘは,よふ,³ãƒŸã—ネミさんSmっ1タヘふjよコルん5ム'5ャ░7ハょ'ミ⬇️5モ3せ#3せ⁴エコム⁶1ひんTjは@TjᶠムクTfょ'ルつ4dっむこミてこミkャ⁷Ejよわル⁶1アん5ムゃ5モよコ+ヘは,よふ4³ã‚“せ#んさクSmヘ1タヘわjよ⁷5ムん#ムイタ⁸は-ろふタっふuしネふしふモにEャ●'ルエ⁘jよ⁘eょ'ルつ4dん9#3せ#'eャ⁷5jよわム⁶1さん1タ✽7ハょ'ミ⬇️5.しネふしへてEふ.よ⁙,ヘ1タそUタ✽ひmろほハんしチミ\こミつヤゃ-レエ⁘eアみタ⬇️はミ●)ムめEひフふムをふムん5ムク=アん5タ⬇️5ムを1さん#ムせ'ミんふネ⁸%ミヤ5モん5ュ⁵61{●Yク=アタ◆モよ⁵⁘{9タhYさᶠし+っ#ルこUさつS~▮#~▮#iん5ムん5ムFふムんTiん5ム⁶/ヒろふcんふャせしされふタ●%5ムん5ムんツムん8よ⁷5ムイタoセへんたd⁷#c³ã‚»ã™â¶1タ●1タ●1タ●){P1め"
-- init
menuitem(1, "music on/off", function() if mus then music(-1,1000) else music(upd_func==up_play and 22 or 0)end mus=not mus end)
menuitem(2, "storm on/off", function() g_storm= not g_storm end)

g_stairs_line=split'670,231,857,138'

-- pull native sprite sheet into lua table so can be restored quickly
game={data={}}
for m=0,8192,4 do
game.data[m\4+1]=$m
end
-- set up palettes for lighting fx
scpal_map={}
local vals=split'0,129,130,131,132,133,13,6,136,137,9,139,140,1,13,143,0,0,0,1,130,128,5,10,2,4,11,3,1,2,4,142'
for i=0,15 do
scpal_map[i]=vals[i+1]
scpal_map[i+128]=vals[i+17]
end

drpal_map={[0]=0,unpack_split'0,1,1,2,1,13,6,2,4,9,3,13,5,4,14'}

g80,g81={},{}
for i=0,255 do
g80[i]=(i&0xf0)+drpal_map[i&0xf]
g81[i]=drpal_map[i\16&0xf]*16+i%16
end

fade_pal(8)
wait(20)
if sub(stat(6),1,1)!='0' then
music(0)
mus=true
else
mus=false
end

palt(0,false)

--load title data
title,house=unpack_dbi(once_dbi),unpack_dbi(house_dbi)
once_dbi,house_dbi=nil

-- unpack title sequence data (see title_gen.p8)
local title_raws={
split"163,4,6,3,4,38,28,16,2,2,69,38,16,2,2,90,48,0,2,4,7,62,16,2,2,92,65,42,3,4,89,88,36,1,4,9,95,52,2,2,7,110,52,2,2,156,35,75,3,4,69,59,85,2,2,45,69,85,3,2,7,87,85,2,2,40,97,85,1,2,220,85,107,4,1", -- drake blue games presents
split"166,13,11,3,4,38,32,19,2,2,69,42,19,2,2,3,52,19,2,2,90,64,3,2,4,15,77,19,1,2,37,76,10,1,1,7,83,19,2,2,75,52,49,2,1,169,33,71,3,4,35,56,65,2,4,15,69,81,1,2,37,68,72,1,1,89,75,65,1,4,89,83,65,1,4,71,92,81,2,3,77,70,105,4,1", -- frankie and philly in
split"128,4,16,3,4,69,26,24,2,2,89,36,8,1,4,89,44,8,1,4,13,53,24,2,2,192,65,24,3,2,7,84,24,2,2,7,94,24,2,2,3,104,24,2,2,153,93,19,1,1,128,29,57,3,4,13,51,66,2,2,38,63,66,2,2,38,73,66,2,2,13,83,66,2,2,39,97,66,2,2,38,95,66,2,2,0",-- halloween horror
--split"0,9,2,3,4,3,32,12,2,2,5,45,12,2,2,7,55,12,2,2,9,40,34,2,2,11,53,34,2,3,13,67,34,2,2,3,78,34,2,2,69,100,34,2,2,64,4,61,3,4,35,27,55,2,4,38,40,71,2,2,15,50,71,1,2,37,49,62,1,1,40,56,71,1,2,41,64,66,1,3,45,72,71,3,2,69,90,71,2,2,40,101,71,1,2,41,40,95,1,3,37,47,91,1,1,15,48,100,1,2,45,53,100,3,2,7,71,100,2,2,37,81,107,1,1,37,89,107,1,1,37,97,107,1,1",
split"128,5,35,3,4,69,28,43,2,2,11,39,43,2,3,11,53,43,2,3,71,68,43,2,3,128,9,81,3,4,69,32,91,2,2,89,42,75,1,4,89,50,75,1,4,13,59,91,2,2,192,71,91,2,2,7,88,91,2,2,7,98,91,2,2,153,97,86,1,1,3,108,91,2,2",-- happy halloween
}
ls={}
for t,title in pairs(title_raws) do
local ptr=1
ls[t]={}
repeat
local letter={}
for i=1,5 do
add(letter,title[ptr])
ptr+=1
end
add(ls[t],letter)
until ptr>=#title
end

parts,snow,bons={},{},{}

for i=0,400 do
add(snow,{x=rnd(128),y=rnd(126)})
end

pulse=0
kits={
{x=350,y=230,vx=0,vy=0,gr=true,face=true,hit=0,flinch=0,sit=30,score=0,name="frankie",bonus=1,bon_cool=0},
{x=370,y=230,vx=0,vy=0,gr=true,face=false,hit=0,flinch=0,sit=30,score=0,name="philly",bonus=1,bon_cool=0}
}

snowback,snowfront={},{}
for i=1,100 do
add(snowback,{rint(128),rint(120)})
add(snowfront,{rint(112),rint(120)})
end

g_cam,g_ss=unpack_data'2,x,y,420,60'[1],unpack_data'4,x,y,vx,vy,0,0,0,0'[1] -- between two kittens, remember to update when kitten start changes

--#include "halloween_platforms.lua"
g_floors=unpack_data('4,x,y,r,b,-4,-10,1028,0,252,122,760,130,0,253,1024,270,840,122,1028,139,824,133,852,146,664,213,682,226,680,205,698,218,696,197,714,210,712,189,730,202,728,181,746,194,744,173,776,185,760,165,792,177,776,157,808,170,792,149,824,161,808,141,840,154,647,221,667,233,640,229,647,238')
g_walls=unpack_data('4,x,y,r,b,250,-2,256,128,-4,-10,0,270,1024,-10,1028,270,200,127,204,156,112,127,116,155,584,129,588,155,880,-2,884,27,624,-2,628,28')
g_paints=unpack_data('5,x,y,r,b,c,648,0,879,7,7,278,16,647,111,8,904,0,999,7,7,256,5,277,122,8,689,16,895,111,13,624,5,642,28,13,643,13,688,31,13,945,16,1000,111,15,896,16,944,31,15,-7,240,127,255,3,200,133,223,159,2,608,144,1001,239,12,265,144,583,239,2,449,209,486,239,1,401,81,430,112,1,880,5,901,27,15,1001,5,1023,36,15,1001,86,1023,121,15,1022,122,1023,123,15,208,157,211,158,4,224,144,264,159,2,584,133,607,158,12,592,157,597,159,2,224,128,583,135,7,608,129,999,135,7,1002,133,1023,154,12,1002,214,1023,250,12,112,133,135,156,4,132,157,142,158,4,136,128,199,135,7,397,40,434,61,9,752,137,823,137,6,752,143,807,143,6,751,128,839,128,13,280,0,623,7,7,752,136,823,142,7,446,168,489,187,9,446,182,489,187,12,446,171,456,187,14,455,169,461,187,4,451,173,457,187,15,447,170,451,187,7,461,172,468,186,6,471,170,476,178,15,478,174,482,183,7,473,179,480,185,6,485,168,489,172,10,467,173,472,186,7,456,178,463,187,5,485,183,487,184,7,476,184,481,185,7,397,48,434,61,5,397,50,406,53,13,397,53,400,61,14,401,56,404,61,14,405,58,408,61,14,409,60,412,61,14,397,48,434,49,3,403,48,410,48,9,417,48,426,48,9,400,51,400,55,2,398,51,398,55,5,408,55,411,61,2,409,52,412,56,7,409,56,409,56,4,412,56,412,56,4,413,53,413,57,15,408,53,408,57,15,419,54,433,60,13,410,54,411,55,15,416,42,401,42,8,413,46,426,46,8,427,44,434,44,8,418,56,418,59,13,424,53,433,53,13,401,55,402,55,14,405,57,406,57,14,277,190,322,220,1,752,137,824,137,6')
g_plats=unpack_data('4,x,y,r,b,608,237,647,240,281,97,350,100,291,77,348,79,342,202,409,205,262,24,276,29,344,232,407,236,336,224,344,228,386,113,445,117,384,70,447,75,403,101,428,105,358,73,369,76,355,96,380,99,264,82,271,85,464,86,527,90,464,74,527,78,456,27,535,30,547,40,612,44,840,86,879,90,969,105,990,108,971,117,988,120,952,94,999,98,960,64,991,67,960,82,991,86,1008,82,1015,87,928,74,935,77,624,229,647,233,407,224,415,228,416,222,431,226,432,198,503,203,344,207,407,212,520,210,551,215,512,225,521,229,552,224,559,228,520,232,551,236,443,165,492,170,536,57,543,60,536,73,543,76,536,89,543,92,395,38,436,42,360,54,383,57,296,54,319,58,328,62,351,66,824,70,857,73,800,54,831,57,776,38,807,41,776,70,807,73,824,38,855,41,952,38,991,41,952,54,991,57,976,25,991,28,1004,34,1017,37,451,229,484,232,434,241,500,244,512,167,551,170,512,183,551,186,568,202,575,206,336,155,416,158,761,122,839,125,328,39,351,42,275,190,324,195,272,231,327,235,40,207,63,211,688,223,847,227,816,207,847,210,736,207,791,210,784,183,839,186,854,178,866,182,872,199,876,203,875,231,895,235,920,239,943,243,880,223,983,226,896,202,967,205,888,157,975,160,968,231,988,235,1008,210,1015,214,1003,153,1019,157,987,199,991,202,65,227,101,231,82,218,102,222,72,210,92,214,83,198,95,202,78,188,86,192,29,180,48,186,984,170,991,175')
g_decs=unpack_data('2,x,y,331,54,311,46,363,46,264,74,341,30,372,88,295,89,386,62,466,78,503,78,536,81,559,32,830,62,838,30,786,62,961,46,952,86,862,78,747,165,774,214,783,198,770,198,957,194,1007,202,734,214,538,158,533,174,491,190,418,214,350,194,317,222,518,174,696,189,806,214,827,30')
g_candles=unpack_data('2,x,y,294,39,430,55,941,208,908,208,594,25,780,17,952,23,810,167,980,154,515,151,433,183,273,215,30,165')
g_bats=unpack_data('4,x,y,vx,vy,600,20,0,0,309,20,0,0,493,20,0,0,293,20,0,0,417,74,1,0')
g_spids=unpack_data('3,x,y,vy,550,160,1,540,40,1,200,70,1,300,90,1,800,25,1,887,214,-1,962,156,1,924,174,1,136,154,1,157,176,1,182,200,1,663,38,1,727,95,1')
g_ghosts=unpack_data('3,x,y,vx,600,200,1,300,67,1,500,175,-1,900,23,1,965,234,1,592,104,1')

g_pumpkins=unpack_data'10,x,y,vx,vy,hx,hy,sp1,sp2,w,c,40,191,0,0,40,191,196,199,24,0,982,24,0,0,982,24,206,238,16,0,966,24,0,0,966,24,206,238,16,0'
g_num_candles=#g_candles

cool,sc,g_num_broken=512,1,0
g_title_cols=split'11,9,0'
use_sprites(title)
cls()

lightning,g_dark_cols,g_lightning_cols=0,split'0,129,130,131,132,1,13,6,136,137,10,139,140,5,4,134',split'5,12,14,10,10,7,7,7,7,7,7,7,7,7,7,7'

set_fade(0.5,{set_dr_func},{dr_title},{set_upd_func},{blank})

g_letter,g_end_cool=1,90 -- 30fps at end so 90=3 seconds

P#99459 2021-11-01 17:23 ( Edited 2021-11-01 18:15)

Cart #halloween_horrors-1 | 2021-10-31 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
13

Banish the evil monsters from the house by putting out the magic candles.

Break ornaments, cups and other items about the house as you go for extra points.

Press left to play as Frankie or right to play as Philly: put out all the candles as fast as possible or with maximum destruction.

For a two player game with both kittens press X: beat the other kitten or work together to get the maximum score in the fastest time.

Controls

Player 1

Swipe/Hit: X, V, M (X on gamepad)

Jump: Up Arrow, Z, C, N (A on gamepad)

Left - Right: Left Arrow - Right Arrow

Drop down: Down Arrow

Player 2

Swipe/Hit: Q (X on gamepad)

Jump: E, Tab, W (A on gamepad)

Left - Right: A - D

Drop down: S

(These are the usual PICO-8 controls)

Menu: P, Enter, Esc

From the menu you can toggle the music and the storm - this stops the lightning flashes.

Go get 'em kitties!

If you like this game, find a problem or have any suggestions then please comment below.

Please consider trying my other games or following me for news on new projects. Particularly, you might enjoy Demystifying the Christmas Tree, an earlier game where Frankie and Philly make a mess at a different time of the year.

And watch out for the kittens returning in the future...

Happy Halloween!

Game programming, art: Drake Blue Games

Music: Modest Mussorgsky and Tim Follin arr. Drake Blue Games

1.0.1 Update

  • Reduced time kittens are unresponsive after being hit, but kept invulnerability time. Should be less likely to get "caught" by enemies. Also pumpkins and skulls more affected by kitten hits to help with this too.
  • Displays both scores at the end of a two kitten game for polite comparison/commiseration with the runner-up.
P#99342 2021-10-29 22:22 ( Edited 2021-10-31 16:28)

These are my own notes on saving token tricks that I've used previously. I'm sure what's here is not unique to me - there are other documents about this subject (e.g. https://github.com/seleb/PICO-8-Token-Optimizations) and I recommend looking at those as well, probably before this one. I intend to add and update here as I learn more.
If you see a mistake or are a true wizard that can offer wisdom to this humble novice then please feel free to comment :)

Starting out

I say "starting out", because the next few ideas concern initialisation and structure, but this might be a good place to suggest that a lot of these tips produce much nastier to read code that's more of a pain with which to work and employing all this stuff straight away, especially if you're new to coding is just going to make the whole experience more difficult and probably not much fun. I tend to bear some of the things here in mind as I code and some things are becoming (bad) habits, but usually I've only used these techniques as a second, third or an in desperation when PICO-8 won't run any more pass.
Coding your game in a more elegant way or even... cutting things(!) that aren't necessary are probably both better ways to save tokens.

Functions or "to _init() or not to _init()"

3 tokens; 19+ characters

I don't use this function at all and do all initialisation at the end of my code, that is, after any other function definitions (so that they're not undefined if calling them from the initialisation code). If you're using the PICO-8 editor I'd suggest putting it in your rightmost tab.
I'm going to end up using global scope anyway and I have yet to find a downside.

function _init()
 my_var=1
end

vs

my_var=1

Initialising variables

1 token per variable

Every time an '=' is used a token is wasted:

a,b,c=1,2,3

(7 tokens)

vs

a=1
b=2
c=3

(9 tokens - 1 for each '=')
To make this (a bit) nicer to deal with I tend to do something like the following when this kind of list gets very long (often longer than shown here):

a,b,c,d=
1, -- a
2, -- b
3, -- c
4 -- d

This uses the same number of tokens and I've had to resort to code minification that will remove the comments, newlines etc. for the last few projects anyway. I use this one, which is great BTW: https://pahammond.itch.io/gem-minify.

Aside - assigning nil

Need to clear a variable to nil (which conveniently evaluates as false - todo: more on booleans)?

a,n=1

4 tokens

vs

a=1
n=nil

6 tokens

Pretty desperate and calling a non-returning function after your variable assignment?

n=nil
nil_returning_function("some text",0,0,10)

9 tokens
vs

n=nil_returning_function("some text",0,0,10)

8 tokens
WARNING: You need to be very sure that the function doesn't return anything. I got caught out by this myself writing this post as I initially used print as an example here and as GPI points out below it does return a value. You need to be very desperate to do this as it really, really doesn't help with code readability and is asking for trouble if you, say, change the function to return a value at a later data. But if you're right up against the token limit...

Back on initialisation - split() is your friend

Got a lot of strings you want in a table?

tab={'baa','baa','black','sheep'}

7 tokens
vs

tab=split('baa,baa,black,sheep')

5 tokens
vs

tab=split'baa,baa,black,sheep'

4 tokens

Don't use brackets for function calls or maths unless you have to, since they add a token for each pair that you use.
Note: the following is perfectly legal in lua:

rnd{1,2,4,8}

(This returns a random value from the table {1,2,4,8})

unpack is also your friend (and itself friends with split)

a,b,c,d=
1, -- a
2, -- b
3, -- c
4 -- d

11 tokens
vs

a,b,c,d=unpack(split'1,2,3,4')

10 tokens

Further variables cost 1 token each i.e. each further variable you add to a statement like this will save a further token compared to the version above (and 2 compared to separate a=1 b=2 etc.)
Be warned, you may have trouble with your variables being initialed as strings, but pretty much every PICO-8 function I've tried (e.g. spr, print, rectfill, pal etc) Just Works. The problems I've had with this have been my own code. It's also very hard to know which number corresponds to which variable so I suggest only using this trick when you really need to.

Of course, you can go further:

a,b,c,d=unpack_split'1,2,3,4'

8 tokens*

The caveat here is, obviously, that you need a function like:

function unpack_split(...)
 return unpack(split(...))
end

10 tokens by itself.

However, once you have that function...

print("hello world",10,20,11)

6 tokens

becomes

print(unpack_split"hello world,10,20,11")

4 tokens

It's remarkably fast - I found that I only hit performance trouble if I used it within nested loops or with very high numbers of objects.

In fact, every time you have 3 or more literal values together you can save at least a token with your unpack_split function anywhere in your program (it's a flat cost for each use in fact). I used it enough that I ran into trouble with the number of characters it used and the compressed size limit - renaming the function to US or similar solves that fairly nicely, once again at the cost of making the code ever less readable.

Order is everything

Armed with these techniques the next thing to consider is when to assign values in you initialisation (or anywhere else).
The more you can bunch together, the better since e.g. fewer '='s are needed that way and you can group more literals into a single unpack_split'1,2,3,4'. Remember you can't reference an assigned value from within the same assignment though.
This doesn't work (unless you really want y to be what x was previously + 3):

x,y=20,x+3

todo: add multi-dimensional table routines

Example from PICO Space

Stare into the void if you're feeling brave enough...

g_part_expl,g_vel,g_rand_cloud,S,g_sun_pal,
 g_candy,
 Sc2,
 g_syls,
 g_scpal_map,
 C,
 g_news,
 M,
 D,
 U,
 g_ratings,
 g_npc_chat,
 g_diff,
 g_diffs,g80,g81,
 ss1,ss2,F,-- start of literals
 g_near,g_far,g_edge,g_msp,g_gal_size,
 g_gal_time,
 g_fin,
 g_sys_p,
 Q,--g_progress?
 g_award,
 p,
 g_music,
 g_gal_seed,
 g_sys,
 O,
 g_cmdr,
 g_kills,g_max_energy =
  {vel,vel,rand_cloud},{vel},{rand_cloud},--g_part_expl
 {c=8,sp=.3,en=8,dam=1},-- ship
 { --g_sun_pal
 split"10,9,8,4,2",-- yellow
 split"7,10,9,8,2",-- white/yellow
 split"7,12,13,15,1",-- white/blue
 split"7,6,13,5,1",-- white/grey
 split"12,13,15,5,1",-- blue
 split"7,11,3,5,1",-- white/green
 split"10,11,3,5,1", --yellow/ green
 },
 split"10,12,11,8",--g_candy
 split"0,1,1,2,1,5,12,2,4,8,3,15,2,2,1",-- Sc2
 split"ca,bal,da,gar,non\-en,pol,der,arp,bun,duc,kit,poo,v\-evee,zir,buf,v\-evil,xan,frak,ing,out,re,far,do,tel,tri,cry,quack,er,dog,pup,sno,ger,bil,pa,n\-ena,jan\-en,es,on",--g_syls
 {[0]=0,unpack_split'0,1,1,2,1,13,6,2,4,9,3,15,5,4,1'},--g_scpal_map
 {x=0,y=0},--C
 { -- g_news
 gal={
 split_comma'president #2 welcomes gerbil delegates to the #1 galaxy on state visit.|government says there are no broccoli or carrot shortages.|vice-president: reports of space weevil incursions are fake news and no cause for alarm.|recipe book by great aunt dahlia using substitutes for carrots and broccoli returns to bestseller chart, four hundred years after first edition|cats complain that kangaroo boxing title challenger hit their reigning champion, "she is supposed to sit in the boxes not punch other animals" ',
 split_comma'president dismisses rumours of a weevil invasion.|"no carrot shortage," says vice-president, "just eat a potatoe instead"|duck wins round-galaxy race by a bill from bunny. bunny claims galaxy is not round.|president #2 denies spending entire security budget on jacuzzi: "no-one will attack us anyway and i like the bubbles".|dog moral philosophy professors to discuss exactly who, if any of them, is a positive young male role model and also the location of "it"|bears win hugging title for twentieth year running',
 split_comma'"things are not getting worse," says president despite weevil presence noticeably increasing.|carrots vanishingly scarce as prices rise sharply. rabbits lobby parliament|"has anyone actually met a weevil?" asks vice president|government officials deny that gerbil delegates left because of weevils eating their carrots|"#1 shall have a universe-beating track and trace system for the weevil incidents," says #2|rhinos record victory in world rugby championship against mice team who have never qualified previously, but had elephants running scared in previous game',
 split_comma'president #2 says: "we are following the best scientific advice on the weevil problems," despite no obvious actions being taken|top military advisers say, "this is not the time to panic, but it could be soon."|"i grew my own broccoli," says mouse, "but weevils stole and ate it."|reports of weevils in almost every system.|what is your family doing to cope with the weevil invasion?|princess macaroon wins jousting tournament for sixth year running despite many jousters staying away due to weevils, "i could joust the weevils too," says macaroon',
 split_comma'#1 becoming overrun by space weevil menace.|what do the weevils want? beeb news asks the experts.|raccoon caught selling parsnips dyed orange as fake carrots says "most animals never even noticed." could you tell the difference?|frog croaking championship described as "riveting"|vice president distributes personal broccoli to poor parrots. parrots respond saying they "do not eat the stuff".|our reporters present 10 tips to cope with a hostile invasion from another species in style|snakes record first win in football. defeated lion captain said after the match: "they really used their heads".',
 split_comma'president #2: "we did everything we could to stop weevils, but it has been an unprecedented situation"|top military advisers say "this is the time to panic!"|"we need a hero," says panda, "someone should find the weevil base and take out the queen weevil." who could possibly do that?|president and vice-president still say carrots and broccoli supplies are fine - does anyone still believe them?|"i, for one, welcome our new weevil overlords," says president #2|giraffe sees image of dog in his breakfast toast|panda says he is tired of people expecting him to pursue car thieves in his own vehicle.',
 split_comma'#1 galaxy saved from weevils by lone pilot. all thankful.|president and vice president arrested accused of hoarding carrots and broccoli.|galaxy looking forward to eating better again.|colonists wanted for mission to former weevil system.|sloths, snails and tortoise alliance begin discussions about weevil sightings.|cats still refusing to commit on entering #1 galactic union- are they in or out?',
 },x=-128,item=0},
 {who=0,system=0,station=0,x=-64}, -- M
 dr_start, -- D
 {}, -- U
 split'\fbharn\-enless,\febit of a softy,\fca little harn\-enful,\f6son\-enetin\-enes dangerful,\f9a v\-evee spot deadly,\fabit of a predator,\fbbug hunter,\f7death incarnate,\f8the extern\-eninator',
 split"got any carrots?|searching for some broccoli\nyou got any?|have you heard there\nare weevils invading?|i don't believe in weevils.|i heard there's a planet\nwith naked apes on it\nwho are obsessed with\na thing called 'money'\n- crazy huh?|i'm heading for the \n#1|weevils are\na government hoax|how are you liking\n#2?|my father was called\n#3|make #4\ngreat again!,#2 should be\nan independent system\noutside the\n galactic union|weevils are a government\ntactic to distract us from\nthe real issues|if #2\nwere independent we'd\nnot have this weevil\nproblem|i'm sure our government\nwill sort everything out\nsoon|aunt dahlia's recipes\nare really good\nbut i miss carrots|i think i saw a\nweevil yesterday|do you have any\nbroccoli captain\n#3?|i hope there aren't any\nweevils raiding around\nthe #1,i still haven't found any carrots|what do i have to do\nto find broccoli in\nthis galaxy?|the weevils are just the\ntip of the iceberg\nmark my words|#3 is\na nice name|i'm #2 born and bred!|the #4\nunion needs us more\nthan we need them!,h-have you s-seen\nany weevils\nround here?\nthe news scares me|that's a nice ship you\nhave captain #3|i miss broccoli more\nthan carrots but i\nstill miss carrots|the government has no\nidea what it's doing|i trust our president\nto fix this weevil\nproblem,my brother still refuses\nto believe in weevils|psst - i know\nwhere you can\nget carrots still|what kind of weevils\nhave you seen\ncaptain #3?|don't let the government\nvaccinate you against\nweevil infection\nthey'll put a chip\nin you!|there are some really nasty\nweevils out there now|i told my parents not to\ntravel anymore|i hope the #1\nis still there,i was told that the\nweevils took over an entire\nsystem as a base|this species of weevil\nis supposed to be led\nby a huge queen|#4 has\nno regard for the rights\nof #2!|when i get to\nthe #1 i\nhope they have\nbroccoli|have you seen the\npresident recently?|i heard that the\n#4 fleet has\nrun away from\nthe weevils!|i don't know what i\nwouldn't do for\na carrot right now,hey captain #3 -\nyou're the best!|thanks for saving the\n#4 galaxy|i still can't find\nany broccoli|it's a lot quieter in \n#2 now|i heard they found carrots\nat the #1|#2 would\nhave coped with the weevils\nwithout the interference\nof #4|are you the real\ncaptain #3?",
 2, -- g_diff
 split'\fb  easy,\fcnorn\-enal,\f8  hard', --g_diffs
 {},{},
 unpack_split'0,0,0,1000,10000,10240,2,1024,0,1,0,0,0,0,0,2307,1,1,23,0,5' -- other globals


This is the most extreme example I have and hopefully ever will commit again...

Functions

Arguments

Rely on default arguments and persistent states (like the draw colour) where you can e.g.

print(a,0,23,10,7)
print(b,0,23,10,7)

could be

print(a,0,23,10,7)
print(b,0,23,10)

Also:

camera(0,0)

vs

camera()

The explicit arguments cost every time.

More can be better

As well as passing fewer arguments to a function, you can also pass more - lua doesn't choke. How can more arguments save tokens?
If you are calling functions that you've assigned to variables that you can call with the same code, but require different numbers of arguments then just pass all the arguments every time e.g. draw methods that change between different entities in your game might sometimes need colours or not.
Also remember that a table index that hasn't been defined is treated as nil. So passing tab.foo to a function when tab.foo has never been assigned is just the same as passing nil.
As long as the code in the functions doesn't choke on nil values for those arguments then you don't need any clever control flow to try and get the number of arguments "right".

Example:

function make_ent(dr,x,y,c1,c2)
 return {dr=dr,x=x,y=y,c1=c1,c2=c2}
end

function draw_a(x,y,c1,c2)
-- some drawing code here
end

function draw_b(x,y)
-- some drawing code here
end

ents={make_ent(draw_a,0,0,8,9),make_ent(draw_b,0,20,6,7),make_ent(draw_a,20,49)}

for ent in all(ents) do
 ent.dr(ent.x,ent.y,ent.c1,ent.c2)
end

Taking this example further, I might look to use unpack_split on the multiple literal values in the make_ent calls. I'd then wonder whether I could find a way of describing all the entities in a single string that I could process with split and unpack - there's likely to be more than three entities in my game after all.

For example, currently I'm writing a game that unpacks all the platforms, walls, floors, monsters, goodies etc. via the same function per collection (now I'm wondering whether I could do all collections together...). The number of bits of data per each entity is the first number followed by values for each entity. i.e.

-- each spid has 3 values x,y position and y vector
g_spids=unpack_raw_data'3,550,160,1,540,40,1,200,70,1,300,90,1,800,25,1,864,200,-1'

I suggest using an editor to produce strings like this rather than try to write the data by hand. The PICO-8 printh function is v handy for getting data out of an editor into a file or to the console you run PICO-8 inside. You do run PICO-8 from the command line, right? :)

Note: a lot of characters are wasted doing it this way e.g. every ','. Hex values would be more compact as well. If you can keep all values within a byte range then encoding them as raw characters works very well too and Zep has even given us the magic function to decode them here: https://www.lexaloffle.com/bbs/?tid=38692.
I use this for image data and sfx data (hope to write about both soon).

Thinking about functions again, remember that in lua the following are equivalent:

tab['a']
tab.a

So that by putting the draw functions in the example further up into a table you could specify which function to use via an element in a data string. Something like:

function make_ent(dr,x,y,c1,c2)
 return {dr=draw_funcs[dr],x=x,y=y,c1=c1,c2=c2}
end
...
for ent in all(ents) do
 ent.dr(x,y,c1,c2)
end

Or even at call time:

for ent in all(ents) do
 draw_funcs[ent.dr](x,y,c1,c2)
end

(I suspect that would be slower - maybe easier to debug though)

If your data parsing function is clever enough then you probably don't need a separate constructor function like that at all.
Along those lines, say if every entity has position (x,y) and most have velocities (vx,vy) but one type only has a colour and no velocity then it's nice to code it with a proper index name "ent.col", but if it means writing separate construction code then consider just using ent.vy and a comment(or at least adding something like local col=ent.vy when you need it).

I haven't done this, but it may be even more efficient to dispense with named elements in your tables so you could do:

for ent in all(ents) do
 ent[1](unpack(ent))
end


This works, for example:

ent=split'1,e,1,2,8'

cls()

dr_funcs={
function(d,...)
print(...)
end
}

dr_funcs[tonum(ent[1])](unpack(ent))

todo: random numbers, number indices vs named indices, caching table values in local variables (fewer tokens and faster too!)

Bonus: unpacking into memory

I'm going to add more to this post, but for now I'll end with this:

Have a table of values e.g. an image stored as bytes? Want to dump it into memory or onto the PICO-8 screen easily? You can as of the recent PICO-8 updates:

poke(0x6000,unpack(data))

6 tokens
Bang - straight onto the screen. Poke4 is even quicker and the same number of tokens if you have your image data nicely packed into table values. I use this for 'extra' sprite sheets, for instance.
Be warned though: down this road lies the terror of the compressed size limit...

(I tried this just now vs memcpy(0,0x8000,0x2000) and it's much quicker - not v scientifically though so YMMV)

P#99050 2021-10-22 21:38 ( Edited 2021-10-23 15:51)

Early on in playing around with PICO-8 I wrote a function to print text with a black outline.

function prt_out(s,x,y,c)
print(s,x-1,y,0)
print(s,x+1,y)
print(s,x,y-1)
print(s,x,y+1)
print(s,x,y,c)
end

I imagine a lot of people have also done this and I can't be the only person who found it a bit clumsy and, near the end of a project when casting about for tokens, wondered if those five very similar print calls couldn't be reduced.

Nowadays we have P8SCII so I thought I'd have a look. Here are some candidates I've written with their token counts and times from my crude testing:

function p1(s,x,y,c) -- 42 tokens, 5.6 seconds
?'\f0\-f'..s..'\^g\-h'..s..'\^g\|f'..s..'\^g\|h'..s..'\^g\f'..chr(c+(c>9 and 87 or 48))..s,x,y
end

function p2(s,x,y,c) -- 26 tokens, 6.2667 seconds
for i in all(split'\f0\-f,\-h,\|f,\|h') do
?i..s,x,y
end
?s,x,y,c
end

function p3(s,x,y,c) -- 38 tokens, 5.6667 seconds
?'\f0\-f'..s..'\^g\-h'..s..'\^g\|f'..s..'\^g\|h'..s..'\^g\f'..sub(tostr(c,1),6,6)..s,x,y
end

function p4(s,x,y,c) -- 30 tokens, 5.7 seconds
?'\f0\-f'..s..'\^g\-h'..s..'\^g\|f'..s..'\^g\|h'..s,x,y
?s,x,y,c
end

function p5(s,x,y,c) -- 42 tokens, 6.1 seconds
print(s,x-1,y,0)
print(s,x+1,y)
print(s,x,y-1)
print(s,x,y+1)
print(s,x,y,c)
end

function p6(s,x,y,c) -- 29 tokens, 6.2 seconds
for _,i in pairs{'\f0\-f','\-h','\|f','\|h'} do
?i..s,x,y
end
?s,x,y,c
end

function p7(s,x,y,c) -- 37 tokens, 6.2667 seconds
for i in all{'\f0\-f','\-h','\|f','\|h','\f'..chr(c+(c>9 and 87 or 48))} do
?i..s,x,y
end
end

function p8(s,...) -- 21 tokens, 6.3 seconds
for i in all(split'\-f\f0,\-h\f0,\|f\f0,\|h\f0') do
?i..s,...
end
?s,...
end

'?' vs print makes no difference to performance, but does save characters and 1 token each use.

I was rather surprised that p2 uses so few tokens and isn't that slow. Also that p1 is actually the fastest (but only just).

I usually find that tokens are more precious to me than a tiny bit of performance so I'm likely to use p2 above (or p8 even). p4 is pretty cheap at 30 tokens and faster than the naive approach so perhaps it's an overall winner (so far).

I'm very interested to know if anyone has a better way(?)

v1 edit:

  • freds72's suggestion is p6. 3 more tokens for .0667 seconds faster. Using pairs() instead of all() causes half of that saving.
    I thought removing the split might make more difference than that, but now I feel a bit better about having split all over my game code to save tokens(!)
  • I removed the unnecessary '\^g' (go home) parts from the P8SCII snippets where appropriate. This saves some characters and performance (but also somehow saved the odd token and I'm not sure how).

v2 edit:

  • a new winner for tokens: p8 at 21! But at the cost of being the slowest at 6.3 seconds in my test. Not by much though. It still works because the P8SCII overrides the colour value being passed.

Cart #drakeblue_prt_out-2 | 2021-11-11 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
10

Bonus

Specify the colour of the outline:

function p4bonus(s,x,y,c,o) -- 34 tokens, 5.7 seconds
color(o)
?'\-f'..s..'\^g\-h'..s..'\^g\|f'..s..'\^g\|h'..s,x,y
?s,x,y,c
end

(Please use any of these functions however you want)

P#98796 2021-10-17 18:19 ( Edited 2021-11-11 14:07)

Cart #db_reflection-1 | 2021-10-15 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
7

A simple demo of a reflection.

Press X to enable/disable the reflection effect.
Press Z to enable/disable the ripple effect.

Use up/down to move the view up and down i.e. change the amount of reflection on the screen.

Uses the extra palette to dim the portion of the screen where the reflection will be.
Draws everything else like normal above the reflection surface.
'memcopy's to the lines below the reflection surface, starting with the line immediately above it.
Adding an offset with some sin() calls moves each line a bit to allow the ripple effect. Downside is a little bit of mess at the edges.

[edit] Minor bug fix to make ripple effect constant with distance from the shore.

P#98701 2021-10-15 16:09 ( Edited 2021-10-15 16:57)

Fireworks

Cart #drakeblue_fireworks-3 | 2021-11-05 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
10

Some fairly simple particle fireworks just for fun.
Press X to switch the screen effect on/off. For more about that see here: https://www.lexaloffle.com/bbs/?tid=41149

Every sixth of a second, 50-100 Particles are emitted at a random point on the screen with random angle/velocity determined by a tiny bit of trig. They're given colours in the top half of the PICO-8 palette (7+) and as the particles get old the colour changes according to the same mapping that the screen fade effect uses to a darker colour.
If they run out of life or become black then the particles are deleted.
There are two possible update functions for each particle - one with a wiggle :)

-- update: tidied code a bit, updated fade effect.
-- update 2: added a flash when the fireworks detonate. Other tweaks. Happy Fifth of November :)

P#90763 2021-04-18 14:02 ( Edited 2021-11-05 14:34)

Making PICO Space

Cart #drakeblue_picospace-0 | 2021-04-03 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
26

This is a rambling description of some of what went into making PICO Space. I've tried to write it for most readers to follow - there's some basic stuff and nothing very advanced. Hopefully it's not too dull and might help someone.

When You Wish Upon a Starfield

Coming up to Christmas of 2020 I had been spending most of my dev time trying to squeeze image data into PICO-8 to make a Dungeon Master clone (I promise I will return to this at some point). I'd got a bit tired of writing compression and encoding routines and feeling like I was fighting PICO-8 rather than playing nicely with it. I'd seen some other nice star-fields and particles in other peoples' games and wondered how much it would take to do my own.

I'd also read about the CAMERA function and suspected that would help - using CAMERA to transform the "view window" once instead of transforming the positions of every single particle many times seemed like it'd be a big win in terms of performance.
I knocked up the following while the girlfriend was watching a Christmas movie to try it out. Ironically, the stars in the final game don't benefit from the CAMERA function at all - although the other particles and everything else do.

Cart #drakeblue_camerastars-0 | 2021-04-07 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
6

There's not a lot to it: use directions to "fly" the red line around (there's nothing there, but the star field). But this is the basis of how the ship flies in PICO Space.

I wrote a post about the stars and fade effect here: https://www.lexaloffle.com/bbs/?tid=41149

Cart #fading_stars-5 | 2021-04-12 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
9

I considered a rotate and thrust method, like in games like Thrust, Oids etc. but decided to keep it simpler, friendlier to those who didn't grow up in the 80s and allow the game to be more dynamic. Left, right and thrust would have saved a precious PICO-8 button though...

In the final version, there's a constant deceleration added to the velocity components of the ship (which gets wiped out if you hold down the controls) - and a very low minimum velocity. Why?

Not Just a Triangle; Not Even a Triangle

The ship (and the NPC ships) are all drawn in the game using a combination of lines and a sprite to cover the hole in the middle (yes, really - I had some triangle drawing code sat in the p8 file for weeks without ever actually using it in the end so it got cut). The vertices used for the lines are calculated from a normalised vector of the ship's velocity. If the velocity gets too close to zero then this calculation, the vertex calculations and the lines don't come up with anything sensible and the ship doesn't draw correctly.

-----------------------------------------------------------------------------------------
-- draws the player or an npc ship
function draw_ship(s)
 ship_pal(s)
    -- calc some vertices based on current heading angle
    --    0
    --   (0)
    --  2   1
    local z,an=s.z<1 and -1 or 1,s.an
    local s0,s1,s2,c0,c1,c2=sin(an)*z,sin(an-0.15)*z,sin(an+0.15)*z,cos(an)*z,cos(an-0.15)*z,cos(an+0.15)*z
    local svx0,svy0,svx1,svy1,svx2,svy2=3.5*c0,3.5*s0,-3.5*c1,-3.5*s1,-3.5*c2,-3.5*s2

    line(svx0*1.2,svy0*1.2,svx1*1.2,svy1*1.2,2)
    line(svx0*1.2,svy0*1.2,svx2*1.2,svy2*1.2)
    line(svx1,svy1,svx2,svy2)

    line(svx0,svy0,svx1,svy1,8)
    line(svx0,svy0,svx2,svy2)
    line(0,0,svx1,svy1)
    line(0,0,svx2,svy2)
    line(0,0,svx0,svy0)

    spr(unpack_split'0,-4,-4') -- cockpit
    reset_dr_pal()
end

ship_pal sets up the correct colours for whatever ship is being drawn - there's a pair of values for every brighter colour apart from the blue used for the cockpit.

The z variable deals with the "flip" ability that the ship has so you can face backwards - otherwise, while the ship does have some inertia it will always be drawn to point in the direction it's moving.

I'll explain more about "unpack_split" in another post.

Here's the ship without the cockpit sprite:

reset_dr_pal is a function that acts like a call to pal with no arguments, but doesn't affect the screen palette. I've used it for a few projects now:

function reset_dr_pal()
 poke4(0x5f00,0x0302.0110,0x0706.0504,0x0b0a.0908,0x0f0e.0d0c)
end

The latest PICO-8 letting consecutive values be multiple pokes at a time has let me optimise this so it's a bit opaque even to me. IIRC it dumps the equivalent of 0,1,2,...,15 into the correct part of PICO-8's memory to reset the draw palette and whatever magic needs to be done for palt too(?), but doesn't hit the screen palette range of memory.

Feel free to lift this for your own code if you think it'll be useful - no warranty expressed or implied ;)

I tweaked the ship drawing code a few times, but it never varied much. Once I had a ship flying about I needed somewhere for it fly to so I made a "planets" table that was populated with random coordinates, drew a circular planet sprite and hit go. That's gone now - it was handy to have as a placeholder, but I don't miss it.

Start of a Solar System

Once I had planets it made sense to me to add a sun/star, but I wanted it to be bigger than the planets. I'd already used 64x64 of the sprite sheet with the planet sprite so I wondered what I could do with PICO-8's circfill command.

It turns out that circ and circfill let you draw pretty large circles for not a great deal of perf cost. I tried various sizes and settled on an 800 pixel radius circle for the sun with some extra circles around it. One is a border, the others are there just because I like how they look. My "game design" excuse is that they give some warning to the player that they're about to fly into a star - not v healthy for their ship.

I have another cart that draws a partial circle within another circle to show the sun on the scanner, but the token count was too much. In the end, the sun on the scanner is drawn by generating lots of points then checking if they are within both the circle of the sun and the circle of the scanner; then drawing a circle of radius 1.

Originally I had yet another circle for the edge of each system - but as the systems got busier the performance cost was just too high so I needed something a bit more "sophisticated". I noticed that the edge circle was so large that it never looked like anything else but a straight line on the little PICO-8 screen so in the game I do a little bit of maths to work out two intersection points with the edge circle and draw a line between them i.e. the edge circle segment is approximated with a line.

--edge of system
if g_dist_to_sun>g_edge-23 then
 -- approximate with drawing tangent line at angle through ship's coordinates
 -- since ship is so far from centre and edge circle is so large
 -- edge is a circle around 0
 -- get angle to ship
 local an,ed=atan2(S.x,S.y),g_edge+rnd'16'
 -- get tangent angle at 90deg and coords of point on edge (with some jitter)
 local an2,ca,sa=an-0.25,cos(an)*ed,sin(an)*ed 
 -- get vector of line using tangent angle
 local ca2,sa2=cos(an2)<<7,sin(an2)<<7
 line(ca-ca2,sa-sa2,ca+ca2,sa+sa2,p%8+8)
end

I use g_dist_to_sun to conditionally draw the sun when needed as well.

Having a circular edge seemed like a nicer way to stop the ship flying too far out of the game than just clamping to a rectangle or wrapping the coordinates. I had a really fancy bit of code that would turn the ship around from anywhere outside the system and force it back. In the end just adding a vector pointing towards the centre of the system to the ship's velocity is what makes it behave like a stuck blue bottle behind glass in the game.

  g_dist_to_sun=dist_trig(S,g_sys.sun)
  if g_dist_to_sun>g_edge then
   local avx,avy=get_dir(S,g_sys.sun)
   vx+=avx
   vy+=avy
  end

Trigonometry for Distances

(As any fule kno) the distance between a point A and B is the square root of the sum of the squares of the coordinate distances i.e. d = sqrt(dxdx+dydy) -- for 2D

Except not in my game.

In a fit of madness I decided to make the systems pretty large, 10240 pixels in radius, in fact. PICO-8 numbers are a 16-bit fixed point type with a range of -32768.0 to 32767.9999847412109375 (0x8000. 0000 to 0x7fff). Which means I was fine until I needed to calculate a large-ish distance and had to square numbers over 1000 e.g.

dist({0,0},{9000,8000}) = sqrt(9000*9000 + 8000*8000) = sqrt(81000000 + 64000000)

Um...

Weirdly, I got a really long way into coding before this actually became a problem and I'm not quite sure how that happened - especially since I was expecting it all along, I'm no stranger to the limitations of number types or even to fixed point floats.

One method I tried to get round this was to divide every value (bit shift in fact) before the squaring operation and then shift back afterwards. I'd lose precision, but in theory it'd work fine i.e. something like:

dist = sqrt( (dx>>4)*(dx>>4)+(dy>>4)*(dy>>4) )<<4

Most of the time I'd only needed the shifted squared value. But I had real trouble getting it to work well - possibly because of other code around it. And sometimes I wanted the true distance (with the sqrt) anyway.

I'd been doing some experimentation with the SIN and COS functions and I'd noticed that they're very fast on PICO-8 and I wondered about whether this might be another solution.

A bit of trig later and I had a candidate:

-- distance with trig
-- to avoid large numbers
-- not v accurate, but seems good enough.
-- also seems v fast on pico-8
function dist_trig(a,b)
 local x,y=abs(b.x-a.x),abs(b.y-a.y)
 if x<y then x,y=y,x end -- accuracy goes down massively if x is much smaller than y so swap them :)
 return x/sin(atan2(y,x))
end

I discovered the accuracy problem mentioned in the code a bit later - it manifested as the ship spontaneously diving into the sun if you flew too close to the top or bottom of it - and only those two points. The code to stop the ship escaping the edge of the system was firing and pushing the ship into the centre of the system because the distance calculation was so bad it thought the ship was at the edge not right by the sun!

Anyway, this function gets used all over the place now and was favourably comparable in accuracy and speed to any of the shifted distance functions that I tried.

Galaxy Generation

Of course, the more sensible solution would probably have been to make the star systems smaller and "zoom" the coordinates only for drawing, but I'd already done most of the work on galaxy creation and drawing and didn't really want to change it too much. I think I actually tried it for 30 minutes, made a mess and then gave up.

When you choose a new game or load from a saved game in PICO Space it generates the whole galaxy from an integer seed (your captain's name is a different value; otherwise they couldn't travel to other galaxies...).

The stars are all generated from the galaxy seed plus a fraction part e.g. the fourth star has something like id=galaxy_id+4*0.0001. This means there are potentially 65535 galaxies (I think, or at least 32767) with every system having a unique id/seed "in the gaps".

Apart from star number 1 which is always at (0,0), the stars are given random positions within a fixed distance from that star with the condition that they can't be too close to an already existing star. This is v fast.

Then the code links the galaxies with wyrmholes (called gates in the code). First it makes a minimum spanning tree with wyrmholes:

  • find the star with minimum distance from star 1
  • add wyrmholes to link the two
  • add the new star to star 1 in a "found" set
  • find the closest star to any star in that set
  • link with wyrmholes
  • repeat last 3 steps until every star is in the "found" set

This way I know that every system is connected to the galaxy as a whole. Without this some systems aren't reachable (I had considered having a jump drive upgrade that let you jump between groups of stars - maybe next time).

Unfortunately my algorithm to calculate the minimal spanning tree gets v slow after relatively few iterations (checking the closest star in an n-size set of stars has a pretty bad O value) and acts as a mechanical limit to how big the galaxies can get.

I'd be more worried about it, but for two things:

  1. The loading view covers it well. It looks like the stars are being generated, but it's actually showing the stars being added to the minimum spanning set.
  2. It's fast enough that even with 70 stars it's not that slow and I think 70 stars is probably enough.

--controversial?--I've noticed in a lot of procedurally generated games there's a whole load of repetitious content where there doesn't really need to be - it doesn't add much to the game and feels a bit like it's there just because the developers could. Now PICO Space is still pretty guilty of this - there's a lot of space in PICO Space and 70 systems is probably still way more than most players will ever visit. The systems don't have that much variety - I really wanted to put in more and kinda of heart-breakingly PICO-8 is easily fast enough to allow it. But there's not much cart space and I just couldn't find the tokens. There were a lot of things I would have added without the token limit and even some stuff I had to cut before release. I never found myself wanting to add more star systems. In fact, for a lot of dev time I reduced the number to make testing faster.--/controversial--

Anyway, long story short: I didn't waste any more time (or tokens) optimising this, instead I slapped in a logo and a few star spr calls and pretended it was a deliberately designed intro sequence...

Once that's done, there are a few iterations after the spanning tree search looking for short distances between stars and adding more gates between them so that there are "loops" and it's a bit easier to get about. This is pretty fast too.

Generating Systems

Then the systems themselves are fleshed out. Originally I generated each system in the galaxy only when the player entered them. In fact, all I had was the current system and some wyrmholes in it. When the player entered a wyrmhole I took the seed value associated with it and generated the new system. I even added a wyrmhole back to the previous system using its seed to give the illusion of persistence. For the galaxy map I was going to run this generation on the fly when it was opened to a certain depth of wyrmholes and that way I'd have a near to infinite galaxy and tiny memory footprint!

Typing it out now it still seems like it might have worked, especially as I know the scope of what's in the game. At the time though, I ditched that idea since I didn't. The final straw was wanting to put "missions" into the game.

Each system has wyrmholes and planets which also may have space stations orbiting them. The wyrmholes, only really have a position, a destination id and a draw function. They are called wyrmholes because I was hoping to have "space wyrms" in the game. Sadly, that never happened.

Planets

Planets have radii and colours and are added in distance "slots" (with the wyrmholes) radiating out from the sun - this way there shouldn't be any collisions between them, even as the planets orbit around the star. Yep, the planets do orbit in the game, but their positions only update when entering a system. I had them update along with everything else for a long time though.

As mentioned elsewhere in this post, the scale of each system is quite large. The planets orbit in circles determined by sine and cosine functions combined with their distance from the star (I balked at setting up stable systems with realistic gravity calculations if only because I'd disappeared down enough wyrmholes already). Unfortunately, the precision of these functions isn't high enough to allow smooth movement when any distance from the sun. It caused the planets to "jump" between positions on their orbit. Unperturbed I wrote a routine to interpolate between the imprecise positions so that the planets would still orbit smoothly. It worked really well and I was sad to have to cut it to regain the tokens it needed.

I was able to leave the scribbly planetary rings in place though, which are drawn by generating a bunch of vertices using sine and cosine values (with random jitter) and drawing lines between pairs of them. Draw the back half of a ring first, draw the planet, then draw the front half of the ring.

Like the planet interpolation, I designed them in a separate test cart first:

Cart #drakeblue_rings-0 | 2021-04-08 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
6

The code in the game is very similar. Knocking up little test carts is nice in PICO-8 since it's v lightweight to get going. Doing this also means less messing up of existing code, especially if you change your mind and it means less flying about trying to find the thing you want to test.

Space Stations

The space stations do orbit as you fly, around the planets. A planet has a station entry in its table so there's only one per planet (early in dev I had up to 4!) and the space station positions get updated with everything else that updates. When you're docked the ship has to match this (that was a fun bug).

The space stations have three different sprites, a few circles and a pal call in their draw function to provide some cosmetic variety. I had written a little cart which would allow me to specify shapes, sprites and map commands in a string that was interpreted at runtime to give many more looks for them, but tokens and performance stopped me using that. And I hit the compressed size limit quite badly too.

At one time there was a ship's garage with mechanic NPCs in every station but alas tokens...

Missions

(kinda)

I wanted to add a whole different variety of these, but eventually I only made the passengers on the noticeboards at the stations - the simplest missions I could think of. They're even more basic than fetch quests (find something and bring it back - sounds complicated!) or even deliveries (plausibly you only have room for one passenger in your small ship so I only needed to track one mission at a time). I ran out of tokens before I could attempt adding any more types.

To make the passenger missions work I needed to have some knowledge of not just the name of the destination system, but also of the objects in the system; specifically the space stations. I'd hoped to have docks on planets, but, again, the token limit got the better of me.

It was so much easier to have the destination systems pre-created and in memory for this. PICO-8 has a generous 2MB for such data (my first computer had 64KB and David Braben and Ian Bell made do with a fraction of that). PICO-Space only ends up using about half of that at any time (when it's working correctly).

The game takes a seed of the current space station id and the progress that the player has made through the game and uses that to generate a number of notices with destinations and a seed for the animal wanting a ride. That way, if you return to the notice board it keeps the same notices, at least until you complete a mission.

When you finish enough journeys a progress global is increased, an upgrade is awarded, different news & NPC messages are unlocked and more dangerous weevils may appear. There's an upper limit to how many journeys are needed for each progress point (four I think) so every player should progress reasonably quickly.

Silly Names

Nonsense word generators are something I've written a few times in the past. They're very effective (and entertaining) for something so easy to do. If you've never written one I highly recommend it, especially for newer programmers.

There is a single list of syllables and a single function that generates all the names in PICO Space. If I had more space/tokens then having different syllable sets etc. per species or system or something might have been fun, but I never felt like this was a weak part of the game and garnered an embarrassingly large amount of amusement out of some of the silly things it came out with.

------------------------------------------------------------------------------------------
-- generate some wacky random names
function gen_name(s)
 stash_n_set_seed(s)
 local n,nm=rnd_spl_str"1|2|2|2|3|3|3",''
 for i=1,n do
  if i>1 and rnd(15)<1 then
   nm..=' '
  end
  nm..=rnd(g_syls)
 end
 unstash_seed()
 return nm
end

The gist of this is:

  • choose length for the name in number of syllables
  • for each syllable up to thate length choose a syllable string and append it
  • occasionally append a space instead of a syllable

The stash_n_set_seed function is my hack to get around the trials and tribble-ations of working with a random number generator for procedural generation (as opposed to a noise function). See below for more about this.

Here's the syllables:

split"ca,bal,da,gar,non\-en,pol,der,arp,bun,duc,kit,poo,v\-evee,zir,buf,v\-evil,xan,frak,ing,out,re,far,do,tel,tri,cry,quack,er,dog,pup,sno,ger,bil,pa,n\-ena,jan\-en,es,on",--g_syls

For more serious names, don't put in such silly syllables e.g. poo.

Ms and Ws

The eagle eyed amongst you may have noticed that PICO Space has "wide" 5x5 pixel M and W letters instead of the usual 5x3 characters that PICO-8 has by default. My other games, e.g. P8C-BUN have similar wide Ms and Ws too. In those games, I have a custom print function that iterates through whatever string is to be printed and when it finds an M or W it replaces it with a sprite that has the wide version of the letter. I added this after I got some negative feedback about the legibility of the text in a game.

Originally PICO Space used the same function, but I found it was a bit of a performance problem. PICO-8 has a much more "spiky" processing time per frame than my other games have had.

Fortunately, zep's addition of P8SCII in the newer version of PICO-8 came at exactly the right moment and allowed me to replace my routine with these magic codes:

m=>  n\-en [print an n then shift the cursor back 2 pixels (\-e) and print another n] 
w=>  v\-ev  [u\-eu looks okay too]

As long as a find/replace is done for those characters in a string to be printed then it "just works" and I was able to ditch my custom print routine at last. This can be done "offline" even. The only tricky part was trying to centre text when the width was no longer as predictable - I do have a mostly working solution for that. Ask me if you're interested.

Taming RND

PICO-8 has no noise function, but it does have RND and a seed set function SRAND. I considered writing a noise function, but I suspected I'd want the tokens for something else and I was concerned about performance. I'd already used RND for the first hacky progress in the game when I thought about this so I gave up on noise and set up a function that saved the current random number (or at least whatever RND returned at that moment) and called SRAND with a value I'd passed to it. Then I was careful with the order I called RND in and did a lot of testing to make sure that I got consistent or random results as appropriate.

For a particular seed, RND always returns the same sequence of subsequent results so, for instance, setting the seed before choosing portrait sprites and generating a bunch of names using RND means you'll always get the same output. That's why there's always the same animal minding each station and the same notices each time you look on a station's noticeboard (at least until you ferry another passenger). I'd restore a "random" seed after using RND in this way so that, for example, the NPC generation stayed unpredictable.

-- set seed and store a random number for later
function stash_n_set_seed(seed)
 g_old_seed=rnd()
 srand(seed)
end

-- restore "randomness"
function unstash_seed()
 srand(g_old_seed)
end

This works pretty well all considered and I never felt I was having performance problems from it. I call the time function for the galaxy seed and pilot id just to mix it up even more.

Animals

I needed characters for the game and I considered identikit faces like Elite 2 uses for a bit. In the end, I drew some animals as placeholders and then never looked back.

Animals feature in my games. Get over it.

I wanted the portraits to move so initially I was going to add animation frames, but horizontally flipping was easier and meant I could have more unique pictures. I figured it was "good enough".
[32x32]

There are 59 animals in total.

News and NPCs

The Beeb News was my first attempt to communicate some story in the game and add a bit of narrative colour (alongside the colour colour). The news stories develop as the player progresses in the game - there's a different set after each upgrade is achieved. I used a very basic template filling function to generate a bit of customisation for each game - e.g. the galaxy name and the president's name change depending on which galaxy you're in.

------------------------------------------------------------------------------------------
-- substitutes values (#n) into a string for the corresponding entry in data (data[n])
function template_fill(t,data)
 local i,out=1,''
 while i<=#t do
  local c=sub(t,i,i)
  if c=='#' then
   i+=1
   out..=data[tonum(sub(t,i,i))]
  elseif c=='m' then
   out..='n\-en'
  elseif c=='w' then
   out..='v\-ev'
  else
   out..=c
  end
  i+=1
 end
 return out
end

You'll notice the M and W substitutions in this - in the last week of development I had to add this to reduce the size of the strings used for the news and NPC chat in order to fit the game under PICO-8's compressed size limit. Fortunately, it works very well and turned out to be quick enough.

The news is drawn using the same scrolling message system that is used for the upgrades and notice board, just with different colour parameters.

----------------------------------------------------------------------
-- draws a bar of scrolling text
function draw_scroller(scroller,y,x,c,bc)
 clip(x,y,128,y+6)
 rectfill(x,y,128,y+6,bc)
 print(scroller.message,x-scroller.x,y+1,c)
 clip()
 scroller.x+=1
 if scroller.x-x>scroller.len then
  scroller.x=-64
  return 1
 end
end

NPCs

The other attempt at story telling comes from the NPC ships. These will send you random messages when they're on-screen. Like the news, the template function adds a bit of customisation e.g. for the current system and there are different sets of messages for each stage of progress in the game.

The NPCs are (obviously) drawn in the same way as the player ship, but on top of basic attributes like position, velocity, colour etc. they also have a target that they head towards; either a station or a wyrmhole. They're spawned from a station or from a random position (with hyperspace particles) to pretend that they've just jumped in. I'd love to have more persistent NPCs, but I probably spent too many tokens, performance and cart space on them already.

One of the more interesting problems to solve was how to stop NPCs spawning in a position then flying towards a target on the other side of the system's sun without them hitting it. I don't check them for collisions so they'd just fly right on through blissfully unaware of any problem. Given the player can't do that it was a bit non-ideal to witness ships emerge from the superhot plasma if you happened to be flying close to the sun at the time. It got even more obvious when I stopped pausing the simulation in the System Map since you could watch the NPCs sun-diving:

I tried various clever solutions with trigonometry and mathematics, but they never worked very well. In the end, I solved this problem with a hack. Whenever an NPC is generated their journey is quickly simulated, start to finish, but with coarser framerate. If they get too close to the sun then that journey is ditched and the NPC journey is generated again until a valid journey is produced. This does make for the occasional performance spike, but on the whole works quite well and for few tokens.

There's a cap on the number of NPCs per system that takes into account the number of weevils to try and make sure there's quite a bit of performance headroom, just in case.

Enemies

To be honest, I was having so much fun making a pretend universe that I left it quite late to add in a challenge to the game. Eventually I admitted that I needed villains so I drew on personal history.

A few years ago we found weevils in our kitchen. All over our kitchen. Eventually we tracked them back to a bag of birdseed that now moved and crackled by itself...

The next hours and days consisted of rooting out weevil and destroying it - weevils became a kind of bogeyman in our house. Nothing is more evil than weevil in our home.

[32x32]

I wanted to make each weevil unique, but in the end they are all based on the same code with varying attributes. As the game goes on more and more dangerous weevils are generated ahead of the ship within a random circle depending on how dangerous the system is and the difficulty level of the game. Some weevils can hit the ship and damage it with their collision, all weevils fire missiles at the ship at different rates and for different amounts of damage.

Space is Big, even PICO Space

One of the biggest problems with the weevils was how to make them possible to fight on the tiny 128x128 PICO-8 screen without making them too slow to stop a player just flying past them. I didn't want to slow down the player either, since getting between locations was taking long enough already. Besides, when I tried slowing the ship, it just felt... bad.

I think the weevils are still a bit too fast to be fair - I'm not convinced that I really solved this problem entirely.

WARNING SPOILERS - DO NOT READ IF YOU WANT TO LEAVE THE ENDING AS A SURPRISE

Weevil Queen

I wanted an ending and a "boss" and so I did some research into big weevils (as you do). It turns out that there are some big ones out there and there are even some that live like ants with a queen. Not to the same scale as ants though.
Add a bit of poetic licence and I came up with the weevil queen that's in the game.
What do you mean: "there isn't one?"
Are you saying you've not completed enough missions to gain the Prototype Scanner upgrade, located the Weevil Base and jumped into the system to fight the Weevil Queen and save the galaxy?
At time of writing, I'm not sure anyone has except me (many testing times). If you have I'd really like to know (please?).

The Weevil Queen is generated in a system that has only one wyrmhole in it (a leaf node in the galaxy "tree") and is by default hidden in the Galaxy Map (and the wyrmhole to it is hidden in the system containing it too). Some of the changes for the weevil system happen during galaxy generation. Most of it is done on entry. There are no space stations or NPC ships and the weevil queen is added. She behaves a lot like a normal weevil, but she has hitboxes at the back of her that don't take damage and she moves more slowly. She fires the most deadly missiles, most frequently. She may also teleport randomly if hit.
The weevil queen is drawn using several sprites together and she gyrates at a speed according to how close to destruction she is.
[32x32]
[32x32]

function(q)
     pal(15,0)
     local en=5-q.life/10

     local sn48,cs48,sn32,cs32=sin(p/48*en),cos(p/48*en),sin(p/32*en),cos(p/32*en)

     q.x+=sn32

     circ_orig(p%96,g_sys.sun.pl[q.life\10+1])

     spr(135,-25+sn48,-10+cs48,2,2) -- right front leg
     spr(135,11-sn32,-10-sn48,unpack_split'2,2,1') -- left front leg
     spr(135,-29,cs32-52,2,2,nil,1) -- right back leg
     spr(135,13,cs48-52,unpack_split'2,2,1,1') -- left back leg
     spr(137,12+sn48,-26+sn32,unpack_split'2,2,1,1') -- left back leg
     spr(137,-27-cs32,-26+cs48,2,2,nil,1) -- right back leg

     spr(128,sn48-15,unpack_split'-50,2,4') -- right wing
     spr(130,-sn48,unpack_split'-50,2,4') -- left wing
     spr(132,cs48-13,unpack_split'-29,3,4') -- thorax
     spr(139,-10+cs48,-8-sn32,3,3) -- head

     reset_dr_pal()
   end

When she is destroyed, she is changed into the Intergalactic Wyrmhole that leads to the next (harder) galaxy, the weevil system loses its weevil flag and all the weevils are destroyed.

I spent a lot of time getting all this to work, all the time wondering if anyone will ever see it.

Tables and "Objects"

I'm not much of an object oriented zealot (learning to program in BASIC in the 80s does that to you), but for the entities in PICO Space I used tables as objects of a kind.
Things like planets, stations, weevils (called aliens in the code) and NPC ships generally have "standard" attributes (x,y for position, bounding box for draw culling etc.) and functions (update and draw).
On entering a system an "updateables" and a "drawables" table is populated with appropriate things from those tables in the current star system and the update and draw functions loop through these and call them as appropriate. Using the CAMERA function I set the origin for drawing to reflect the object's position then the draw function draws from there. Things with a map function get drawn on the System Map etc. It works quite well and feels a bit like adding components to entities.
I had a lot more separate lists and loops for these early on just to get going - I find doing things "the dumb way" first and improving it later works well for me (along with, if it ain't broke don't fix it...).

Some things e.g. particles are kept separate still, for speed.

Particles

There are two big types of particle in PICO Space. Points and circles. There's a function to add both of these in a single line each. They have a lifetime, position and update functions.

Points have two update functions - a typical velocity update one and a more abstract randomly swapping x and y velocity update. The former is used in the explosions. The latter is most obvious in the hyperspace cloud at the very start of the game.
Circle particles draw at a different size corresponding to the life left of the particle.

Why do they look the way they do? It's the screen fade effect.

See https://www.lexaloffle.com/bbs/?tid=41149 for more information.

Menus

All the menus use the same code and are driven by data passed to them. There's nothing amazingly clever about them (or that), but I found it was much nicer in every respect to work with them once I took this approach. So I suggest writing generic menu routines soon if you find you're needing more than one of the things in a project - don't put it off.

Memory and Performance

PICO-8 makes both of these things really easy - Ctl/Cmd P along with a constant print of stat(0) if it went above a threshold value (usually 1024).
The latter helped me find a nasty problem very near release. For a long time I'd paused the game except when flying the ship in space, but particles were drawn and deleted in the draw function. When I enabled the game to update, even while in the maps and docked, I was generating particles (NPC ship engines, NPC hyperspace, sun flares), but never deleting them. So I'd run out of memory. Sometimes after a very long time. It happened much faster when I moved the cursor about in the map views so I spent ages debugging those to no joy only to eventually realise that I was running out of memory due to the ship engines firing a bunch of particles out into my dwindling memory...

Loading and Saving

I was very worried about getting the load and save functions to work, especially with the web player, but in the end it was very easy. Dump some values into cartdata to save, read them back when loading. I'm presuming the browser cache gets these when playing in a browser(?).
One slight downer is the paste codes don't work on the web. They seem to work fine in "native" PICO-8 though and the "binary" versions. It's the only thing that doesn't work there.

The best thing about having save games was for testing during development. I built up a text file with a long list of different saves that allowed me to test different stages in the game, different galaxies and systems etc. I could edit them a bit to quickly test some things as well.
I'd highly recommend implementing something like this if you have a longer game and want to test it - especially since it's something your players might be able to use too.

Tokens

As you may have gathered if you read this far, I ran out of tokens.
Procedural generation produces content from code in preference to pre-defined content in storage so it wasn't overly surprising. I've seen others struggle with memory for proc gen, but I found the 2MB limit wasn't ever a problem.

A large amount of the end of development involved trying to save tokens.

Look out for my followup post about that, working title: "A desperate programmer's attempts to squeeze galaxies into a PICO-8 cart"

--
If you have any questions about the above post or anything else to do with PICO Space please ask them below and I'll try to answer them (or maybe even extend this article even further).

P#90118 2021-04-09 15:46 ( Edited 2021-04-13 11:00)

PICO Space

Cart #drakeblue_picospace-1 | 2021-10-16 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
26

v1.1
Defend the animals of the galaxy from the space weevil invasion in your grandad's space taxi!

Check the notice boards at space stations for prospective passengers. Gain rewards when they're delivered safely and discover how to defeat the weevil menace.

The galaxy is quickly becoming a more and more dangerous place so be careful out there - with 50-70 systems and some questionable government policies an animal needs to look after themselves!

Please report any bugs or if something is unclear below. Feedback helps me improve and is always welcome :)

Controls

Main Menu

  • Use Up, Down and X to select options in the menus.
  • Return/Start/P will always take you to the Main Menu
  • Hold Return/Start/P to access the normal PICO-8 menu.

I recommend a gamepad if you have one, but keys work fine too. Holding the X and O buttons together is useful to use the ship flip ability successfully. On mobile, I've found swiping my thumb backwards and forwards between the X and O buttons works okay, but it does seem easier to play with keys or a gamepad (to me at least).

Starting the Game

Use the up and down directions to highlight an option and X to select it.

  • Begin New Game - starts a new game with the currently show captain in a new galaxy (Captain Magerquack by default).
  • Change Captain - chooses a new captain and galaxy with which to start a new game using the Begin New Game option. This doesn't affect the currently stored game or any game from a pasted code.
  • Continue Last Game - this retrieves the previously auto-saved game from your browser's cache or PICO-8's cartdata. The game is saved at the Dock Menu and on entering a system via a hyperjump.
    If you want to preserve further game states beyond this then use the next option.
  • Load From Pasted Code - every time the game is saved PICO Space places a save game code into the clipboard. By pasting this code into a text file or similar then multiple game saves can be maintained. To restore from one of these codes:
    > - copy the code into the clipboard from the application you've used to store it
    > - switch back to PICO Space running in PICO-8 and paste it
    > - Use X to select the Load From Pasted Code option in the menu

NOTE: THIS DOESN'T SEEM TO WORK IN THE BROWSER - YOU WILL NEED TO DOWNLOAD THE STANDALONE PICO-8 CONSOLE VERSION TO LOAD FROM SAVE CODES OR ACCESS THE GAME THROUGH SPLORE
THE GAME WILL BEHAVE UNPREDICTABLY (PROBABLY CRASH) IF A MALFORMED CODE OR OTHER DATA IS PASTED INTO IT

  • Set Difficulty - Adjusts how dangerous the galaxy is in the game. This option affects new games only so needs to be set before choosing "Begin New Game".
    > - Easy - for casual or inexperienced players, but still with some challenge
    > - Normal - a reasonable challenge, success is not guaranteed on each journey
    > - Hard - a difficult game likely to require multiple reloads

Please tell me if you think these ratings aren't very correct so I can try to tweak them

How to Get Around the Galaxy

(the following is an extract from the dog-eared manual you found in the glove compartment of your grandad's ship)

Congratulations on your purchase of a Farwilre P1C0 Star Taxi!

Your ship will enable you to fly anywhere within a system's boundary by directing it with the arrow keys or control stick (if fitted). Be careful of flying into a system's sun as there may be undesirable explosive consequences.

  • The Short Range Scanner is useful to see what's close to you and is located in the bottom right of the viewscreen.
  • The current system or current target (with distance in standard galactic units) is shown in the top left.
  • Your ship's remaining shield energy is displayed in the bottom left. If this is depleted then it will flash to indicate that you should dock at the nearest space station for recharging.
  • To locate space stations or other objects within a star system press Return, Start or P to bring up the main menu.

Main Menu (Pause)

Select the System Map (with Up, Down and X) from the options shown.

System Map

Solar systems are large so to avoid excessive journey time it is advised to utilise your ship's hyperdrive from the System Map (where traffic laws permit).

To do this:

  • Locate the cursor where you wish to jump to by using the direction keys
  • Press O, Z or V to execute a hyper jump
  • Press X to target the nearest system object. The target is shown in the top left and also indicated by a red marker.

Note: be sure to peruse the System Map in a safe location to avoid possible accidents while it has your attention. Last year 45% of ship accidents happened while pilots were viewing their map screens - don't let the next one happen to you!

Systems in PICO Space are linked by Hyperspace Wyrmholes (of as yet unknown origin). These can be used to traverse between stars even in ships that are unequipped with an interstellar-capable drive. Thus to travel to another system simply fly into the centre of a wyrmhole and your ship will travel through hyperspace to a point in the system indicated in the wyrmhole's name.

If you know which adjacent star you wish to travel to, it is easiest to find where the wyrmhole to it is by using the System Map.

To navigate about the greater galaxy and hence determine which wyrmholes to use it is advisable to examine the ship's Galaxy Scanner.

Galaxy Scanner

The Galaxy Scanner will open centred on your current location - shown by the ship icon and expanding circle indicator. Adjacent stars (i.e. those which are connected by a pair of wyrmholes) have lines drawn between them. Systems adjacent to your current location have green lines drawn to them. Corresponding wyrmholes should be visible on the System Map.

The view and cursor in the centre may be moved using the direction keys. The currently highlighted system will be the closest to the cursor position.

The galaxy is quite large so for optimal viewing it may be necessary to zoom the view in or out by holding the X key and using the Up and Down directions to change the scale of the view.

If your ship is fitted with an Interstellar Drive then it is possible to hyperjump (without traversing a wyrmhole) to adjacent systems by highlighting them with the cursor in this view and pressing O, Z or C.

Status

At any time your current status can be checked from the Main Menu by selecting this option.

The screen shows:

  • You, the captain of the ship and your combat rating
  • Any current message
  • Your current passenger if one is present and to where they wish to be taken.
  • Information about any upgrades that have been added to your ship
  • The energy level remaining for you ship's shields
  • The danger level of this galaxy

Weaponry

Should you be unfortunate enough to encounter a hostile threat on your travels your P1C0 Space Taxi is fitted with a standard P3W Blaser; a small but adequate defensive weapon.

  • Tap X (and release) and the P3W will fire
  • Hold X to charge multiple shots that will rapidly fire on releasing the trigger.
  • Note: while energy is diverted to charging the P3W your shields are unable to recover strength.

Dock Menu

When docked at a space station the following standard options will be available:

Notice Board

Waiting passengers are listed here for an enterprising space taxi owner to transport to their desired destination.

Use the Up, Down and X keys to select a passenger that you wish to transport. Be careful to choose a journey you can complete - an animal never backs out of a deal!

New Paint

Choose from a range of desirable colour schemes to customise your Farwilre pride and joy!

Launch to Space

The station will launch you back to space so you may continue on your journeying.

extract ends

Tips

  • Choose your passengers wisely - some wish to travel further than others. Check on where they want to go via the Galaxy Map before committing to a journey you aren't sure you want to take.
  • Plan your journey before leaving the safety of the space stations.
  • If your shields are damaged it may be wise to stop at a station on the way to your destination to recharge them.
  • Charge your weapon before encountering enemies, but don't hold X constantly as it stops your shields recharging as you fly.
  • Use your hyperspace ability within systems to save on journey time or to escape a tough spot, especially as the galaxy becomes more and more dangerous.
  • Note that the game is only paused in the Main Menu, other screens don't freeze time.

If you are not sure what to do:

  • try ferrying passengers
  • later in the game, try reading the Beeb News or listening to what the NPC ships say to you.

Good Luck, Captain!

V1.1

  • UI colour changes

  • fade effect improved

  • screen shake added

  • difficulty reduced (a bit); enemies are slower, shots do less damage
P#89915 2021-04-03 16:56 ( Edited 2021-10-16 15:40)

Fading Stars Demo

Cart #fading_stars-6 | 2021-04-28 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
9

Demo showing the combination of the stars and fade effect I used in PICO Space: https://www.lexaloffle.com/bbs/?tid=42279.

[UPDATE 2021-4-12: 8bit and 16bit cached modes (explanation below and in code), some other small tweaks]
[UPDATE 2021-5-6: added interleaved 8bit mode]

Stars

The stars are just simple particles that have x,y,z coordinates.

In this demo I use a couple of sin functions to give them some movement combined with a divide by the z coord for a bit of parallax. In game, I feed in the player's position.

Then I clamp the resulting x,y values to the screen with the modulus operator so they're always visible (%128). It does mean that the same stars go past constantly, but otherwise I was processing a lot of particles that don't get seen very often (not aiming for realism here).

Fade Effect

This works by mapping the colour of every pixel on the screen to another colour that tends to a target e.g. 0/black.
You can use a similar mapping with the pal(x,1) function to e.g. do fades to black between screens etc. but that fades everything including anything drawn that frame.

In this demo I process the pixels already in screen memory so that the screen is faded by a step, then draw fresh stars on top of that.

It's pretty expensive to do the whole screen (IIRC about 90% of performance at 60fps) so I've set it up to do every fourth scan line, starting from a different point each frame. Effectively a quarter of the screen is faded at a time. It takes 4 frames to fade the whole screen one step.

I initially tried fading in quarter strips top to bottom, but the tearing on bigger objects like planets looked pretty bad.

Using the order 0,2,1,3 for the scanlines does some rough dithering to make the effect look a bit more uniform. A random value flr(rnd(4)) works quite well too, but is messier looking.

Since I found using poke4 to work on 8 pixels at a time was fastest (not surprising really) dithering horizontally is limited and isn't in the demo. Nevertheless, I keep meaning to try a "Z" pattern i.e.

 0000000011111111
 2222222233333333

I'm concerned it might cost too much more in performance/tokens for too little visual improvement.

[edit]
Of course, as soon as I write about it the old subconscious starts working away and it takes 5 minutes to implement just that - a reverse N pattern as it turns out. Same performance, same tokens. See the new cart.

Pros of Effect

  • You can draw whatever you want really and the effect essentially "just works" as a replacement for a cls().
  • Cross-fading out from a scene just happens "free".
  • very simple particles look much more complicated than they really are

Cons

  • You can't draw anything that moves without the fade effect "catching" it. It can be mitigated by drawing around your objects (e.g. black borders), but if the view moves more than the width of the border you're out of luck.
  • Conversely, the effect only works where you don't draw that frame - so if your game has e.g. a full-screen scrolling background that's drawn every frame then you won't see any effect at all. For a space game this isn't a huge problem, but it's still visible here and there.
  • If nothing moves then there's no effect - try hacking the stars to be still in the demo.
  • Performance cost is approx 21% at 60fps.
  • Obv costs some tokens.

Caching

The effect works fine by extracting each pixel's colour value via shifting and masking then dumping the mapped values back onto the screen, but it's still pretty performance heavy.
When I was writing PICO Space I'd read a few times that procedurally generated content used a lot of memory so I didn't want to try anything like the following, but now I have a much better idea of the game's memory requirements I thought I'd give it a go.

8-bit Mapping

Pixels in PICO-8's screen are determined by a 4-bit value, but peeking and poking only works with 8-bit granularity at best i.e. a pair of pixels or more at once. The mappings I have contain 16 values for each possible colour of a pixel.

Considering pairs of pixels instead of single pixels, there are 16 * 16 = 256 possible combination of colours that need to be mapped. Why not store a table with each of these values - it can't be that large, right?

Turns out it isn't, especially when compared to the 2MB of space lua is given in PICO-8. In fact the demo seems to only use about 2K or so (which is still a lot more than the 256 bytes it should take, but still pretty small).

This means that a lot of masking and shifting isn't as necessary inside the inner loop. It even takes fewer tokens. The performance improvement is enough that half or even all of the screen being processed per frame isn't too bad.

16-bit Mapping

The next step was obviously to try mapping 4 pixels at a time using 16-bit values.

This would need a table of 16^4 = 65536 entries which isn't very big for a modern machine, but is pushing it pretty far for PICO-8. It's possible - take a look at the code. It also takes up a lot more memory: about 1200KB it seems. That's well over half of the total space available and for my purposes in PICO Space is enough to give me sporadic out of memory errors as it stands (PICO Space takes about 600-900KB depending on the size of the current galaxy and how much is going on in it at any particular moment). For other games it may be absolutely fine and it's tempting since there's about a 2x speed-up compared to my original implementation of the effect using this technique.

A Bit Too Far

PICO-8's number format is 16bit.16bit fixed point so every value I've been storing so far is actually 32 bits in size whether I use all of those bits or not. Why not use them all?

Storing mappings for 8 pixels isn't going to work: 16^8 = 4,294,967,296 - a bit too much for PICO-8.

Instead, the last implementation that I've tried (so far) stores two 16-bit values in each number in the cache table so that the same amount of mapping values as in the previous section takes half the entries and hence half the space. The upper 16 bits take the even values; lower 16 bits the odd values.

This brings the memory usage down to about 600KB or so, which is fairly reasonable.

Unfortunately, the two mapping values packed into a single PICO-8 table value need to be unpacked to be used in the inner loop of the effect. By the time shifts and masks are applied to do this I couldn't get the performance to really be any better than the original effect (without any caching of values), never mind faster than the other cached value versions.

Yet Another Way

Up until this point I'd only considered making the effect faster and not "better". Two horizontally adjacent pixels are represented by each byte in the screen so one of the first compromises I'd made was to assume I couldn't fade these separately per frame and so fading the whole screen over four frames was done with at least chunks of two horizontally adjacent pixels at a time.
Since the 8bit cache version uses so little memory, is faster and deals with all combinations of two pixels both fading on the same iteration it struck me that there wouldn't be much cost to keeping two caches of 8bit values, one with the left side pixel faded, one with the right and swapping which cache is used per frame. When combined with alternating which rows are processed, this allows a dither pattern that works on a block of 2x2 pixels - no more horizontal chunking:

01
23
P#86396 2021-01-12 15:57 ( Edited 2021-05-06 05:46)

Cart #demystify-3 | 2021-03-31 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
15

Demystifying the Christmas Tree

Help your kitten destroy as many decorations as you can before the tree is fully decorated or their energy runs out and they nap.

Try to destroy multiple decorations within a short time to get combo bonuses and improve your score. Use your energy wisely, young feline.

Play as Philly or Frankie on their own or with a friend against each other. See who can cause the most chaos!

Controls

Action      Frankie (Player 1)  Philly (Player 2)
jump        up                  E
run         left/right          S, F
descend     down                D
poke        O(Z)                W
swipe/hit   X                   Q

Return      menu, including music on/off and restart.

(These are the standard PICO-8 controls so some other keys work too)

Background

Demystifying the Christmas Tree recreates a real event from a Christmas past, although in real life the kittens not only broke decorations, but also took out parts of the tree itself. They also didn't run out of energy (as anyone who has had kittens would know).

If you like this game, find a problem or have any suggestions then please comment below.

Please consider trying my other games or following me for news on new projects.

Happy Holidays :)

1.0.1 update: after testing with the owner of the kittens (my girlfriend's mother aka "mother-outlaw") I've added an option to change the speed that the arms decorate the tree.

1.0.2 update: added fade effect to intro. Because I felt like it.

1.0.3 update: added combo indicator after user feeback.

P#85468 2020-12-16 13:32 ( Edited 2021-03-31 12:19)

Silly Snow

Cart #db_silly_snow-3 | 2020-12-02 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
2

(update: added 2 new methods that incorporate gusts of wind)

Controls

UP/DOWN chooses different behaviour for the snow flakes
X changes the colour of the falling snow so you can see the "trick" a bit easier
O/Z toggles drawing the non-snow bits of the graphics each frame on and off

What it is

--

TL;DR
Only track falling snow, add lying snow to the background.

--

As it's that time of year again, I thought I'd write some winter-themed stuff.

This is my take on ye olde faithful falling snow demo, which I'm using for the intro screen for another cart (on its way soon). I've cleaned it up a bit, added way more comments than I normally do and added some options to show what's happening a bit and show some variations. Click "Code" below the display above if you're interested.

I first typed in a version of this demo from a magazine back in the mid 80s on an Atari 800XL. In the 35ish years since then I'm not sure I've ever implemented it again though.

The gist of how it works:
Falling snow is represented by a collection of simple particles that are represented by a position (x,y). Each frame that position is updated (mostly downwards i.e. y+=some_dist).
If the snow flake encounters an obstacle (i.e. a pixel that isn't empty or the bottom of the screen) then the snow flake has landed.

Tracking snow flakes after they've landed isn't practical (even with PICO-8 and it certainly wasn't with an 800XL) and is also a bit pointless since once a snow flake has landed, it aint going anywhere. Well, at least not in this demo. Instead, the particle is destroyed and the pixel where it landed is set to white and becomes part of the background (in this demo that means it has a colour greater than 1).
This is an incredibly basic version of how a lot of "physics" engines work: updating only moving or otherwise interesting parts of the simulation and "parking" the rest. Usually they'd check the stationary particles in some way for whether they need to be activated. If you wanted to simulate collapsing drifts etc. there's nothing really to stop you keeping a record of where the snow has fallen or scanning for white pixels and perhaps checking to see if a condition to collapse has been met. Then you could find all the snow pixels affected by the collapsing pixel and "wake" them up. You would probably need to check at a slower than per frame rate for performance reasons.

As with most sims the fun is in playing about with it, hence the 9 method variations in the cart e.g.

  • the exact behaviour of the flakes in the air
  • whether they land or slide to the side on encountering the background can be tweaked.
  • wind gusts...

Blustery Blizzards

Methods 8 and 9 add a wind factor to the updated position for each flake generated by a value derived from complex and deep trigonometric knowledge (aka trial and error). The nice thing about this is it's only calculated once per frame and only added per snow flake so it's very cheap. Adding sin values per flake based on the flake's position gives some odd results.

Limitations

  • for there to be stationary, landed snow relies on not clearing the screen. If you clear the screen then you're pretty snow drifts all disappear. You can still have snow flakes falling, but they will disappear a frame after they land...

  • The methods in this cart are fairly non-destructive of the background, but it's fairly easy to make a version that "eats" into what's already on the screen (acid snow!). I've included an option to re-draw the background each frame. As long as your background doesn't move then it works fine (remember, no screen clear).

  • "tunneling" through thin parts of the background varies in frequency depending on your method. Generally, the more random the snow flake movement, the more problems you'll have.

On the Atari ST there was a game based around this that I got from a magazine cover disk called "Downfall". I could only find one video of it and the uploader doesn't seem to really understand how to play it sadly.
You were supposed to compete against a friend to draw lines to funnel snow from your side out of a hole at the bottom that would then be sent to their side (and vice versa). You could also funnel snow to the sides to give you energy for "special powers" that let you e.g. draw lines on your opponent's side.

I expect that explanation's as clear as a blizzard; hopefully the code isn't as bad.

Happy holidays :)

P#84926 2020-12-02 12:47 ( Edited 2020-12-08 17:56)

The Pico Mermaid

Cart #thepicomermaid-0 | 2020-11-09 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
4

Use (X) to control the Pico Mermaid as she fetches pearls from the bottom of the sea back to the surface. Avoid the piranhas that will swim faster and faster as the mermaid retrieves more pearls.

This is my entry into Tweet Tweet Jam 5 and so the code fits into 560 characters (two tweets).

Features:

  • Single-button controls (X)
  • Animated and multi-colour pixel art sprites*
  • air and water physics*
  • Two particle systems*
  • Difficulty ramp*
  • Score effect*
  • Death effect*
  • start animation*
  • current score and high score display*
  • in-game instructions*

(* kinda)

Here's the code:

P=pset::A::x=64y=0v=-9t=0w=127e=0d=.6s=0for z=0,29do
poke(z\6*x+z%6+1,'0x'..sub('e0e800800880ce0800444404e008002444043b0000444480b000004088',1+z*2,2+z*2))end::B::P(69,e<1and w or y,7)H=max(s,H)e=y>119and 1or e
?'tAP❎ gET●:'..s..' hI:'..H
v+=.5C=cls
if(y>8)v-=max(btnp(❎)and 2or.4,v-.6)
flip()C(1)rectfill(0,0,w,9,12)t=(t+d)%w
y=min(120,y+v)spr(0,x,y,1,1,t&8<1,e<1)for i=3,14do
d=-d
t=-t
h=40*i-t-4&w
pal(4,i)k=i*8k+=7*sin(h/w)if((h+4)\8==8and 4>abs(k-y-2))C(8)goto A
spr(1,h,k,1,1,d<0)P(rnd(8)+x,h*d%y+3)P(k*d/.7,h\d)end
if(y<9and e>0)e=0s+=1d*=-1.2C(7)
goto B
P#84034 2020-11-09 11:37 ( Edited 2020-11-09 11:40)

P8C-BUN is now uploaded to itch.io here: https://drake-blue.itch.io/p8c-bun

I've also been working on something else that I really want to share a preview of soon, but I need to clean it up just a little more first.

P#83617 2020-11-02 14:36

P8C-BUN

Cart #drakeblue_p8cbun-2 | 2020-12-26 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
6

Help your chosen bunny cover each level with poo then escape down a rabbit hole. Don't get caught by the fox or anything else that's out to get you!

Finish all 16 levels without restarting to achieve "Iron Bun" or just try to post a high score. Start at whatever level you like.

Controls

  • Use the d-pad/arrow keys to direct your bunny.
  • z/c/(O) to show where you haven't pooed yet.
  • x to paws and return to the title screen.
  • You can toggle the music or return to the title screen from the menu as well.

Tips

  • The buns will keep running in the direction you choose until there's no clear path in front of them so there's no need to hold down the arrow keys.
  • If you choose a new direction before a junction or corner they'll remember that and turn immediately so corner early for maximum speed.
  • The bananas will give you a speed boost, but leave skins behind that you (or the fox) may slip on.
  • Macaroon is too tough to be caught by the fox or anything else so if you just want to play through the levels (or practice) choose to play as her. Real-life Macaroon has seen off cats and kills blankets on a regular basis.
  • The fox gets faster and the red kite will fly over as you cover more levels so choose which level you start from wisely if you're aiming for Iron Bun.
  • Manipulate the fox, especially using the rabbit holes, to make your life easier.

1.1 Update: text legibility and efficiency improvements
1.1.1: restored kite spawn to be after a few levels and not right from the beginning.

This is my first PICO-8 game (in fact, my first full game for anything) so any feedback is welcome. More info about P8C-BUN is here: DrakeBlue.com.
This game was inspired by our real-life pet rabbits (especially Oreo, who does tend to poo everywhere) and exists thanks to the patience of my gf (who runs the rabbit-oriented website mentioned on the title screen RabbitRetail.co.uk and uses it to help donate to rabbit charities and rescues as well as feed ourselves and our own bunnies).

Purchase to download and support more development and bunnies here: https://rabbitretail.co.uk/products/p8c-bun-game

Or here: Drake Blue on Itch.io

P#83276 2020-10-24 11:46 ( Edited 2020-12-26 10:43)

Follow Lexaloffle:        
Generated 2021-11-28 21:08:03 | 0.152s | Q:62