The making of

BOXING 1K

JS1k: X
When the JS1k 2019 theme of "X" was announced, the first things that popped into my mind were "X games" and "generation X". Eventually, I started pondering retro games of my youth, and I decided to try to do a few (hopefully) faithful 1K reproductions of (or at very least, tributes to) some classics.

I've always liked Activision's Atari 2600 games, so after a quick search, a flood of nostalgia, and a few failed attempts at cramming other games into 1K (Pitfall, River Raid, Jungle Hunt), I figured I'd give Activision's classic Boxing a try.

Bitmapped Boxers?
I was pretty sure right off the bat that there was no way I was going to be able to use raster images here. There have been some JS1k demos in the past with good bitmap compression (RPG battle, hypnotoad, etc.), but I was confident that between the size of the boxers and the number of animation frames, it was just going to be too much, even at 1-bit color depth – but I figured I'd give it a try nonetheless. As it turns out, there's a fine line between determination and naiveté...

Manually Recreating the Artwork
The first thing I did was pull a screen grab of an Atari 2600 emulator running Boxing into Photoshop to take a look at the images I'd be working with. It was quickly obvious that trying to grab 0's and 1's from a zoomed-in boxer wasn't happening, let alone for multiple frames of animation.

Tooling
The next thing I did (again assuming it would probably be futile) was to create a tool which would capture the rendered window of the 2600 emulator, grab the various frames of the boxer, and compress them as best it could. The tool was quick to write, and it made it pretty easy to get compressed images I could embed into code. Unfortunately, when all was said and done, the sprites alone were over 1k, even with the best compression I could come up with.

Drawing Things Myself
I'm not an artist, so when it became clear that I had to draw these guys myself, I knew I needed some help. The first thing I did was to create a very simple drawing tool which would let me draw circles and curves. The tool would then give me JavaScript code which reflected what I just drew. Again, it dawned on me: I'm no artist. Trying to generate code from a boxer I was hand-drawing was the wrong approach.

Drawing Things Myself: Take 2
After realizing that there was no way I was going to be hand-drawing a boxer that looked anything like the original, I decided to change my approach. I broke down the boxer into a set of parameters which defined his appearance, and I figured I'd tweak those until I came up with a boxer that looked like the original.

At his simplest, the boxer has a head and nose, two arms, and two gloves. I figured I could draw these with two circles for the head, two circles for the gloves, and one or two quadratic curves for the arms. This was a start, but it became obvious pretty quickly that the figure was much more nuanced than that, especially when animated. I needed control over things like the angle and distance of the arm being drawn back, the bend at the elbow, arm spacing and extension, etc.

Eventually, I came up with an extensive list of configurable options that gave me good control over the boxer's appearance. These configurable options (non-coincidentally) mapped directly to the parameters of our primitives: the x and y radius of the head, nose, and gloves as well as the start, end, and control points of our quadratic curves.

Making Him Move
With the ability to adjust the appearance of my boxer as needed now at my disposal, half of the hard work was done. I now had to animate the guy while trying to stay faithful to the original animation. I added the ability to tween animation from the "neutral" stance to a second configured stance. At this point, it was really just a matter of trial and error to find model settings which looked good and tweened smoothly from neutral to swinging and back.

Putting it All Together
With the boxers all drawn and animated, all that was left to do was to write the actual game (LOL!)

The game itself was pretty standard fare. A lot of typical byte-saving tricks, a lot of cheating with numbers to increase duplication and reduce size. The rendering and hit detection use the same chunk of code (note the two nested .map calls) for both arms of both boxers, which was essential to making things fit.

Hitting the 1K Limit
I couldn't fit in a round timer or the hallmark collapsed face of the original, but I was fine with that. The boxers also don't swap directions if they get too close, but I was fine with that, too. I would have liked to improve the AI a bit... right now, the computer is just a straight up slugger, but at least he's smart enough to try to follow you and punch with the better hand. As it is, I'm happy with the way it turned out.

Credits
Obviously, everything from the original classic is property of and copyrighted by Activision, Atari, etc. This 1K version was written for fun, so don't sue me...

Play It
Play it on JS1k.com or here. Arrow keys to move, Z and X to swing.

Other JS1k 2019 Entries
Somewhat unrelated, but my other JS1k 2019 entries are:

  • Yars 1K. A tribute to another Activision classic, Yars' Revenge. Play it here. Arrow keys move, space shoots and launches the cannon.
  • Sharked. Play it here. Fish the rough waters with a couple of cold ones as you avoid the sharks. Hook a shark or run out of beer, and you're done. Up and down arrows to cast and reel in your line. Requires Unicode 9.0 (2016) support.
    onkeydown = onkeyup = e => { K[e.which-37] = (e.type[5]); }

    K = [];
    P = 3.2;

    W = {s:0,m:[[30,30,20,100,60],[30,30,20,100,60]],n:{},c:'#ddd',x:300,y:300}
    B = {s:0,m:[[30,30,20,100,60],[30,30,20,100,60]],n:{},c:'#000',x:1600,y:600}

    setInterval(()=>{
        c.lineWidth = 30;
        c.fillStyle = '#684';
        c.fillRect(0,0,1920,1080);
        c.strokeStyle = '#c84';
        c.beginPath();
        c.rect(200,100,1920-400,1080-400);
        c.stroke();

        F=[];
        G=[];
        [-1,1].map((x,j)=>{
            [-1,1].map((y,i)=>{
                $=j?W:B;
                m=$.m[i];
            
                c.fillStyle = c.strokeStyle = $.c;
                c.fillText($.s,!j*1500+200,80);

                c.beginPath();
                c.ellipse($.x,$.y,40,40,0,-P,P); // head
                c.ellipse($.x+40*x,$.y,10,10,0,-P,P); // nose
                F.push({p:x,l:$.x+40*x,t:$.y,r:10+$.x+40*x,b:10+$.y});
                c.fill();
                
                c.beginPath();
                c.moveTo($.x,$.y-m[0]*y);
                c.quadraticCurveTo($.x-m[1]*x,$.y-m[3]*y, $.x-m[2]*x,$.y-m[3]*y);  // backward arc
                c.quadraticCurveTo($.x+m[4]*x,$.y-m[3]*y, $.x+m[4]*x,$.y-m[3]*y);  // forward reach
                c.stroke();
                
                c.beginPath();
                c.ellipse($.x+(m[4]*x),$.y-m[3]*y,30,30,0,-P,P);
                G.push({p:x,l:$.x+(m[4]*x),t:$.y-m[3]*y,r:30+$.x+(m[4]*x),b:30+$.y-m[3]*y});
                c.fill();

                if ($.n.m == i) 
                    if ( $.n.f++ < 8) for (p in $.n.d) m[p] += $.n.d[p]*$.n.s;
                    else
                    if ($.n.s>0) 
                        $.n.s = -1,
                        $.n.f = 0;
                    else 
                        $.n = 0;
                
                // n.m = arm
                // n.f = frame
                // n.s = sequence/direction
                // n.d = deltas

            });
        });
        
        if (K[0]) $.x-=4;
        if (K[1]) $.y-=4;
        if (K[2]) $.x+=4;
        if (K[3]) $.y+=4;

        if (K[51] && !$.n.d) $.n = {f:0,s:1,m:0,d:[-1,-6,-18,-3,18]};
        if (K[53] && !$.n.d) $.n = {f:0,s:1,m:1,d:[-1,-6,-18,-3,18]};
                
        /* diagnostic */
        // hit boxes
        /*c.beginPath();
        c.save()
        c.strokeStyle = '#f00';
        c.lineWidth = 1;
        F.forEach(v=>{
            c.rect(v.l,v.t,v.r-v.l,v.b-v.t);
        });
        G.forEach(v=>{
            c.rect(v.l,v.t,v.r-v.l,v.b-v.t);
        });
        c.stroke();
        c.restore();
        */

        F.map(x=>{
            G.map(y=>{
                if (!(y.l > x.r || y.r < x.l || y.t > x.b || y.b < x.t)) 
                    ((y.p<0)?B:W).s++,
                    ((y.p>0)?B:W).x+=80*y.p;
            });
        });

        if (Math.abs(B.x-$.x)>200) B.x-=2*Math.sign(B.x-$.x+200);
        if (Math.abs(B.y-$.y)>80) B.y-=2*Math.sign(B.y-$.y+80);
        if (Math.random()<.1&&!B.n.d) B.n = {f:0,s:1,m:($.y-B.y<0),d:[-1,-6,-18,-3,18]};   

        $.x=$.x<250?250:$.x>B.x-200?B.x-200:$.x;
        $.y=$.y<242?242:$.y>638?638:$.y;
        B.x=B.x<$.x+200?$.x+200:B.x>1670?1670:B.x;
    }, 16);
Copyright © 2019 Jason Plackey