**TOP★BURN 1k**
A PICO-8 Jam Game
in 1024 Characters by [@CasualEffects](https://casual-effects.com)
[Click here to play!](topburn.html)
# PICO-1K Jam
My goal for this jam game was to make an entire (3D!) game in 1024 characters of source code,
as reported inside the [PICO-8](http://pico8.com) editor. The final code hits exactly that
size, including the comment at the top and the newlines.
It includes flying, enemies, shooting, guns overheating, depleting fuel, a score tracker,
explosions, and a game over state. The player's jet is based on sprites from the original
_Afterburner_ game, redrawn to fit PICO-8 palette and sprite limitations. The 3D ground was
from an earlier Tweet jam entry that I made.
The complete [source code](topburn.p8) is:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--top★burn
a=48n=32k=64_={}camera(-k,-k)c=16x=0y=0t=0o=0g=0q,b,r,s,h=sqrt,btn,rnd,spr,sspr::a::l=t%8/4map(0,0,-k,-k,c,c)for j=3,k do
z=j/4h(q(q(z))*c-l,1,k+k,1,-j*n,j,k*4*z,1)end l=l<1o=max(o-1,0)f=b(4)and o<n if f then m(r(3))o+=2 if(o>n)o=98
end i=l and o>n and s(124,-k,-40,4,1)or h(0,56,min(o,n),6,-k,-a),l and t>650or h(a,n,n-t/n,8,n,-a) i=b(1)p=24m=sfx
if(i and x<c)x+=2 p=c
if(b(0)and-k<x)x-=2 p=c
if(b(2)and y<n)y+=1 p=k
if(b(3)and-n<y)y-=1 p=74
s(86,x+8,a,4,2) for e in all(_)do e.x+=e.d e.z+=.1 j=e.d<0w=88z=4/e.z
if(e.l==0or e.z>k)del(_,e) if(e.l)e.l-=1 j=r(2)>1goto e
h(56,k,c,c,e.x*z+24*z,a*z,c*z,c*z,j)w=k if(f and max(abs(u/z-e.x-21),abs(v/z-e.y-8))<8)e.l=8m(3)g+=.75
::e::h(0,w,56,24,e.x*z,e.y*z,56*z,24*z,j)end if(r(98)<1)d=sgn(r(2)-1)add(_,{x=r(n)-d*k-c,d=d,y=r(k)-a,z=1})
z=.7u=x*z+13v=y*z-8s(2,u,v) if(f and l)s(c,u*.9,v*z)s(17,u,y*z-5)z+=.1 s(38,x*z+10,y*z-2,2,1)z+=.1 s(46,x*z+14,y*z+1,2,1)
s(p,x,y,6,3,i)s(240,-30,-41,g,1)flip()t+=1
if(t<999)goto a
::g::s(137+t%2*k,t%192-140,-n,7,1)flip()t+=1 goto g
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Code with Comments
The [commented version](topburn-comments.p8) makes it a little easier to see what is going
on. The code is actually reasonably organized and many of the variable names (although a single
letter) are at least mneumonics:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- top*burn
-- @casualeffects
-- a pico-8 game in 1024
-- characters (this is the
-- version with comments, which
-- is larger!)
--
-- common constants:
-- k = 64
-- a = 48
-- n = 32
-- c = 16
--
-- common functions:
-- q = sqrt
-- b = btn
-- s = spr
-- h = sspr
-- r = rnd
-- m = sfx
--
-- t = timer (frame count)/fuel
-- x,y = plane position
-- z = scale factor
-- _ = enemy array
-- p = plane sprite index
-- i = right button (during plane code)
-- j = flip sprite left-right
-- o = gun overheat status
-- f = true if firing
-- u,v = reticle coords
-- e = enemy
-- w = enemy sprite y
-- l = modulo counter (for background) and blink (for hud and bullets)
-- g = kills
-- d = temp var
--
-- enemies:
-- x,y,z = coords
-- l = life
--background
a=48n=32k=64_={}camera(-k,-k)c=16x=0y=0t=0o=0g=0q,b,r,s,h=sqrt,btn,rnd,spr,sspr::a::l=t%8/4map(0,0,-k,-k,c,c)for j=3,k do
z=j/4h(q(q(z))*c-l,1,k+k,1,-j*n,j,k*4*z,1)end
-- hud
l=l<1o=max(o-1,0)f=b(4)and o<n
if f then m(r(3))o+=2
if(o>n)o=98
end
-- blinking gauges
p=l and o>n and s(124,-k,-40,4,1)or h(0,56,min(o,n),6,-k,-a),l and t>650or h(a,n,n-t/n,8,n,-a)
-- controls
i=b(1)p=24m=sfx
if(i and x<c)x+=2 p=c
if(b(0)and-k<x)x-=2 p=c
if(b(2)and y<n)y+=1 p=k
if(b(3)and-n<y)y-=1 p=74
-- plane shadow
s(86,x+8,a,4,2)
-- enemy plane logic
for e in all(_)do e.x+=e.d e.z+=.1 j=e.d<0w=88z=4/e.z
if(e.l==0or e.z>k)del(_,e)
if(e.l)e.l-=1 j=r(2)>1goto e
-- hit detection and explosion animation
h(56,k,c,c,e.x*z+24*z,a*z,c*z,c*z,j)w=k
if(f and max(abs(u/z-e.x-21),abs(v/z-e.y-8))<8)e.l=8m(3)g+=.75
::e::h(0,w,56,24,e.x*z,e.y*z,56*z,24*z,j)end
-- new enemy spawn
if(r(98)<1)d=sgn(r(2)-1)add(_,{x=r(n)-d*k-c,d=d,y=r(k)-a,z=1})
-- player reticle + bullets
z=.7u=x*z+13v=y*z-8s(2,u,v)
if(f and l)s(c,u*.9,v*z)s(17,u,y*z-5)z+=.1 s(38,x*z+10,y*z-2,2,1)z+=.1 s(46,x*z+14,y*z+1,2,1)
-- player plane + kills
s(p,x,y,6,3,i)s(240,-30,-41,g,1)flip()t+=1
if(t<999)goto a
-- game over state
::g::s(137+t%2*k,t%192-140,-n,7,1)flip()t+=1
goto g
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
I wrote it all like this, in that form. It isn't compiled or converted from a more readable
version. In order to squeeze it as much as possible, I had to work with the final version
directly.
# Motivation
I made this because it was a fun challenge and the next natural point after Tweet Jam (140
characters). As a professor, it was also a good educational example for my students of how
parsers work, old-school arcade game design tricks for managing complexity in tight RAM
budgets, and to create interest among them for PICO-8.
I participate in little game jams on weekends with my children and
friends. Optimizing code to minimize the number of characters is obviously not a good idea or
useful skill in general programming.
However, there _are_ a few reasons this kind of activity is actually productive and
educational:
1. My brain works the same when I'm working on any kind of problem. Playing a board game,
optimizing a AAA game shader for performance on PS4, working on scientific research, or
doing a silly activity like this--I find that they all keep me sharp and help expand
the set of patterns I can intuitively match. Maybe you've had a similar experience.
2. Reverse-engineering classic game mechanics and graphics reveals a lot about how they were
designed. That increases my enjoyment and appreciation of them.
3. Minimizing code size mostly minimizes complexity as well. I was surprised how much more
elegantly some game mechanics could be implemented when pressed, vs. the more brute-force
approach I would normally have taken. This now encourages me to seek more elegant solutions
in the future for normal game code...simplicity is always important because simple, concise
code is simpler to modify and debug.
4. Every now and then, 2.5D and sprite-based tricks crop up in modern 3D graphics. So, not
everything in here is fantastical. (I even used Quake I-style imperfect perspective
correction this very week in a high-end shader to amortize cost.)
5. Minimizing object code/microcode/circuit area can actually have a significant performance
impact when working at those low levels. Sometimes we create "slow" small code that is
actually fast because it requires fewer registers, enables instruction pairing, requires
less power, fits in the instruction cache, or enables more computation per die area than
something bigger which seems faster in a scalar context.
But really, this was just a fun exercise!
# Techniques
Some techniques that I used:
- Functions are first-class objects in Lua, so commonly used ones can be bound to single-letter
variables to save characters
- Because of the way it is implemented, the PICO-8 parser requires a space between a number and
'abcdefx' because they can appear in hex numbers. However, it does not require a space before
other letters, so statements beginning with other letters can be directly jammed against
assignments that terminate in a number
- Single-line `if` statements run to the next newline and can have any number of
space-separated statements in the consequent
- Some common [tweetjam tricks](https://gist.github.com/kometbomb/7ab11b8383d3ac94cbfe1be5fb859785)
- The sprite sheet and even sound effects are designed to allow different animations and
sequences to be computed as patterns, as well as using fewer digits (e.g., sprite 9 is
cheaper to access than sprite 10 in character cost)
- I used dark blue instead of opaque black in many places to avoid the character cost of
`palt(0, false)`
- Reduced the number of unique multi-digit constants, so that a few single-letter variables
could replace as many as possible
- Directly managed the event and rendering loop to avoid function call overhead
- Used sprite stretching for the 3D effects on both the planes and ground
- Drew repeated sprites directly into the sheet to avoid a `for` loop on the stars
- Used non-integer arguments to `spr` for the sprite size, which allows fractional sprites
- use `1<0` for `false` and `1>0` for `true` (this was optimized out by the end)
- use `a and b or c` for `if a then b else c end`