The idea of this entry for the JS1K contest was to do the impossible: a 1K remake of the famous WOLF5K that rocked the final edition of the5K. It does not feature guns, evil grins and violence for in WOLF1K there is no room for guns or any form of violence.
You can also try a development version of WOLF1K in which the SHIFT and TAB keys shows a few extras.
Hope you enjoy your stroll in the walls of WOLF1K and don't panic as the 15 rainbow characters swift away to their fate.
Features and non-features
WOLF1K features a 32x32 map ( a 1024 cells grid ) with textured walls colored by orientation ( North, South, East, West ), fog, 3 transparent bitmap graphics in 8x8, 15 rainbow characters steering smoothly across the map, collision detection, probably the most crazy optimization tricks I ever wrote.
Does not feature guns, violence: in WOLF1K, there is no room for guns or any form of violence.
It's the little things
Love is the little things. Here are a few little things I put much love into and you may not notice at first:
- WOLF1K starts with the screen completely white. Suddenly the 15 rainbow charaters sweep away to reveal the maze and the colorful 1k walls.
- The walls are colored by orientation and feature a smooth shine and distance fog.
- The graphics are sharp and clean. The game screen is not covered a soup of blurry graphics like you could expect from such a small production.
- The 15 rainbow characters do not run erraticaly. Yes they follow their course but swift around smoothly.
- When walking into a wall neither do you nor the 15 rainbow characters get stuck. Instead you glide on them to keep a smooth motion.
How does it work ?
The 3D rendering
WOLF1K works exactly like the original Wolfenstein 3D and Wolf 5k. It is using the raycasting technique. The world is built from a uniform grid of walls and corridors. In order to draw the world, a single ray is traced for every column of screen pixels and a vertical slice of wall texture is selected, scaled and colored according to where in the world the ray hits a wall and how far it travels before doing so.
The world
The world of WOLF1K is a regular grid of 32x32 cells. Obviously storing these 1024 cells in one byte or one bit each is not feasible. It would use the 1024 bytes or 128 bytes if stored as bits. It actually is the result of a binary operation applied to the ASCII codes of the source code of WOLF1K plus a mural on each edge of the world.
The graphics
There is 3 transparent bitmaps graphics of 8x8 pixels each: 2 textures for the walls and 1 sprite for the rainbow characters.
The naive approach would be to store a base64 encoded GIF image but it uses way too many bytes and implies to use the drawImage method which produces blurry graphics when upscaling.
Since the textures are monochrome and 8 pixels tall, we really only need 1 byte per column. We have 3 textures, of 8 columns each. That gives us 3x8=24bytes to store the graphics as a string. Since the characters of that string have a charCode that collide with the tokens of the JavaScript packer, we encode it in base64 and it becomes 32bytes long.
Rendering the graphics
The base64 encoded graphics are easily decoded using the builtin method atob()
That way it is possible to draw a column of graphic by checking each of the 8bits of a character and drawing a colorful rectangle for each bit set. This has the added benefit of producing sharp graphics. Clearing the screen, colors, fog and all things shiny.
The colors are all based on HSL where the saturation is fixed, the hue depends on the orientation of the wall or index of the rainbow character and the luminosity depends on the proximity to the player.
Clearing the screen is done by drawing a full height column at 0% luminosity before drawing any slice of wall. How does that fit in 1024 bytes of JavaScript ?
Obviously the code was minified and featured all the micro optimizations in the book and then some more. As a result Google's Closure Compiler and other automated JavaScript optimizers only managed to break the code or make it bigger instead of smaller.
The original code is carefully hand crafted to please the JavaScript packer. In it's minified version, prior packing, it does not contain a single whitespace, exclamation mark, double quote, hash symbol, dollar sign, tilde... and weighs 1370bytes.
Tweaking the code to be packer friendly and using and abusing existing features, methods and variables was key to reach the 1024 bytes mark.
Styling of the page and Canvas element
Initially WOLF1K used the same setting as the Animated Quaternion Julia ? I entered at the JS1K contest, i.e. using the default 300x150 canvas and using its innerHTML to inject a style rule setting every element on the page to full width and height with zero margin and hidden overflow and a black background.
A.innerHTML='<style>*{margin:0;width:100%;height:100%;overflow:hidden;background:#000}</style>';
While very clean are capable of risizing, this cost 96bytes or 9% of the whole 1024bytes available and was used only once in the code. For a while I was reluctant to compromise on this but it was simply too much. When showing a preview of WOLF1K to Andreas Bovens ? and fiddling with the code, he convinced me WOLF1K was fine with a more limited styling.
We tweaked and removed CSS properties one by one and got some pretty good results like the following:
A.innerHTML='<style>*{width:98%;height:98%;background:#000}</style>';
Eventually I got rid of the innerHTML style injection altogether and went for the classic A.width=innerWidth-20 used by other participants of the JS1K contest in combination with A.height=A.width/2. When put in the main loop these assignments allowed for resizing too. Since A.width is used all other the place, the whole thing packs very well and made the styling virtually free.
Functions declarations and shadowing
Function declarations are verbose. They cost a minimum of 13 bytes each and WOLF1K has a total of 4 functions:
- keydown handler
- keyup handler
- main loop
- sorting of the slices & entities
In a 1k contest we simply can't afford to waste 52 bytes, i.e. 5% of the available space, on such a trivial thing.
To make these 4 function declaration pack well, they all use the same signature function(x,y){...} to please the packer.
Actually there is a 5th function shadowed in the keydown handler:
- collision detection
onkeydown=function(x,y)
{
// Flag the key x.which as down
// If called to check a collision detection, x.which is simply undefined and it causes no harm
A[x.which]=1;
// Go on with the collision detection which is harmless for the key handler
// gets the combined X & Y coordinate for the collision detection.
// Note that the argument y is optionnal.
x|=y<<5;
return (x&31)*(x&480)*(_.charCodeAt(x++%73)&19)?1:0
};
Combined rendering
The rendering loop, the last 4 lines of the original source code, combines the depth sort of the columns of wall and the rainbow characters and their rendering.
If the texture coordinate of the current entry of the sorted list is set to zero, a rainbow character is drawn. Otherwise single column of wall is drawn and the X loop, the middle for loop, is collapsed to process and render a single byte of texture instead the all 8 bytes making the 8 columns of each textures.
The packer
I looked at various JavaScript packers over the years and wrote a few myself. The most compact and efficient ones are all based on the search & replace approach. Basically find the substring of code that appear several times and provide the highest gain when replaced by a token. Repeat until there is no more tokens available or such substring.
This time around, @cowboy's packer ? showed the most compact way to combine the token and substring. A single string contains the list of tokens, each followed by the substring of code it replaces. The tokens were non printable character, which simplified the search to the following regular expression: /.([ -~])+/g.
The initial regular expression allowed to use any printable characters. The initial source code of WOLF1K didn't use many of the first printable characters. With a bit of discipline it was possible to push a little in the ASCII table and not use any whitespace, exclamation mark, double quote, hash symbol, dollar sign, or tilde. This allowed for more tokens and the reglar expression became: /.([%-}]+)/g. To gain one more token, I used the fact that the very first character of the token+substrings map could be anything and used a printable characters that isn't used in the original code.
These changes provided 6 more tokens to WOLF1K than the initial implementation.
Also I gained 8 bytes on the unpacking code by using code=code.split(x[0]).join(y) instead of another code=code.replace(RegExp(x[0],'g'),y) which was bigger and would require to escape tokens like the dollar sign.
Being packer-firendly
Knowing that the packer is looking for substrings of code that are repeated, we can craft the code to please it using subtles and otherwise meaningless changes.
In some places, using a coma instead of a semicolon helped increase the redudancy of the code. In other places where doing similar computations, using the exact same variable names then copying some result into other variables lead to big gains.
Here are a few examples of the interresting snippets that were specially crafted to please the packer:
// function signature
function(x,y)
// to compute the combined XY coordinate in the map
;x|=y<<5;
// to get a cell in the map and a slice of texture
.charCodeAt(x++%73)
// to compute the X and Y properties of the ray
%=1;u=a<0?-a:a,F=(a<0?d:1-d)/u;
// get the effect of the keys to move and turn the player
:A[j]-A[j+2];
// 3 out of the 7 for loops starts with
for(n=A.width,i=0;
// sets the color of the texture and the black background
B.fillStyle='hsl('+D[n][2]+'1,99%,
// to clear a column and draw a rectangle of texture
B.fillRect(a,b,u
Original source code before minification
This version is sprinkled with a few comments.
// the usual suspects
// A is also used a keydown map
// B is also used as player/entities collection
A=document.body.children.c;
B=A.getContext('2d');
// the keydown method is also the collision detection with the map
// the map is actually a 32x32 big ( 1k ;) ) square using the charCode & 19 of uncompressed JS code % 73
// NOTE: Make sure to set _ if you want to try the uncompressed version
onkeydown=function(x,y)
{
A[x.which]=1;
x|=y<<5;
return(x&31)*(x&480)*(_.charCodeAt(x++%73)&19)?1:0
};
// clear the keyboard mapping ( aka A )
// set the keyup method
// initial the player and rainbow characters in 1.1, 1.1, facing towards 1.1 radians
for(n=A.width,i=0;n--;B[n]=[1.1,1.1,1.1,A[n]=0])
onkeyup=function(x,y)
{
A[x.which]=0
};
// D is the array of wall slices raycasted + entities on screen
D=[setInterval(function(x,y)
{
// update the width & height of the canvas to fit the window
A.width=innerWidth-20;A.height=A.width/2;
// raycast the walls
for(n=A.width,i=0;n--;D[n]=[t,n,S,8+(S+x*9&8)|(X+y+t*s+t*c)*8&7])
{
w=X=x=B[i][0];v=y=B[i][1];
z=B[i][2];
x|=y<<5;
b=z+n/A.width-.5,
// Y properties of the ray
a=s=Math.sin(b),d=y%=1;
u=a<0?-a:a,F=(a<0?d:1-d)/u;
G=F,r=u;
// X properties of the ray
a=c=Math.cos(b),d=X%=1;
u=a<0?-a:a,F=(a<0?d:1-d)/u;
// loop until the ray hits a wall
for(;onkeydown(x);)
F<G?(t=F,F+=1/u,x+=S=c/u):(t=G,G+=1/r,x+=S=32*s/r)
}
// process the entities
for(n=A.width,i=0;i<15;i++)
{
// move
b=B[i][2];
j=38;
t=i?1:A[j]-A[j+2];
X=B[i][0]+=onkeydown(B[i][0]+Math.cos(b)*t/8,B[i][1])*Math.cos(b)*t/8;
Y=B[i][1]+=onkeydown(B[i][0],B[i][1]+Math.sin(b)*t/8)*Math.sin(b)*t/8;
// turn
j--;
t=i?Math.random()*8-4:A[j]-A[j+2];
B[i][2]=(b-1*t/16+9.42%6.28)-3.14;
// angle relative to the player
b=z-Math.atan2(Y-=v,X-=w);
i&&Math.cos(b)>.5?D[n]=[Math.sqrt(X*X+Y*Y),A.width/2-A.width*b,i,0,++n]:0
}
// sort the array of slices & entities and render them
for(D.sort(function(x,y){return+x[0]-y[0]});n--;)
for(a=D[n][1],b=a/A.width-.5,F=A.width/2/Math.cos(b)/D[n][b=0],c=8,u=v=F/4+1,x=D[n][3],x?B.fillRect(a,b,u=c=1,A.width,B.fillStyle='hsl('+D[n][2]+'1,99%,0%)'):a-=F;c--;a+=F/4)
for(B.fillStyle='hsl('+D[n][2]+'1,99%,'+v+'%)',d=atob('CBF+/p6f9AC9bsP/w/dqvdvb2NvD29sb').charCodeAt(x++%73),y=8,b=A.width/4,b-=F;y--;b+=F/4)
d>>y&1?B.fillRect(a,b,u,v):0
},9)]
Other recent experiments
There are many experiments and projects like WOLF1K to discover other here.
- FRONTFEST MOSCOW It was an honour to be invited to Fronfest Moscow 2017 with the little family to give my first workshop; implementing a Twin-stick shooter using ES6 and Canvas, and to continue my CODE🎙ART series of talks + live coding aiming to inspire new web developer artists. on November 18th, 2017
- VOLTRA VOLTRA: Grinding the Universe, a gritty JavaScript demo, winner of the 1024 bytes demo competition at the Assembly 2017. on August 6th, 2017
- BREATHING EARTH Another take on Nadieh Bremer mesmerizing Breathing Earth visualisation, running at 60fps on a 2D Canvas without libraries or frameworks. on June 26th, 2017
- DEMO REEL AND TINY JAVASCRIPT AT FRONT TRENDS I had the pleasure to speak about creating bite sized audio-visual demos, and LIVE code one at Front Trends 2016 in Warsaw, Poland. on May 19th, 2016
- COTTON CANDY First stab at webGL, in 1k between two nappy changes. It's glitchy and tiny but I quite like this puppy. It ranked #3 at DemoJS. on July 2nd, 2011
- JSPONGY Since Mentor^TBC released Spongy, an amazing 128b intro raymarching a Menger sponge, I wondered how far such effect could be size optimized in JavaScript while keeping complex camera path. The answer: 281 bytes. on October 23rd, 2009
- FIRE Warming up with a 32 bytes fire effect for MSDOS after a few years break from the demoscene for studies and work. on September 30th, 2004
- 256B.HTM The first edition of the 256B.HTM contest allowed 5 entries per author. Mine ranked 1st, 10th, 19th and 29th out of 63. on March 1st, 2002
Let's talk
Don't be shy; get in touch by mail, twitter, github, linkedin or pouet if you have any questions, feedback, speaking, workshop or performance opportunity.