JS1k 2014
March 2014
2P Games
Welcome again, to the making-of of 2P Games classic and 2P Games ++, my team projects for JS1k 2014.
I've made these games with the awesome code golfer Subzey, who helped me in many code-golfing projects last year
(a JSfiddle clone, a chip8 emulator, a Pastebin clone, a JS packer, a JS-to-unicode converter, ...)
Source code
Commented (5735b)
Minified (1613b)
Packed (1024b)
The idea
Subzey and I code-golfed a minesweeper game in 480b in 2013 and it was really fun and very instructive.
So, for JS1k 2014, I gave ourselves a big challenge: to fit as many 2-player games as possible in 1kb. Who knows, we could maybe fit three, four, five simple games in 1k...?
People have already made awesome 2-player games for JS1k (1, 2, 3, 4, 5, 6, 7, 8, 9), but we've never seen game compilations, and that's why I chose to try that. I chose five board games: Tic-Tac-Toe, Tic-Tac-Toe 3D, Four in a row, Reversi and Checkers.
Then, we tried to implement those games separately, in good old HTML and JS (the boards are made with tables, not drawn on the canvas):
We quickly (and sadly) abandoned the last two games (reversi and checkers) because the first three already needed more than 2kb.
We gathered those 3 games into one, and golfed them as much as possible, until the whole demo could fit in 1kb. This process included reducing the graphics to the strict minimum (just tables, X and O's). It actually went under 750b, and from that point, we had to decide whether to enhance the graphics or add a fourth game. We decided to make 4 games in one (because that would be impressive), and make a 2k version with great graphics.
Besides the graphics, almost all our efforts have been made on the 1k version and its 4 games. Actually, we golfed that demo on three levels!
1st level of golf: JS optimisation
The first goal is indeed to have the shortest JS code possible.
A lot of effort has been made to fit the menu + the four playable games in 1613b.
The games needed a lot of variables, and we had to introduce 23 global vars. Almost all the alphabet, and most of them are heavily reused.
To save bytes and avoid any kind of DOM manipulation, all the elements on the screen are redrawn after each player's move.
Here's the program's structure:
// Draw function
a=function(c,d,e){
// Init HTML (menu)
// When a button of the menu is clicked, the game state is reset,
// and the model m (an array representing the board) is initialized.
h="<center><font face=arial><p><button onclick=m=[];f=2;a(p=g=s=1)>XnO<button onclick=m=[];f=3;a(p=g=s=1)>XnO3D<button onclick=m=[];f=1;a(p=g=s=1)>Find4<button onclick=m=[];f=w=0;m[27]=m[36]=-1;m[28]=m[35]=1;a(p=g=s=1)>Flip</button><p>";
// If a game has been started
if(c){
// write table HTML (the board) according to the model
// ... snip ...
// Write informations under the board (current player, winner, ...)
// ... snip ...
}
// Update page's <body>
b.innerHTML=h;
}
// Show the menu on load
a();
// Onclick (when a player clicks on the board to play)
q=function(c,d,e){
// If the game is not over and the cell is empty
if(g&&!m[c]){
// Current player plays current game and updates the model
// ... snip ...
// Change player
p=-p;
// Redraw everything
a(1);
}
}
The board drawing snippet is very simple, and made of three loops. One for the boards, another one for the lines, and a last one for the cells of each line, depending on the model. "f" represents the current game (0: reversi / 1: find 4 / 2: XnO / 3: XnO 3D).
<P>, <TR> and <TH> elements don't need to be closed.
<TH> elements have the advantage of being bold and centered, but their borders are hidden when they are empty. That's why we use the blank character \xa0 in the empty cells. (i.e. the 0's in the model.)
// Loop on tables, write table HTML
for(i="1113"[f];i--;){
h+="</table><p><table border>";
// Loop on lines
for(j="8633"[f];j--;)
// Write line HTML, loop on cells
for(h+="<tr>",k="8733"[f];k--;)
// Write cell HTML
h+="<th width=20 onclick=q("+[l,7-j,7-k]+") id=t"+l+">"+"X\xa0O"[1+(m[l]=m[l++]||0)];
}
h+="</table><p>";
The rules of each game were reduced to their minimum after a lot of iterations. After each click, the game updates the model and checks for a victory:
Tic-Tac-Toe, Tic-Tac-Toe 3D:
for(i=3;i--;)
for(j=3;j--;)
if(
k=i*9,
l=j*3,
// In the model, the players marks are represented as 1 and -1.
// If 3 or -3 is found in this array representing all the possible alignments,
// it means that a player has won.
~[
m[k+j]+m[k+j+3]+m[k+j+6], // Columns 2D
m[k+l]+m[k+l+1]+m[k+l+2], // Lines 2D
m[k+4]+m[k+0]+m[k+8], // Diagonals 2D
m[k+4]+m[k+2]+m[k+6], // Diagonals 2D
m[l+10]+m[l]+m[l+20], // Lines 3D
m[l+10]+m[l+2]+m[l+18], // Lines 3D
m[i+12]+m[i]+m[i+24], // Columns 3D
m[i+12]+m[i+6]+m[i+18], // Columns 3D
m[13]+m[0]+m[26], // Diagonals 3D
m[13]+m[2]+m[24], // Diagonals 3D
m[13]+m[6]+m[20], // Diagonals 3D
m[13]+m[8]+m[18], // Diagonals 3D
m[l+i]+m[l+i+9]+m[l+i+18] // Same cell in all tables
].indexOf((m[c]=p)*3) // here, we update the model and test the alignments
)
g=0; // game over
Find four:
// If a wrong cell is clicked, apply gravity
for(;35>c&&!m[c+7];c+=7);
// Test if 4 marks are aligned
for(i=6;i--;)
for(j=7;j--;)
if(
k=i*7+j,
~[
j<4&&m[k]+m[k+1]+m[k+2]+m[k+3], // Horizontally
i<3&&m[k]+m[k+7]+m[k+14]+m[k+21], // Vertically
i<3&&j<4&&m[k]+m[k+8]+m[k+16]+m[k+24], // Diagonally 1
i<3&&j>2&&m[k]+m[k+6]+m[k+12]+m[k+18] // Diagonally 2
].indexOf((m[c]=p)*4)
)
g=0;
Reversi:
// Reset cell's playability
s=0;
// For each direction
for(i=2;~i--;){
for(j=2;~j--;){
// Reset that direction's playability
// If the neighbour is the opponent
if(i|j&&(t=0,m[8*(d+i)+e+j]==-p)){
// Loop on the next neighbours in that direction
// If current color is found, stop, good direction
// If an empty cell is found, stop, bad direction
for(
k=d+i,l=e+j;
~k&&k<9&&~l&&l<9&&(m[8*k+l]!=p||!(s=t=1))&&m[8*k+l];
k+=i,l+=j
);
// If this direction is playable
if(t){
// Loop on the opposite neighbours
for(
u=d,v=e;
u!=k||v!=l;
u+=i,v+=j
){
// Toggle them
m[u*8+v]=p;
}
}
}
}
}
// Compute score
w=0;
for(i=64;i--;){
w+=m[i];
}
After each move, all the screen is updated, including the board and the game status (leader, winner, draw, pass, etc...)
// Board not full
if(~m.indexOf(0)){
h+=(
g
?
// Game not over: show next
"XnO"[p+1]+" next<p>"
:
// Game over (not reversi): show winner
"XnO"[-p+1]+" won"
)
// Reversi
if(!f){
// Leader, pass button
h+=(w?w>0?"O>X":"X>O":"O=X")+"<p><button onclick=a(p=-p)>pass";
}
}
// Board full
else{
h+=
f
?
// Not reversi: show winner or draw
(g?"draw":"XnO"[-p+1]+" won")
:
// Reversi: show winner or draw
(w?w>0?"O won":"X won":"draw")
}
All this code was minified with Google Closure Compiler and optimized manually.
2nd level of golf: RegPack optimization
The JS source could be much smaller than that, but when you deal with JS packers, the main rule is not to be short, it is to repeat yourself as much as possible. That's why many snippets of the code are arranged (and sometimes "unoptimized" to have a better result in a JS packer.
3rd level of golf: make the packed code smaller
During the development of this game, RegPack v3 was being developed by Siorki. Subzey and I used this tool up to its limits and helped the author improving it via Github. We also tweaked the regexp in the final result to lose a few extra bytes and fit the whole thing in 1k!
for(_='/** snip **/';g=/[^ -?DHLMOTX[-~]/.exec(_);)with(_.split(g))_=join(shift());eval(_)
The 2K version
The only change in the "++" version is high-quality graphics. Here, the boards and the players marks can get the right shape, colors, borders, texts, which enhances the gaming experience a lot. Here's the code that changed in the "redraw" function:
// Reversi
if(f==0){
h+="<table border>";
for(j=8;j--;){
h+="<tr>";
for(k=8;k--;){
h+="<th width=20 onclick=q("+[l,7-j,7-k]+") id=t"+l+" bgcolor=#5b5>"+["<table bgcolor=0 width=20 height=20 class=i></table>","\xa0","<table bgcolor=#fff width=20 height=20 class=i></table>"][1+(m[l]=m[l++]||0)];
}
}
h+="</table><br>";
}
// 4 in a row
if(f==1){
h+="<table cellspacing=5 bgcolor=#aad>";
for(j=6;j--;){
h+="<tr>";
for(k=7;k--;){
h+="<th width=20 onclick=onclick=q("+[l,7-j,7-k]+") bgcolor=#fff style='border-radius:50%;overflow:hidden'>"+["<table bgcolor=red width=20 height=20 class=i></table>","\xa0","<table bgcolor=yellow width=20 height=20 class=i></table>"][1+(m[l]=m[l++]||0)];
}
}
h+="</table><br>";
}
// Tic Tac Toe
if(f==2){
h+="<table style=border-collapse:collapse>";
for(j=3;j--;){
h+="<tr>";
for(k=3;k--;){
h+="<th width=20 onclick=q("+[l,7-j,7-k]+") style='"+(j?"border-bottom:2px solid;":"")+(k?"border-right:2px solid;":"")+"'>"+"X\xa0O"[1+(m[l]=m[l++]||0)];
}
}
h+="</table><br>";
}
// Tic Tac Toe 3D
if(f==3){
for(i=3;i--;){
h+="<table border style=margin-left:"+(4*(1-i))+"em>";
for(j=3;j--;){
h+="<tr>";
for(k=3;k--;){
h+="<th width=20 onclick=q("+[l,7-j,7-k]+")>"+"X\xa0O"[1+(m[l]=m[l++]||0)];
}
}
h+="</table><br>";
}
}
// Board not full
if(~m.indexOf(0)){
h+=(
g
?
// Game not over: show next
"Next player: <b>"+
[
["<table bgcolor=0 width=20 height=20 class=i></table>","\xa0","<table bgcolor=#fff width=20 height=20 class=i></table>"],
["<table bgcolor=red width=20 height=20 class=i></table>","\xa0","<table bgcolor=yellow width=20 height=20 class=i></table>"],
"X\xa0O",
"X\xa0O"
][f][p+1]+"</b>"
:
// Game over (not reversi): show winner
"<b>"+[
["<table bgcolor=0 width=20 height=20 class=i></table>","\xa0","<table bgcolor=#fff width=20 height=20 class=i></table>"],
["<table bgcolor=red width=20 height=20 class=i></table>","\xa0","<table bgcolor=yellow width=20 height=20 class=i></table>"],
"X\xa0O",
"X\xa0O"
][f][-p+1]+"</b> won!"
)
// Reversi
if(!f){
// Leader, pass button
h+="<p><button onclick=a(p=-p)>Pass";
}
}
// Board full
else{
h+=
f
?
// Not reversi: show winner or draw
(g?"draw":
[
["<table bgcolor=0 width=20 height=20 class=i></table>","\xa0","<table bgcolor=#fff width=20 height=20 class=i></table>"],
["<table bgcolor=red width=20 height=20 class=i></table>","\xa0","<table bgcolor=yellow width=20 height=20 class=i></table>"],
"X\xa0O",
"X\xa0O"
][f][-p+1]
+" won!")
:
// Reversi: show winner or draw
(w?w>0?"<table bgcolor=#fff width=20 height=20 class=i></table> won!":"<table bgcolor=0 width=20 height=20 class=i></table> won!":"Draw!")
}
Maxime and Anton
Floppy dragon
Welcome to the making-of of Floppy Dragon, my individual entry for JS1k 2014.
In February 2014, all the Internet was talking about the game Flappy Bird, so I decided (for fun) to make a mini-game inspired by that, but featuring a dragon instead of a bird, and stalactites in a cave instead of pipes. I also wanted to introduce many cool details such as responsive display, nice animated graphics, random maps, increasing speed and difficulty, textual interface, etc. And, of course, it had to fit in 1k of JavaScript.
Source code
Commented (2876b)
Minified (1507b)
Packed (1024b)
Responsive display

In JS1k entries, a represents a fullscreen canvas, and c represents the 2D context of this canvas. On load, the canvas' width and height are automatically set to the innerWidth and innerHeight of the window. There are many ways to draw into this canvas, but I chose the following approach:
First, I scale the canvas' context with this ratio:
c.scale(a.height/1E3,a.height/1E3);
Then, the game just has to draw on the canvas' context as if it was 1000px high and infinitely long, and the drawing will spread on all the screen.
For the orange background, I simply draw a huge rectangle on the canvas when I start rendering each frame:
// Background
c.fillStyle="#E50";
c.fillRect(0,0,4E4,4E4);
Random map

In the source code of Floppy Dragon, there's a little piece of code that initializes the game. This code is executed on load, but also when the player loses and decides to try again. In this snippet, I generate an array g with 1000 random integers between 0 and 7, stored from index 0 and index 1000:
g=[];
for(o=0;o<1E3;o++)
g[o]=g[o+1E3]=8*Math.random()|1-1;
In the game loop, this array is used as a heightmap to draw the upper and lower walls of the cave, and the obstacles:
A new "step" is drawn every 40px to make the walls look less flat, and an obstacle is drawn every 20 steps. All these points depend on the heightmap values. The obstacles use these values x 100.
To sum up, the walls are just two big polygons that are drawn with a very similar code, the only difference is that they are mirrored and show the obstacles in a complementary way (a 300px gap is kept between each pair of stalactites) In the following code, k represents the horizontal offset of the scene. This rendering is performed at each frame.
// Draw the walls
c.fillStyle="#920";
// Roof
c.beginPath();
c.moveTo(-k,0);
for(o=2E3;o--;)
o%20
?
// Wall
c.lineTo(40*o-k,20+4*g[o])
:
// Obstacle
(c.lineTo(40*o-k,100*g[o]),c.lineTo(40*o-k-4,100*g[o]));
c.lineTo(40*o-k,0);
c.fill();
// Ceiling
c.beginPath();
c.moveTo(-k,2E3);
for(o=2E3;o--;)
o%20
?
// Wall
c.lineTo(40*o-k,980-4*g[o])
:
// Obstacle
(c.lineTo(40*o-k,100*(g[o]+3)),c.lineTo(40*o-k-4,100*(g[o]+3)));
c.lineTo(40*o-k,1E3);
c.fill();
The maps repeats itself every 1000 steps, thanks to this code that goes 4000px back as soon as the game scrolls more than 4000px:
// Loop on the map if we are far enough
d&&!e&&(k+=20,4E4<k&&(k-=4E4));
User interface
The user interface is made of different texts showing the game name, the score, and a start / restart message. These texts are centered and scaled. In the following code, d is set if the game is started, f is the score, and e is set if the game is over.
// Text
c.fillStyle="#fff";
c.font="6em Arial";
d&&c.fillText(f,a.width/(a.height/1E3)/2-30*1,500);
d||c.fillText("#FloppyDragon",a.width/(a.height/1E3)/2-30*11,500);
e&&c.fillText("score",a.width/(a.height/1E3)/2-30*4,400);
c.fillText(e?"restart":!d?"start":"",a.width/(a.height/1E3)/2-30*5,600);
User interface also means controls, and the controls are simple: press any key or click or tap to start, flap, or restart. When those events are fired, the "reset sequence" is executed (if the game is over), or the dragon flaps (if the game is running, or starting). h represents the vertical speed of the dragon, i and j represent its coordinates, and l is the game's speed.
// Controls
ontouchstart=onmousedown=onkeydown=function(){
// If game is over, reset
if(e){
d=e=f=h=0;
g=[];
for(o=0;o<1E3;o++)
g[o]=g[o+1E3]=8*Math.random()|1-1;
i=j=300;
k=20;
l=50;
}
// If game is not over, start the game if it's not started, and jump
else{
h=45;
d=1;
}
};
Talking about framerate, as I explained earlier, the game's speed is increasing. A variable (l) holds the number of milliseconds between each frame. It starts at 50 and is decremented after each obstacle passed, with a lower limit of 20. The rendering function is called recursively with the corresponding timeout:
// Game loop
(n=function(){
// ... snip ...
// Increase score and game speed after each obstacle
!d||e||(k+280)%800||(f++,20<l&&l--);
// ... snip ...
// Loop
setTimeout(n,l)
})()
At each frame, we also apply gravity (i.e. increment the vertical coordinate of the dragon)
// Apply gravity if the game has started
d&&(h-=6);
Finally, we have to detect when the game is lost (when the dragon's head hits the floor or the ceiling or an obstacle. Here's how it is done:
// Game over
if(50>j||950100*g[20*~~(k/800)+20]+250))
e=1;
Draw the dragon
Last but not least, when all the game mechanics are ready, and the code is minified and packed, I have only 360 bytes left to draw the hero of the game: the dragon. I thought I could simply use a unicode character such as U+1F409 or U+1F432. But these models are not very good-looking, and they aren't displayed correctly on all browsers.
I could also use pixel art (like this) or bezier curves (like that) but this would have used too many characters to draw what I had in mind.
So I decided to draw a shape made of tiny lines, and compress it with a home-made technique:
The shape starts at the neck of the dragon. Each point of the shape has coordinates very close to the previous point. My goal was to store the X and Y coordinates of each point in an unique ASCII character. So I stored the X offset in the first 3 bits of each character (range: -4 to +4), and the Y offset in the last 4 bits (range: -8 to +8).
As a result, I was able to represent the dragon's body plus the two different wings shapes in just 196 characters (most of these are invisible).
p="fEFf&{{~_=,;=vviJ.jfVi/.OoyizyhkhEwf74)\n$fwwuvtU`"+(10<h?"iZ[*)yj:*im**y|Ktdww54#5Dy\iz[Kzi[Jiijk[e@1!":"zl]LfU{\lKtBUh{zzU66iigig5\n&iiyz{vfwwiyDfwiiE");
And here is the code that draws the full shape at the good coordinates. Note that the coordinates are inverted when the dragon dies, in order to draw the falling animation.
c.fillStyle="#000";
c.beginPath();
c.moveTo(v=i,w=(j-=h));
for(o in p){
y=8-2*(p.charCodeAt(o)>>4);
z=16-2*(p.charCodeAt(o)&15);
c.lineTo(v+=(e?y:z),w+=(e?-z:y));
}
c.fill();
To finish, a few final optimizations and JScrush helped me fit this game in 1024 bytes, and I submitted it two weeks after the beginning of the contest. It had the honor to be mentioned here and here by Peter van der Zee, who organizes JS1k every year.
Cheers,
Maxime