Skip to content

Instantly share code, notes, and snippets.

@kometbomb
Last active June 27, 2023 13:00
Show Gist options
  • Star 45 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save kometbomb/7ab11b8383d3ac94cbfe1be5fb859785 to your computer and use it in GitHub Desktop.
Save kometbomb/7ab11b8383d3ac94cbfe1be5fb859785 to your computer and use it in GitHub Desktop.
PICO-8 tweetjam stuff

PICO-8 size optimization stuff for tweetcarts

Here are some simple ways to make your PICO-8 code fit in 140 280 characters (as in the #tweetjam #tweetcart craze). I did not invent these, I merely observed and collected them from the tweetjam thread.

LUA syntax stuff

  • Use single character variable names
  • Use x=.1 and x=.023, not x=0.1 or x=0.023
  • x=1/3 is shorter than x=.3333
  • You don't need to separate everything with spaces or write them on their own lines, e.g. circ(x,y,1)pset(z,q,7) works just as well
  • x=sin(t)*10+32 y=cos(t)*23+10 can be reordered and written as x=32+10*sin(t)y=10+23*cos(t) to save a few characters
  • Don't use hexadecimals, 0xffff is one character longer than 65535
  • An example encountered in the wild: sin(y/x)-(cos(x/y)*2+5) can be written as sin(y/x)-cos(x/y)*2-5
  • a="01234567" v="0x"..sub(a,i,i) might be shorter than a={0,1,2,3,4,5,6,7} v=a[i] with long enough tables (useful for palettes)
  • ({1,2,3})[index] is shorter than t={1,2,3}t[index]

PICO-8 stuff

  • PICO-8 does weird preprocessor stuff not available on vanilla LUA: e.g. x+=1 is shorter than x=x+1 - but often you need to have a newline after that etc.
  • ?0,x,y,color (it's the same as print(0,x,y,color)) is perhaps the shortest way to draw more complex things on screen than pixels and circles (explore the character set)
  • Don't bother with the _update()/_draw() structure, just use a goto, e.g. ::s:: print(rnd()) goto s
  • You don't often even need flip() - the screen will automatically keep updating at some point (but you can't control it then, obv)
  • The default cart has one built in sprite you can use with spr()
  • Remember the mirrored screen modes for symmetrical stuff
  • Starting from 0.1.11, you can use t() instead of time()
  • x\1 is shorter than flr(x) (the \ operator divides by a number and rounds down)
  • "any arguments mid doesn't get are treated as 0, so mid(0,x,127) and mid(x,127) do the same thing" - @WinslowJosiah

Example how to move left and right in minimal chars

This is too long:

if(btn(0))x-=1
if(btn(1))x+=1

This is shorter:

b=btn()
if(b>0)x+=b*2-1

Get the angle 0..1 from button input

Either one of the following will extract the angle suitable for sin()/cos() from the cursor keys (amazing snippet stolen from @pancelor):

a=btn()*.6&.75
a=btn()*12\5/4

General size-optimization tricks

  • If you use e.g. a number like 1000 in various places, try if you can define it as a variable and use that variable instead of the longer number
  • 99 is almost as good as 100 but it's shorter. x/99 is almost equal to x/100
  • If you already have e.g. x=10 and need e.g. to divide y by 10000, use x: y/=x^4
  • Forget perfection. If you have an effect that uses 64 in one place and 60 in the other, try to use 60 in both. Or meet in the middle with 62. You might be able to save a few characters with the above variable substitution trick
  • You should also try to substitute function names like circfill with shorter names e.g. c=circfill c(x,y,1) c(50,50,10) if you use them a few times in the code (no point substituting e.g. cos() if you only use it twice)
  • Forget about the usual FOR X=0,127 DO FOR Y=0,127 DO ... END END and use x=rnd(128) y=rnd(128), it will eventually cover all screen pixels when you do it again and again (this is called the Monte Carlo algorithm), this gives a distinct not-quite-random look for effects and is good for things that would never work fast enough if you tried to do it for every pixel every frame
  • You can reset the random seed each frame with srand(0) to get the same series of random numbers for e.g. a star field effect, you might want to experiment with the seed number to get the best selection of random numbers (maybe there is a nice melody somewhere in the randomness?)

An example

Here is a simple fire routine:

t={0,1,9,2,7,2,10,4,8,9}
function _update()
 for i=1,5000 do
  x=rnd(128)
  y=rnd(128)
  c=pget(x,y+rnd(2))
  if rnd(50)<c then c=t[c] end
  circ(x,y,1,c)
  pset(y,127,7)
 end
end

-- 178 chars

The routine executes every frame, picks 5000 pixels randomly and draws a circle (with radius 1) at x,y based on the pixel color at x,y+1. There is a color map t that is used to nicely fade out the colors (the table contains the next color for a color - i.e. 7 = white, t[7]=10 => yellow, t[10]=9 => orange, t[9]=8 => red etc.). pset() is used to draw a white line at the very bottom of the screen, which seeds the fire. It is is based on the random "Monte Carlo" way of doing things so we don't need a for-loop to iterate the whole screen.

First things first: ditch the _update() callback and simply make the routine draw as fast as possible with an infinite loop. The original routine does this 5000 times a frame but we can drop the for-loop, we just want to push as many pixels as fast as we can.

t={0,1,9,2,7,2,10,4,8,9}
::s::
 x=rnd(128)
 y=rnd(128)
 c=pget(x,y+rnd(2))
 if rnd(50)<c then c=t[c] end
 circ(x,y,1,c)
 pset(y,127,7)
goto s

-- 141 chars

Let's substitute the repeated 128's AND rnd()'s.

r=rnd
z=128
t={0,1,9,2,7,2,10,4,8,9}
::s::
 x=r(z)
 y=r(z)
 c=pget(x,y+r(2))
 if r(50)<c then c=t[c] end
 circ(x,y,1,c)
 pset(y,127,7)
goto s

-- 141 chars

We didn't gain any characters at the previous iteration! Don't worry. This is because of the unneeded whitespace the definitions for r=rnd() and z=128 have, the newlines. We will take care of that whitespace later so this is still a useful step.

We have a 127 in the pset() call, why not simply use 127 in place of the 128's earlier in the code? Also, we can use the shorter PICO-8 if-then structure.

r=rnd
z=127
t={0,1,9,2,7,2,10,4,8,9}
::s::
 x=r(z)
 y=r(z)
 c=pget(x,y+r(2))
 if(r(50)<c)c=t[c]
 circ(x,y,1,c)
 pset(y,z,7)
goto s

-- 130 chars

Let's get rid of the whitespace (you can't put that shortened if-clause right after the pget(), it will be a syntax error, the preprocessor works like that).

r=rnd z=127 t={0,1,9,2,7,2,10,4,8,9}::s::x=r(z)y=r(z)c=pget(x,y+r(2))
if(r(50)<c)c=t[c]
circ(x,y,1,c)pset(y,z,7)goto s

-- 118 chars

We can reorder some of the variables so we don't need those whitespaces We will also move the definition of z inside the main loop - it's a tiny bit slower, of course, and z will be nil the first time we go through the loop but it saves a whitespace and it still works.

t={0,1,9,2,7,2,10,4,8,9}r=rnd::s::x=r(z)y=r(z)c=pget(x,y+r(2))z=127
if(r(50)<c)c=t[c]
circ(x,y,1,c)pset(y,z,7)goto s

-- 116 chars

And that's the shortest I can make it. It is short enough to tweet with a gif of it in action. :)

@jeremyredhead
Copy link

Actually, I noticed that z=12x=45 usually works; specifically, it'll parse fine unless 'x' (letter following numeric) is one of abcdef, as that'll give "malformed number" something or other error (confuses it for hexadecimal i think). I actually tested this in PICO-8 and lua's online demo.

Also, the use for this tip is probably less likely than the string-sub-table one, but: if you're calling a bunch of different functions with similar args, you could do something like

function r(f,v)
  f(--[[common args]], v)
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment