Or, let’s write kilosized games!

Just a few days ago I was reading about somebody writing games whose source code could fit on a business card. “Cool”, I though, “I can do that too!”. Well, maybe. I’m not a videogame programmer and I have little artistic abilities, so I set a somewhat easier target: write a small game whose source code fits in a 80x25 terminal screen (2000 charactes).

Since I wanted to output some graphics and not only characters on a console, I opted to use javscript running in a browser.

The bad news is that I couldn’t reach the target size, the good news is that I got close, 2046 bytes, which is still less than 2 KB. You can see the whole source code below here:

<body><div><score></score></div><script>var e=setTimeout,r=setInterval,a="getBou
ndingClientRect",t="getElementsByTagName",n="background",o="position",v="style",
l="absolute",i="innerText",p="createElement",s="transform",x="removeChild",d=doc
ument,f=d[t]("HTML")[0],c=f[v],u="translate",m="borderRadius";c[n]="black",c.col
or="white",c.cursor="crosshair",c.overflow="hidden";var h=()=>{var f,c=!1,h=d[t]
("DIV")[0],$=h[v];$.textAlign="center",$.t="none",$.width=$.height="100%";var b,
g=h.offsetWidth,M=h.offsetHeight,y=1e4,T=[],k=[],w={},C=d[t]("SCORE")[0],E=0;C[i
]=E,C[v].fontSize="3em",C[v][o]=l,C[v].right="10px";for(var I=e=>{C[i]=E+=e},O=0
,R=1;R<7;++R)T[R-1]=R*(g/7);h.addEventListener("click",r=>{((r,a)=>{var t=g/2,v=
d[p]("defense");w[f]=v;var i=v.style;i[s]=u+`(${r}px, ${a}px)`,i[n]="yellow",i[o
]=l,i.zIndex=t,i.width=i.height="4px",i[n]="yellow",i[m]="2px",i.left=i.top="-2p
x",i.transition="2s",h.appendChild(v);var f=O++;e(()=>{i.width=i.height="100px",
i[n]="red",i[m]="50px",i.left=i.top="-50px"},100),e(()=>{h[x](v),delete w[f]},2e
3)})(r.clientX,r.clientY)});var B=e=>{for(var r of(h[x](e.o),e.target.o[i]="💥",e
.target.v=!0,b))if(!r.v)return;C[i]="Game Over: "+E,clearInterval(f),c=!0},j=(e,
r)=>e.left<r.right&&r.left<e.right&&e.top<r.bottom&&r.top<e.bottom,D=()=>{if(!c)
{var r=d[p]("bomb");r[i]="🚀";var a=b[Math.floor(6*Math.random())],t=Math.random(
)*g,n=a.l,x=M,f={o:r,target:a},m=`rotate(${3*Math.PI/4-Math.atan((n-t)/M)}rad)`,
$=r[v];$[s]=u+`X(${t-g/2}px) ${m}`,$.transitionDuration=y/1e3+"s",$[o]=l,$.color
="red",$.transitionTimingFunction="linear",h.appendChild(r),e(()=>{k.push(f),r[v
][s]=u+`(${n-g/2}px,${x}px) ${m}`},100),e(D,G)}},G=2e3;b=T.map(e=>{var r=d[p]("c
ity");return r[i]="🌃🌃🌃",r[v].left=e+"px",r[v].bottom=0,r[v][o]=l,h.appendChild(r
),{o:r,l:e}}),r(()=>{G*=.995,y*=.995;var e=Object.values(w).map(e=>e[a]()),r=b.m
ap(e=>e.o[a]());k=k.filter(t=>{var n=t.o[a]();for(var o of e)if(j(n,o))return I(
10),h[x](t.o),!1;for(var v of r)if(j(n,v))return B(t),!1;return!0})},100),f=r(()
=>I(1),1e3),D()};h();</script>

You can play with it here, or check the animation below: it’s responsive, mobile-ready and buzzword compliant!

Gameplay on Firefox

More seriously, the gameplay is quite simple: missiles are pouring down, and you have to intercept them before they destroy the city. I’m calling the game “Missile defense”, but yes, it’s not original and you can guess where it took inspiration from :-).

While the game itself is nothing exceptional, writing it was an interesting detour from my usualy work-related programming activity. Having size as a major constraint in the program design was interesting (usually it is not an issue, only business requirements are), and playing with minification was an adventure by itself.

Structure of the game.

If source code size would not be an issue, the structure of such a simple game would not probably be worth of discussion. Having to keep the size low instead means often having to decide between a perfect (but “bloated”) implementation of a certain algorithm and a quick and dirty that almost get it right but fails in the corner cases.

The first issues I met was graphics, even more so since I’m not a designer and I didn’t really want sto start a graphics program to paint pixmaps. In any case they take space so I decided not to used them. All the “graphics” that you see in the screen are in fact Unicode characters. Among other things this means that the game may look differently in different browsers, depending on the font used; for example on my computer Chrome will render them in a single color while firefox will show the beautiful multi-color characters that you can see in the Gif above.

Moving the “images” around takes time and code. For this reason missile animations are implemented with a two-step animation and an appropriate animation duration (the exact duration will decrease while playing to make the game more and more difficult). The explosions are made bigger with a simple transform and its duration. This also let us have a perfectly fluid gameplay, since it all managed by the browser.

What is left is a loop that creates every so often a new missile, some code for keeping track of the score and another loop checking collisions between missiles and targets/defensive explosions.

Trimming the game.

After the initial implementation the unminified code was about 7KB, so I decided to remove a number of feature that were interesting but not essential for the gameplay. These include: ability to start a new game (sorry, you will have to reload the page), simple bounding box algorithm for collition detection (so don’t be surprised if from time to time a missile will excape an explosion), disabling text selection, and avoiding from time to time a spurious scrollbar appearing for an istant. After doing this, the game went down to about 5700KB.

Playing with the minification

The final step was to use a minification tool to reduce the size at the bare minimum. I started with RegPack since it seems it is often used for Javascript competitions with limited size constraints. I was not completely satisfied with the result though, since I couldn’t get below 2.5KB, and anyway I don’t take advantage of many of its assumptions, like using the canvas or webgl for the output, so I went with a more traditional uglify-es, with a number of tweaks to “help” the minifier do its job.

The tweaks were mostly one of the following:

Using a local variable for globals used many times.

The minifier cannot rename global variables, so if their name is quite long it make sense to make a an extra local variable and reference that In this way we can replace

document.getElementById('x');
document.getElementById('y');
document.getElementById('z');

with something like

var getElementById = document.getElementById;
getElementById('x');
getElementById('y');
getElementById('z');

which hopefully will get renamed to something like

var x = document.getElementById;
x('x');
x('y');
x('z');

The more the more the function is called, the more we save bytes.

In a similar vein, object properties cannot be renamed, but can be accessed through a variable whose name we can decide. For example

element1.style.background = x;
element2.style.background = y;
element3.style.background = z;

can be changed to

var background = 'background';
var style = 'style';
element1[style] [background] = x;
element2[style] [background] = y;
element3[style] [background] = z;

which the minifier hopefully can shorten to as much as

var a = 'background';
element1[a][b] = x;
element2[a][b] = y;
element3[a][b] = z;

Again, the more an attribute is used, the more we can take advantage of this trick.

Those two trick were more than enough to reach a size below 2048 bytes. I think that an even smaller size could be achieved, but I didn’t want to pass my time tweaking a minifier, so I called it a day. I will try to take it further next time I will try something similar.

The unminified source can be found on here, but I suggest to check it only after reading the whole article, since, as I will explain below, is not idiomatic javascript and will probably make little sense without some explaination.

Game issues

The game as some playability issues. Most of them are the result of trimming the code (for example text selection is not disabled, so you can select it by mistake while playing), others because I did not tune much parameters, expecially the collision detection loop that seem to run not often enough. I guess with some fiddling it can be improved but since it was my first try I’m satisfied with the result. Having already learned a few tricks, next time I can dedicate more time to polishing.