This year I decided to write something for JS1K. My entry is based on a game from my childhood called Thrust). The basic premise of the game is to navigate a ship down into a cavernous planet, retrieve a pod and escape with it back to space.

Play Thrust at JS1K

The original game featured simple physics, pixel perfect collision detection, fuel depots, nuclear reactors, gun turrets and tractor beams – there was no way I could recreate all this in just 1K so I had to decide which elements I wanted to try and keep… I ended up here:

  • Stay faithful to the original look.
  • Reduced number of game objects – a ship, a pod, the pod plinth and a map.
  • Pixel perfect collision detection – scraping the along cavern walls gives a real sense of panic.
  • Physics – the pendulum effect experienced when the pod is attached to the ship.
Screenshot of JS1K thrust and the original BBC micro version (inset)

Early on I also decided to avoid using packers like RegPack and JSCrush because I wanted the minified source code to remain semi-legible. I have nothing against these tools, I just find the challenge of writing code to fit into 1K more rewarding than writing repetitive and verbose code that compresses well.

I found writing a game in 1024 bytes incredibly tricky. Many JS1K entires are rolling demos and don’t need to worry about handling user interaction and dealing with consequences. Listening for input, handling collision detection and managing game state really eats up space!

Although my entry didn’t make the top 10 it did receive an honourable mention.

The code

Here’s the final source for my entry. The minification process is simple, remove all comments and white space from the original source and convert the setInterval function body into a string:

C=[],B=[],O=Math,b.bgColor=Z=0,T=.001,onkeydown=onkeyup=function(a){C[a.which-32]=a.type[5]},setInterval('for(Q=[],R=a.width|=d=0;4>d;)with(B[d]=B[d]||(W=d&&3>d,L=U=V=M=0)||{o:["MTRWWOSOMCGOCOHWMT","MCFFCMFTMWTTWMTFMC","HkJiJ^DYFWMZTWVYP^PiRk","CaCOLOMPMRNSUSSWOXO]Q]QZTYWSUQOQNPNO[O[a"][d],x:210*W,y:1090*W,r:0,u:0,v:0}){P=B[0],t=O.sin(r),q=O.cos(r),M||(d||(E=5*T*!! C[6],r+=(!C[5]-!C[7])/50,u+=U+t*E,v+=V-q*E+T,S=2+y*T),F=x,G=y,x+=u,y+=v,1==d&&(U=x-P.x,V=y-P.y,J=O.sqrt(U*U+V*V),K=.5*(J-70)/J,L&&J>68?(r=O.atan2(V,U)-1.5,x-=U*=K,y-=V*=K,u=x-F,v=y-G+T):(L=O.abs(U)<20&&71>J&&!!C[8],U=V=0))),1!=L*d&&c.beginPath();for(f in o){if(Y=o.charCodeAt(f)-77,d>2&&(Y*=70),1&f){if(J=X*q-Y*t+x,K=Y*q+X*t+y,d>L)for(j=0;f>1&&$>j;)D=Q[j++],E=Q[j++],F=Q[j],G=Q[j+1],g=K-E,h=H-D,i=I-E,k=J-D,n=F-D,m=G-E,M|=g*h>i*k^(K-G)*(H-F)>(I-G)*(J-F)&&i*n>m*h^g*n>m*k;else r+=M&&M++,$=Q.push(J,K);H=J,I=K,c[f?"lineTo":"moveTo"](S*(J-P.x)+R/2,S*(K-P.y)+a.height/2)}X=Y}A=3>d?"stroke":"fill",c[A+"Style"]="#6c5",L^++d&&c[A]()}B=M>149||P.y<-50?[]:B',15)

Here's the unminified, commented source code:

C = [],
B = [],
O = Math,
b.bgColor = Z = 0,
T = .001,
onkeydown = onkeyup = function (a) {
  C[a.which - 32] = a.type[5]
},
setInterval(function () {
  for (Q = [], R = a.width |= d = 0; 4 > d;)    // clear the canvas ready for rendering
    with (
      B[d] = B[d] ||                               // get next object or....
      (W = d && 3 > d, L = U = V = M = 0) || {     // ...add object if it doesn't exist
      o: [                                            // object data
        "MTRWWOSOMCGOCOHWMT",                            // ship
        "MCFFCMFTMWTTWMTFMC",                            // orb
        "HkJiJ^DYFWMZTWVYP^PiRk",                        // orb plinth
        "CaCOLOMPMRNSUSSWOXO]Q]QZTYWSUQOQNPNO[O[a"       // map
      ][d],
      x: 210 * W,                                     // x position
      y: 1090 * W,                                    // y position
      r: 0,                                           // rotation
      u: 0,                                           // x velocity
      v: 0                                            // y velocity
    }) {
      P = B[0],                                       // store a reference to the player
      t = O.sin(r),                                   // pre-calculate sin (used later for direction and vertex transforms)
      q = O.cos(r),                                   // pre-calculate cos
      M || (                                          // is the player alive?
        d || (                                           // update player
          E = 5 * T * !! C[6],                              // increase thrust if 'up' key is pressed
          r += (!C[5] - !C[7]) / 50,                        // rotate if 'left' or 'right' keys are pressed
          u += U + t * E,                                   // increase players X velocity, adding U from orb movement if attached
          v += V - q * E + T,                               // increase players Y velocity, adding V from orb movement and gravity
          S = 2 + y * T                                     // zoom in as the player descends in to the caverns
        ),
        F = x,                                           // store object current X position
        G = y,                                           // store object current Y position
        x += u,                                          // move object along x axis
        y += v,                                          // move object along Y axis
        1 == d && (                                      // update the orb
          U = x - P.x,                                      // get X delta between ship and pod (used next iteration to adjust player)
          V = y - P.y,                                      // get Y delta between ship and pod
          J = O.sqrt(U * U + V * V),                        // get distance between ship and pod
          K = .5 * (J - 70) / J,                            // calc distance delta between ship and pod
          L && J > 68 ? (                                   // is the pod attached to the ship?
            r = O.atan2(V, U) - 1.5,                           // set rotation angle of pod (-1.5 is approx. Math.PI/2)
            x -= U *= K,                                          // move pod along X axis
            y -= V *= K,                                          // move pod along Y axis
            u = x - F,                                            // set pod X velocity
            v = y - G + T                                         // set pod Y velocity and add gravity
          ) : (                                             // if the pod isn't attached to the ship...
            L = O.abs(U) < 20 && 71 > J && !! C[8],            // capture it if the ship is directly above and 'down' key is pressed
            U = V = 0                                             // clear inertia values
          )
        )
      ),
      1 != L * d && c.beginPath();                    // start a new path unless the orb is attached to the ship (draws the arm automatically)
      for (f in o) {                                     // read the object vertex data (f%2==0 is X data, f%2==1 is Y data)
        if (Y = o.charCodeAt(f) - 77,                       // decode the coordinate
            d > 2 && (Y *= 70),                             // if this object is the map, scale it up by a factor of 70
            1 & f) {                                        // if this is the second vertex coordinate, we have an X and Y
          if (J = X * q - Y * t + x,                           // rotate and translate along the X axis
              K = Y * q + X * t + y,                           // rotate and translate along the Y axis
              d > L)                                           // if object is the ship (or orb, if attached) add vertex to the collision array
            for (j = 0; f > 1 && $ > j;)                          // check this line doesn't intersect any lines in the collision array...
              D = Q[j++],                                         // ...if it does, set the player dead flag (M==1)
              E = Q[j++],
              F = Q[j],
              G = Q[j + 1],
              g = K - E,
              h = H - D, 
              i = I - E, 
              k = J - D,
              n = F - D,
              m = G - E,
              M |= g * h > i * k ^ (K - G) * (H - F) > (I - G) * (J - F) && i * n > m * h ^ g * n > m * k;
          else 
            r += M && M++,                              // simulate an explosion by rotating the ship / orb (M==0 when player is alive)
            $ = Q.push(J, K);                           // store these points in the collision array
          H = J,                                     // store end X data as the start of the next line
          I = K,                                     // store end Y data as the start of the next line
          c[f ? "lineTo" : "moveTo"](                // draw the line
            S * (J - P.x) + R / 2,
            S * (K - P.y) + a.height / 2
          )
        }
        X = Y                                      // if this is the first vertex coordinate, store it as X
      }
      A = 3 > d ? "stroke" : "fill",               // fill the ground, stroke everything else
      c[A + "Style"] = "#6c5",                     // set fill / stroke style for the object
      L ^ ++d && c[A]()                               // paint the object (if object is the ship with orb attached skip the paint)
    }
    B = M > 149 || P.y < -50 ? [] : B           // if the player escapes or crashes, reset the map
  }, 15)

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.