Last year, I entered JS1K for the first time with a game based on the 8-bit classic, Thrust. My entry for 2015 is another retro game — this time inspired by the arcade shooter Defender. Since writing this article, Defender went on to win JS1k 2015!

My JS1K 2015 entry: Defender

Squeezing such an iconic game into a kilobyte while keeping true to the original was a real challenge. The 1024 byte limit meant I couldn't include all the original game content but I did try to keep the core of my entry as close to the original as possible. Here are some of the features I managed to implement:

  • The AI: Alien landers hunt down and abduct humans. Abducted humans are mutated and return to hunt down the player. Landers and mutants also fire missiles at the player.
  • Infinite scrolling landscape and parallax starfield.
  • Sprites, lazers, explosions and warp effects.

Play Defender

Note: This version uses a custom shim to create a full viewport, fixed aspect-ratio canvas using CSS. The JavaScript is identical to that used in the official JS1K entry. To play the entry in the official shim, please head over to the JS1K website.

Building Defender

As with last year, my intention was to write an entry that would fit into 1024 bytes without mangling the source code with a packer (I like to see how things work). I did manage to write a playable version of Defender in 1023 bytes without a packer, but I wasn't entirely happy with the way it looked and I wanted my entry to have more features. To get the most from my 1K limit I was going to need a packer — time to start again.

The original 'no-packer' prototype of Defender

Try the original prototype

Tooling

To make developing with a packer efficient I needed a build tool, so I wrote a simple grunt task to minify the source with Uglify, post-process the result (adding my optimisations) and then pack it with RegPack. The packed output was injected into the JS1k 2015 shim and saved to disk. The grunt task also monitors the source file, triggering the build process whenever changes are saved.

Once the tooling was ready I started by cherry-picking parts of my prototype, reformatting and unrolling code to make it "packer friendly". Next, I incrementally added new features such as colour graphics, mutants and improved AI until I was just over the 1K limit. Once I was happy with my entry I disabled Uglify and, using the minified output as my source, began manually shrinking the code.

Writing code for a packer

Writing compact code is a completely different challenge to writing code that compresses well — with a packer, it's all about repetition.

For example, in the prototype I use a logical flip-flop to control the main for loop counter, allowing the collision and movement logic to run twice for enemy landers. In the on state, the code is used to check collisions against, and fire at the player. In the off state the same code is used to hunt down and grab humans. In the final packer-friendly version, I dropped the flip-flop in favour of duplicating the entire code block to introduce repetition and produce a smaller output file.

To get the best out of RegPack I needed to understand how it worked with my code so I created a tool to visualise the compression patterns it was using. The tool patches the decompression loop of RegPack (and JSCrush), forcing it to wrap each decompressed substring in a <span> element. The packer is then run and the resulting string is rendered to the DOM. Finally, a little CSS is used to produce a visual representation of the various strings and how they are nested.

Defender source code compression patterns

Try the visualisation tool

Identifying substrings that compress well allowed me to restructure the source code to exploit these patterns and further reduce file size. Simple changes, such as reordering operators in an expression or ensuring an =0 assignment preceeds a comma, saved one or two bytes per statement. These small gains soon added up.

One of the biggest repeated patterns is the pixel plotter. It's used to draw the terrain, stars, sprites and the lazer pixels. The same set of statements are used to clip, scale, colour, and draw the pixel rectangle, so they were crafted to have an identical signature to ensure they pack efficiently:

X<255&&c.fillRect(X<<1,Y<<1,2,2,c.fillStyle="hsl("+C+",99%,50%)")

Drawing sprites

My entry contains 5 single colour sprites, each 7 pixels wide by 7 pixels high. Limiting a sprite to a single colour allows each 7 pixel row of image data to be stored in a single byte, using its individual bits to determine if a pixel should be painted or not. Sprites are purposely 7 pixels wide to avoid touching the upper, 8th bit — setting this bit high will cause the value to be stored as 16 bits.

Storing data this way meant I only used 7 of my 1024 bytes for each sprite, but it did present a problem; some of the image data contained bit combinations that produced unprintable characters when converted to a string. To get around this, I XOR each byte with fixed value of 69 (determined by the sprite generation tool) to ensure that each byte contained enough high bits to produce a printable character.

The enemy 'lander' sprite before and after XOR treatment

Once the sprites are encoded, the resulting string looks like this:

var myEncodedSprites = '[y}}U]UMA8Z8AMEMYYMEE {V:V{  {V:V{ ';
The encoded sprite data used in the final entry

Note: The lander sprite is purposely duplicated for the mutant. In the original game the two enemies look very similar. Duplicating the sprite results in a smaller packed file (yet more repetition) and uses less bytes than any logic I could write to share the sprite.

The sprites used in the demo were generated with their row/column data transposed so they appear rotated. I did this to cater for variable width sprites (the original game has a wider player ship) without having to deal with the issues of crossing the 8th bit. Unfortunately, I didn't have enough space left to make use of this.

Drawing the sprites is simply a case of applying the XOR again to remove the mask, shifting bits and painting rectangles. Here's an example of painting the player ship:

for(l=55;X=l&7,Y=l>>3,l--;)(69^'[y}}U]U'.charCodeAt(Y))>>X&1&&c.fillRect(Y,X,1,1)

The colour of the sprite is determined by setting the fillStyle property with hsl(i, 100%, 50%) where the hue i is the index of the sprite (0-4), multiplied by 99 (another packer friendly value). The 4th sprite produces a hue value greater than the maximum 360, but that's not a problem because hue is an angle so values implicitly wrap.

Warping and explosions

Creating the visual effects such as enemy explosions and warping, or mirroring the player sprite when it changes direction was fairly simple. All the effects are achieved by scaling the coordinates of each sprite pixel as it is painted. Explosions scale both the X and Y values and mirroring is acheived by scaling the X coordinate by -1. As I'm already painting each sprite pixel, scaling them during render required little effort. Here's an example of how it works:

// t = 0      // Sprite                     (0 = player, 1 = human, 2 = missle)
// d = 1      // Direction                  (-1 = left, 1 = right)
// w = 1      // Warp / Explosion factor    (1 = normal)
// x = 100    // X position
// y = 100    // Y position

for(l=55;U=l&7,V=l>>3,X=x+d*(V-3)*w,Y=y+(U-3)*w,l--;)
  (69^'[y}}U]UMA8Z8AMEMYYMEE'.charCodeAt(t*7+V))>>U&1&&c.fillRect(X,Y,1,1)
An animation of an enemy 'lander' warping into the level

In the final demo you may have noticed that the sprites are flipped when they explode. This is because the warp factor is a negative value during the explosion and I had to trade-off computing the absolute value to save bytes.

Drawing the background graphics

The background graphics in Defender consist of a rugged undulating terrain and a parallax starfield. While both these elements are purely cosmetic, dropping either of them from my entry made the game feel lifeless so both had to be included.

The terrain is generated from a simple cosine wave. The current loop iteration count is given the bit mask treatment and passed to Math.cos() and the result is added to the value from the previous iteration, producing an undulating mountain range.

Finding the perfect bit mask was a case of trial and error. The terrain must tile seemlessly because the player can fly in the same direction continuously, therefore any terrain generation formula must produce identical start and end values. I found Math.cos(iteration / 5 & -11) produced a reasonably rugged mountain range while appearing to wrap infinitely:

for(Y=127,X=1023;X;Y+=Math.cos(X--/5&-11))c.fillRect(X,Y|0,1,1)
The entire seemless terrain

The planet width is actually a bit mask (yeah, another one) with the lower 10 bits set high. The width is derrived from Math.pow(2, 10) - 1, which produces a value of 1023. This mask is used with a logical AND to keep values inside the world. I'm using a bitwise operator over modulus because stripping bits prevents negative values.

The same values calculated for the terrain are also used to draw the parallax starfield. The X position is halved, producing a slower scrolling speed, and the Y value is scaled up and logically ANDed with 1023. Scaling the Y position in this way purposely results in the majority of the stars ending up off canvas, which saved writing logic (and bytes) to distribute them along the X axis.

The code

Here’s the final, packed source for my entry:

for(_='s=KC=GL=B&&Q<4QP,X=`W[_=1=Math.cos(0%)")25)):c.fill,onkey99 s. {V:V{   -H+127+||(w--<1&A,Y),0,(q=-511+(E[d])Rect(x-x+511&Ar==function(k){E[0]._k.which-32]=,Style="hsl("+C+", %,-y,psqrt(q*q+r*rnatan2(r,qunvsin(nX<5QX<,Y<,2,25H=$=J=D,A023,E=[]down1}up0},setInterval(Wfor(F27,BGd=A,A;d--;){if(G`d-H=F+d/5&-11,G-F,X/=2,Y *F< Q,JQ(G5*d`(16-z)*d*Dx=y,d<Qwith(for(l=55,x+=u*g,y+=v*g,zQz--,w?(KK<-Q(A*=t>E.splice(d,1)yp<5?t>1Q:JQ-rPrPp30Qq*DQ,t?t<2?(v=s?-1:y4yQ(B5,t<3?zu+v?w=-A:(z= (zB3,O=x,N=y,z= +dsQ!wQ!(sQs!=?y-8pQ(x=x,y-=r,K):KtPE[$%d],t=K0(H=x+u*2,I=!_5]-!_7],D=I||D,u-=I?u*I<Q-I/2:u/,y+=_8]?y40:yQ-!!_6],zz6*_0]J=z>9));U=l&7,V=l>>3`x(U-3)*D*++w=y+*(V-3G *t,l--;(69^"[y}}U]UMA8Z8AMEMYYMEE".charCodeAt(7*t+V))>>U&1Q);dB$1?2-!$:$% Q4,N=70*L%24O= *$LQE.push({g:L/4,t:L-1,w:L>3Q,s:Bu:v:z:x:O,y:N})}$++},16)';g=/[- -_`PQBGK]/.exec(_);)with(_.split(g))_=join(shift());eval(_)

...and here's the full annotated source code:

/*
 * Defender: 1024 by Keith Clark 
 * 
 * A 1K game inspired by the classic arcade shooter Defender. Written
 * for the 2015 JS1K competition.
 * 
 * Save the humans from alien abduction! Pilot your ship and fire its
 * laser at enemy landers before they abduct and transform humans into
 * mutants programmed to hunt you down!
 * 
 * Use the arrow keys to fly and space to fire.
 *
 * web: keithclark.co.uk | tweet: @keithclarkcouk
 */

// Initialise any globals

H =                                                 // Camera position
$ =                                                 // Game frame ticks
J = 0,                                              // Player fire flag
D = 1,                                              // Player direction
A = 1023,                                           // World width
E = [],                                             // Entity stack

// Handle user input
// W[0] = fire           [space bar]
// W[5] = thrust left    [left arrow]
// W[7] = thrust right   [right arrow]
// W[6] = climb          [up arrow]
// W[8] = dive           [down arrow]

onkeydown = function(k) {                           // key down state handler
  W[k.which - 32] = 1;                                 // set key flag
},
onkeyup = function(k) {                             // key up state handler
  W[k.which - 32] = 0;                                 // clear key flag
},

// Game loop

setInterval(W = function(k) {
  for (
    F = 127,                                           // mountain range Y start position
    c.fillRect(L = 0, C = 0,d = A, A,                  // clear the screen
      c.fillStyle = "hsl(" + C + ",99%,0%)"
    )
    ;
    d--                                                // for 1023 cycles...
    ;
  ) {
    if (

      // Draw a mountain pixel

      C = 25,                                             // set colour to brown
      X = d - H & A,                                      // set pixel X position
      Y = F += Math.cos(d / 5 & -11),                     // set pixel Y position
      X < 255 &&                                             // if the pixel is in the viewport 
        c.fillRect(X << 1, Y << 1, 2, 2,                        // plot the pixel
          c.fillStyle = "hsl(" + C + ",99%,50%)"
        ),

      // Draw a starfield pixel

      C = -F,                                             // set colour to dark blue
      X /= 2,                                             // half mountain X pixel (simple parallax)
      Y = 199 * F & A,                                    // set pixel Y position
      Y < 99 && X < 255 &&                                   // if the pixel is in the viewport
        c.fillRect(X << 1, Y << 1, 2, 2,                        // plot the pixel
          c.fillStyle = "hsl(" + C + ",99%,50%)"
        ),

      // Draw a player lazer pixel

      J && (
        C = 5 * d,                                        // set colour spread from red to green
        X = (16 - E[0].z) * d * D - H + 127 + E[0].x & A, // set X position
        Y = E[0].y,                                       // set Y position matches player Y position
        d < 25 && X < 255 &&                                 // if the pixel is in the viewport 
          c.fillRect(X << 1, Y << 1, 2, 2,                      // plot the pixel
            c.fillStyle = "hsl(" + C + ",99%,50%)"
          )
      ),

      E[d]                                                // check we have an entity to process

    // Game logic

    ) with (E[d]) for (
      l = 55,                                             // entity sprite has is (8 * 7 - 1) bits
      x += u * g,                                         // increment entity X position
      y += v * g,                                         // increment entity Y position
      z && z--,                                           // decrement entity timer (used for fire rate / bullet life etc.)

      w ?                                                 // if this entity warping (w>0) or exploding (w<0)
        (
          s = s.s = 0,                                       // clear link with any other entity (lander -> human -> lander)
          w-- < -25 && (                                     // decrement warp / explosion counter. If explosion has ended...
            A *= t > 0,                                         // stop the game if the entity is the player 
            E.splice(d, 1)                                      // remove the dead entity from the game
          )
        )
      :                                                   // the entity is out of warp and "in play"
        (
          q = -511 + (E[0].x - x + 511 & A),                 // get x delta to player (-511 to +511)
          r = E[0].y - y,                                    // get y delta to player
          p = Math.sqrt(q * q + r * r),                      // get distance to player
          n = Math.atan2(r, q),                              // get angle to player
          p < 5 ?                                            // if entity has hit player
            t > 1 && E[0].w--                                   // if entity is not a human, explode player
          :
            J && -r < 4 && r < 4 &&                          // if player is firing and entity inline with player 
            p < 130 && q * D < 1 && w--,                        // if entity is in front of player, explode entity
          t ?
            t < 2 ?                                          // if entity is a HUMAN
              (
                v = s ?                                         // if grabbed by a lander
                  -1                                               // make human climb (the lander will actually chase it)
                :                                               // if not grabbed by a lander
                  y < 140,                                         // if human is in free air, fall to earth
                y < 1 && (L = 5, w--)                           // if human was captured, kill it and spawn... a... MUTANT!
              )
            :
              t < 3 ?                                        // if entity is a BULLET
                z || (                                         // if entity timer has reached 0
                  u + v ?                                         // and it's moving
                    w = -A                                        // instantly destroy it
                  :
                    (                                        // if it's not moving, it's a new bullet
                      u = Math.cos(n),                       // set X direction to track player
                      v = Math.sin(n),                       // set Y direction to track player
                      z = 99                                    // set bullet life
                    )
                )
              :
                (                                           // if entity is a LANDER or a MUTANT
                  u = Math.cos(n),                             // set X direction to track player
                  v = Math.sin(n),                             // set X direction to track player
                  z || (                                          // if the timer has reached 0
                    L = 3,                                        // spawn a BULLET
                    O = x,                                        // spawn at entity X position
                    N = y,                                        // spawn at entity Y position
                    z = 99 + d                                    // reset the entity firing timer
                  ),

                  s && !s.w && !(s.s && s.s != E[d]) ?         // if tracking a human (a lander)
                    (
                      q = -511 + (s.x - x + 511 & A),             // get x delta to human (-511 to +511)
                      r = s.y - 8 - y,                            // get y delta to human
                      p = Math.sqrt(q * q + r * r),               // get distance to human
                      n = Math.atan2(r, q),                       // get angle to human
                      u = Math.cos(n),                            // set X direction to track human
                      v = Math.sin(n),                            // set Y direction to track human
                      p < 1 && (                                  // if lander is grabbing human
                        x = s.x,                                     // align x values (prevents rounding issues)
                        y -= r,                                      // align y values (prevents rounding issues)
                        s.s = E[d]                                   // link the human to the lander
                      )
                    )
                  :
                    s = t < 4 && E[$ % d],                       // if not tracking and not a mutant - pick an entity...
                    s.t == 1 || (s = 0)                             // if it's a human, track it
                )
            :
              (                                             // if entity is the PLAYER
                H = x + u * 2,                                 // set the camera
                I = !W[5] - !W[7],                             // determine X input
                D = I || D,                                    // set player direction
                u -= I ? u * I < 25 && -I / 2 : u / 25,        // set player X velocity
                y += W[8] ? y < 140 : y && -!!W[6],            // set player Y position
                z || (z = 16 * W[0]),                          // is player able to fire?
                J = z > 9                                      // set player firing flag
              )
          )
      ;

      // Entity rendering

      U = l & 7,                                            // get sprite bit column
      V = l >> 3,                                           // get sprite bit row
      X = x - H + 127 + (V - 3) * D * ++w & A,              // pixel X position
      Y = y + w-- * (U - 3),                                // pixel Y position
      C = 99 * t,                                           // set entity colour
      l--                                                   // decrement counter, ready for next bit
      ;
      (69 ^ "[y}}U]UMA8Z8AMEMYYMEE  {V:V{   {V:V{ ".charCodeAt(7 * t + V)) >> U & 1 && 
        X < 255 && c.fillRect(X << 1, Y << 1, 2, 2,
          c.fillStyle = "hsl(" + C + ",99%,50%)"
        )
    )
    ;

    // Entity spawning

    d || (
        L = $ < 11 ?                                        // first 11 cycles add player / humans
          2 - !$                                               // player if first cycle, or 9 humans
        :
          $ % 99 < 1 && 4,                                  // every 99 cycles add a lander
        N = 70 * L % 240,                                   // the spawn X position
        O = 99 * $                                          // the spawn Y position
    ),

    L && E.push({                                        // If new entity flag is set, add it
        g: L / 4,                                           // entity speed
        t: L - 1,                                           // entity type
        w: L > 3 && 25,                                     // entity warp / explosion frame
        s: L = 0,                                           // entity target (and clear entity flag)
        u: 0,                                               // entity X velocitiy
        v: 0,                                               // entity Y velocitiy
        z: 0,                                               // entity timer
        x: O,                                               // entity X position
        y: N                                                // entity Y position
    })
  }

  $++;                                                   // increment frame counter

}, 16)

Personal Achievements

  • 2017 Web Designer Magazine: CSS VR interview
  • 2015 JS1k – winner
  • 2014 Net Awards: Demo of the year – winner
  • 2014 Net Awards: Developer of the year – longlist
  • 2013 Public speaking for the first time
  • 2011 .net Magazine innovation of the year – shortlist

Referenced in…

Smashing CSS, CSS3 for web designers, Programming 3D Applications with HTML5 and WebGL and more.

My work is referenced in a number of industry publications, including books and magazines.