Hackers News

Bouncing Beholder [JS1k entry]

Bouncing Beholder

My winning JS1K entry—a JavaScript platform game that fits in 1024
bytes.

I gave a talk on this at the November Berlin JS user group. Slides here. Though they might be hard to follow
without commentary.

(See here for my old, space-defense game entry.)

Use the arrow keys to move and jump. Collect as many coins as you can. Be wary of purple-colored
grass.

This is the code (newlines added):

c=document.body.children[0];h=t=150;L=w=c.width=800;u=D=50;H=[];R=Math.random;for($ in C=
c.getContext('2d'))C[$[J=X=Y=0]+($[6]||'')]=C[$];setInterval("if(D)for(x=405,i=y=I=0;i<1e4;)L=\
H[i++]=i<9|L9?0:X;j=H[o=\
x/u|0];Y=y9&S<41;ta(u-S,0);G=cL(0,T=H[i],0,T+9);T%6||(A(2,25,T-7\
,5),y^j||B&&(H[i]-=.1,I++));G.P=G.addColorStop;G.P(0,i%7?'#7e3':(i^o||y^T||(y=H[i]+=$/99),\
'#c7a'\));G.P(1,'#ca6');i%4&&A(6,t/2%200,9,i%2?27:33);m(-6,h);qt(-6,T,3,T);l(47,T);qt(56,T,56,\
h);A(G);i%3?0:T$-9?1:D);ta(S-u,0)}A(6,u,y-9,11);A(5,M=u+X*.7,Q=y-9+Y/5,8);A(8,M,Q,5);fx(I+'c',5,15)}D=y>h?1:D"
,u);onkeydown=onkeyup=function(e){E=e.type[5]?4:0;e=e.keyCode;J=e^38?J:E;X=e^37?e^39?X:E:-E}

WHY?

I’ve heard people wax poetic about programming old, limited-memory machines. I wouldn’t know
anything about those—at the time they were current, I was writing rudimentary number-guessing
games in BASIC. But doing this competition entry gave me a taste of what they might be talking
about.

In typical 21st-century programming, the machine limits one has to deal with are wide and fuzzy.
Program size is rarely an issue, so like painters working on an infinite canvas, we often don’t know
when to stop. When a program has to fit in a tightly limited space, the experience is different. You
program by carefully refining every single expression, chipping away at your code until it reflects
your vision as well as it can.

In terms of productivity, this is an awful way of coding. But it certainly is fun. Not to mention
that it gives me an excuse to use every kind of weird hack I can think of.

HOW?

For a start, of course, there are the tiny local tricks that save a few bytes here and there,
which adds up to at least a hundred bytes on the whole program. |0 truncates, && or ?: can
replace if (sometimes), & can replace && (sometimes), you can reuse initializers
(J=X=Y=0), a with statement can shorten object access, etc.

Compression algorithms, such as Google’s Closure Compiler and UglifyJS, and various eval/replace hacks suggested for
the JS1K contest, don’t really do much on properly hand-compressed code. In fact, they all ended up
making the code bigger…

The tiny size required me to design the program in a ‘holistic’, highly un-modular way, meaning
every single aspect of the program could influence every other one. There was an issue causing the
clouds to be drawn incorrectly for negative X coordinates. To work around this would have required
quite a few extra characters (I was using x|0 where I actually needed Math.floor(x)). Instead, I
made the playing field start at 400 and put empty space at the start to prevent the player from
seeing any negative X coordinates. Problem solved.

Mechanized Abbreviation

The coolest hack in this program is probably the mechanized abbreviation of the canvas context
methods. Method names like quadraticCurveTo, createLinearGradient are nice and explicit, but those
two taken together already eat 3.5% of the bytes available—when only referenced once! I needed to
use them, but I wanted to avoid spelling them.

Turns out I can get away with that. At the start of the program there is a for/in loop that goes
over the properties of the canvas context, and adds a new property, with a shorter name, for each of
them. It took some experimenting to find an abbreviation algorithm that doesn’t have clashes on any
of the methods we use—I ended up using the first letter of the name plus the 7th letter, if
any. So lineTo becomes l, and quadraticCurveTo becomes qt. I can then use these short names to
actually access the methods—without ever having written out the full name.

This does, of course, not work for properties like fillStyle. You can copy those, but the copies
won’t do anything.

Functions As a Scarce Resource

Functions are hugely useful for factoring out pieces of shared functionality, and thus shortening
code. Unfortunately, the word ‘function’ is 8 characters, and the minimal overhead for a function
definition something like 14 bytes, 20 if you actually want to return something.

Thus, a function has to be really, really useful before it pays off to define it.

The program started off with five functions, which has since been reduced to two. In one of these
places, I have little choice—window.onkeydown only takes function values. I’m using the same
function for onkeydown and onkeyup, which turned out to be more efficient anyway. The checks for
which key is pressed or released are also repeated in both. To check whether an event is a keydown
or a keyup, I used e.type[5], where e is the event object. If this is a keyup event, the type of the
event does not have a 6th character, so that this evaluates to a falsy value.

The other function used is the one called A. This rolls three pieces of functionality into one
(saving me two function keywords). It takes a fillstyle as its first argument, and an optional x, y,
and radius after that. If the optional arguments are provided, it starts by drawing a circle. Then
it sets the fillStyle of the canvas context to the provided style, or—if the style is not a
gradient—it uses it as an index into a string of colors. After this, it calls (the abbreviated
versions of) fill() and beginPath() on the canvas context. Note that, because a canvas context is
specified to start with an empty path, it is safe to start drawing before the first call to
beginPath, and thus beginPath, though it is usually done before one starts drawing, can be made part
of our ‘after-drawing routine’.

This function is used in three different ways. Obviously, it is used to draw colored circles (the
game contains a lot of circles). But code that has drawn a path in some other way (the
ground blocks) can also call it to just assign a fillStyle and fill the path. Finally, code that
just wants to set the fillStyle can use it for that—as long as no path is in the process of being
drawn. Now that‘s reusability. The program uses this function in ten different places.

The World

The game world is divided (along the x axis) into 50-pixel-wide units. When starting a game (or
at game-over time), an array is initialized containing a randomized height-map. The gaps work mostly
the same as the other positions, their height is just off the bottom of the canvas. The generating
algorithm takes some care to not introduce gaps of more than one unit, since those would be
unjumpable. This heightmap array (plus the player’s position, speed, and a time counter for
animation) represents pretty much the whole game state.

So how does the game know where the coins are, if it is not explicitly keeping state for them?
Every block whose random height is divisible by 6 gets a coin, and when the player collects the
coin, .1 is subtracted from the height, and the coin no longer shows up.

Apart from block height, block’s x-coordinates can also be used to add distinctive features.
Every third block gets a decorative tree, if it is visible. If it is invisible, it gets a (stylized)
Piranha Plant. Every seventh block is purple/sinky. This produces a relatively nice random world,
without requiring involved data structures or lots of code.

Physics

The ‘physics’ in this game are coded in an entirely ad-hoc and special-cased way. Player movement
needs to be restricted in two ways—you can’t walk through the sides of blocks, and you shouldn’t
fall through the top. The first is handled by simply cancelling horizontal movement whenever it
would take the player more than nine (the highers 1-byte number…) pixels below the ground, and the
second is simply a direct check against the height array. If the player is below or on the ground,
his y position is set to ground level, and his vertical speed is set to zero, unless the up arrow is
pressed, in which case it is set to minus ten (minus is up). In the other case, where the player is
above the ground, one is added to the vertical speed, creating a gravity effect.

Collision detection is also handled case-by-case. The most involved case is collision with the
plants, which takes some 20 characters. The ‘is the player near the middle of this block’ part of
the test is reused to determine whether a coin is being picked up.

CODE

Below follows a somewhat expanded, formatted, lightly commented version of the code. The interval code
was made a function (it is a string the compressed version) to conveniently allow newlines inside of
it.

canvas=document.body.children[0];
screen_height=time=150;
last_height=screen_width=canvas.width=800;
unit=dead=50;
heights=[];

// The abbreviation loop, initializing the variabled needed by the key-handlers on the side.
for(prop in context=canvas.getContext('2d'))
  context[prop[jump=speed_x=speed_y=0]+(prop[6]||'')]=context[prop];

setInterval(function(){
  if(dead)
    // initialize the player position, score, and heightmap
    for(x=405,i=y=score=0;i<1e4;)
      // (screen_width is reused as the off-the-screen height of gap blocks)

      // a block can be a gap if its index is <9, or if the last block was no gap. after this test,
      // a random number is compared to .3 to determine whether an actual gap is generated, or a
      // regular random height.
      last_height=heights[i++]=
        i<9|last_height9?0:speed_x;
  // compute final player height index, and ground level under it
  ground=heights[player_index=x/unit|0];
  // adjust y and speed_y based on whether we are on the ground or not
  speed_y=y9&scroll_pos<41;

      // ta for translate. move to start of block to make other drawing commands shorter
      ta(unit-scroll_pos,0);
      // cL for createLinearGradient, for the ground/grass gradient
      gradient=cL(0,height=heights[height_index],0,height+9);
      // if height is divisible by 6, there's a coin here. draw it. if the player is standing on the
      // ground, in the middle of this unit, pick up the coin
      height%6||(A(2,25,height-7,5),y^ground||player_in_middle&&(heights[height_index]-=.1,score++));

      // abbreviate, since we need this twice (and use it again to test whether a value passed to A
      // is a gradient)
      gradient.P=gradient.addColorStop;
      // this implements sinky terrain---when the index is divisible by 7, we use a different color,
      // and do the sinking if the player is standing here
      gradient.P(0,height_index%7?'#5e1':(height_index^player_index||y^height||
                                          (y=heights[height_index]+=plant_pos/99),'#a59'));
      // brown earth color for the bottom of the gradient
      gradient.P(1,'#b93');

      // this draws the clouds
      height_index%4&&A(6,time/2%200,9,height_index%2?27:33);

      // draws the terrain block. m is moveTo, qt is quadraticCurveTo, l is lineTo
      m(-6,screen_height);qt(-6,height,3,height);l(47,height);qt(56,height,56,screen_height);A(gradient);

      // draw deco trees or piranha plant (height==screen_width for gap blocks), check for collision
      // with plant
      height_index%3?0:heightplant_pos-9?1:dead);

      // undo block-local translation
      ta(scroll_pos-unit,0)
    }

    // draws the player, using the speed to adjust the position of the iris
    A(6,unit,y-9,11);
    A(5,iris_x=unit+speed_x*.7,iris_y=y-9+speed_y/5,8);
    A(8,iris_x,iris_y,5);

    // color is already dark from eye pupil, draw score with this color
    fx(score+'¢',5,15)
  }

  // check whether the player has fallen off the screen
  dead=y>screen_height?1:dead
},unit);

onkeydown=onkeyup=function(e){
  // if this is a keydown event, new_val gets the value 4, otherwise 0
  new_val=e.type[5]?4:0;
  e=e.keyCode;

  // give jump a truthy value if up was pressed, falsy if up was released
  jump=e^38?jump:new_val;

  // similar for speed_x, inverting new_val if left is pressed
  speed_x=e^37?e^39?speed_x:new_val:-new_val
}

License

Copyright (C) 2012 by Marijn Haverbeke

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

admin

The realistic wildlife fine art paintings and prints of Jacquie Vaux begin with a deep appreciation of wildlife and the environment. Jacquie Vaux grew up in the Pacific Northwest, soon developed an appreciation for nature by observing the native wildlife of the area. Encouraged by her grandmother, she began painting the creatures she loves and has continued for the past four decades. Now a resident of Ft. Collins, CO she is an avid hiker, but always carries her camera, and is ready to capture a nature or wildlife image, to use as a reference for her fine art paintings.

Related Articles

Leave a Reply