(p=new Image).src='data:;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAQAgMAAAAZudU+AAAADFBMVEUAAABSUlKlKgD///8IGsOpAAAAl0lEQVQI12NgYOBQCg3lP8AAAguaQkPNN7AIAJlqKo6Ob/+wBgCZixYFBlbuF3QBMru6CgvD6sFMiRmhoanpYAWZXqGhUfGMDiAFS0JDQyGiWlmhoa5hwkeAzElA0cA03gIgc0aSo6NrKDPIPim1wMDAULC2jo7CwpAQ7gcgNywKDRUN4VAAMjsVQkNZJoCZCh2hQJU2NgC8WipFK+hqJAAAAABJRU5ErkJggg';l=1;f=c.imageSmoothingEnabled=0;onkeydown=e=>c[b[e=e.which]=1,e&1]=(e&2)-1;onkeyup=e=>b[e=e.which]?c[e&1]=b[e]=0:0;h=20;w=s=32;A=h*w;e=A-w-1;m=[o=h];q=5;r=_=>((q+=q*q|5)%A)/A<.8;i=f=>{for(j=0;j<A;)f(j,j%w,j++/w|0)};$=_=>{i((j,x,y)=>m[j]=x*y&&x<w-1&&y<h-1?r()?r()?1:3:0:2);m[w+1]=4;m[e]=0};setInterval(_=>{i((j,x,y)=>c.drawImage(p,(o&&(x+y)%h<=h-o?2:m[j])*8,0,8,16,x*w,y*w,s,s));c.font='99px monospace';c.fillStyle='#fff';o&&c.fillText(l,w,A-w);o?--o||$(l++):m[e]?o=h:i(j=>a[j]!=f&&(g={3:_=>!m[k=j+w]?1:m[k]==t?m[k=j-1]+m[j+w-1]<1?1:m[k=j+1]+m[j+w+1]<1:m[k]==4&&a[j]==f-1&&(l=1,o=h,q=5),4:_=>c[0]&m[k=j+c[0]*-w]<2||c[1]&m[k=j+c[1]]<2}[t=m[j]])&&g()?m[m[j]=0,a[k]=f,k]=t:0);f++},99)
// predefined global variables:
// ctx - canvas 2D context
// piggybacks (on predefined global variables):
// processed - tile process markers - piggybacked on canvas element
// keyDown - key down flags - piggybacked on body element
// playerVectors - player vectors - piggybacked on canvas 2D context
// tiles
(tiles = new Image).src =
// browsers are resilient to omitting the content type (image/png) and the trailing base-64 paddings (=)
'data:;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAQAgMAAAAZudU+AAAADFBMVEUAAABSUlKlKgD///8IGsOpAAAAl0lEQVQI12NgYOBQCg3lP8AAAguaQkPNN7AIAJlqKo6Ob/+wBgCZixYFBlbuF3QBMru6CgvD6sFMiRmhoanpYAWZXqGhUfGMDiAFS0JDQyGiWlmhoa5hwkeAzElA0cA03gIgc0aSo6NrKDPIPim1wMDAULC2jo7CwpAQ7gcgNywKDRUN4VAAMjsVQkNZJoCZCh2hQJU2NgC8WipFK+hqJAAAAABJRU5ErkJggg';
// current level
level = 1;
// processed frame counter to prevent reprocessing a moved tile
processCount =
// turn off image smoothing to get crispy and chunky pixels
ctx.imageSmoothingEnabled = 0;
onkeydown = event =>
playerVectors[
keyDown[
event = event.which // grab arrow key code (37: left, 38: up, 39: right, 40: down)
] = 1, // mark key as down
event & 1 // vector index based on key (0: y vector, 1: x vector)
] = (event & 2) - 1; // set vector to -1 or 1 depending on key (y vector is inverted)
onkeyup = event =>
keyDown[event = event.which] // if key was down
? playerVectors[event & 1] = // then set corresponding vector to 0
keyDown[event] = 0 // and mark key as not down
: 0;
mapHeight = 20;
mapWidth = tileSize = 32;
mapArea = mapHeight * mapWidth;
exit = mapArea - mapWidth - 1; // inside bottom right wall
map = [
levelTransition = mapHeight // level transition counter (0: run game, >0: level transition)
];
seed = 5; // PRNG (pseudo-random number generator) seed
// randomly returns true/false, with 80% preference for true
preferably = _ => (
(seed += seed * seed | 5) // PRNG
% // modulus to contain random values within range
mapArea // just a large value we already have
) / mapArea < .8;
// calls given callback on every map tile
iterate = callback => {
for (j = 0; j < mapArea;)
callback(j, j % mapWidth, j++ / mapWidth | 0)
};
// starts a level
startLevel = _ => {
// initialize map
iterate(
(j, x, y) =>
map[j] =
x * y // if not top or left edge
&& x < mapWidth - 1 // and not right edge
&& y < mapHeight - 1 // and not bottom edge
// then pick a random non-wall tile
? preferably()
? preferably()
? 1 // dirt - 80% * 20% = 16%
: 3 // boulder - 20% * 20% = 5%
: 0 // empty space - 20%
// else put a wall
: 2 // wall
);
// place player on top left
map[mapWidth + 1] = 4; // player
// place exit
map[exit] = 0 // empty space
};
setInterval(_ => {
// render map
iterate((j, x, y) =>
ctx.drawImage(
tiles, // source image
// source tile x
(levelTransition // if level transition in progress
&& (x + y) % mapHeight <= mapHeight - levelTransition // diagonal blinds
? 2 // wall
: map[j]) * 8, // tile from map
0, // source tile y
8, // source tile width
16, // source tile height
x * mapWidth, // destination tile x
y * mapWidth, // destination tile y
tileSize, // destination tile width
tileSize // destination tile height
)
);
ctx.font = '99px monospace';
ctx.fillStyle = '#fff';
levelTransition && ctx.fillText(
level,
mapWidth,
mapArea - mapWidth
);
levelTransition // if in the process of level transition
? --levelTransition || // then decrement counter
startLevel(level++) // start next level if counter has reached 0
: map[exit] // else if exit is occupied (by player)
? levelTransition = mapHeight // then start level transition
: iterate( // else process map
j =>
processed[j] != processCount // tile hasn't been already processed in this frame (might have been moved into this position)
&& (handler = { // then invoke tile handler
3: _ => // boulder handler
!map[dest = j + mapWidth] // if beneath the boulder is empty
? 1 // move boulder down
: map[dest] == tile // else if resting on another boulder
? map[dest = j - 1] + map[j + mapWidth - 1] < 1 // else if can slide left
? 1 // move boulder left
: map[dest = j + 1] + map[j + mapWidth + 1] < 1 // else if can slide right
: map[dest] == 4 // else if the player is beneath the boulder
&& processed[j] == processCount - 1 // and if the boulder is falling
&& (
level = 1, // restart game
levelTransition = mapHeight, // restart level
seed = 5 // reset seed
), // crush player
4: _ => // player handler
playerVectors[0] & map[dest = j + playerVectors[0] * -mapWidth] < 2 // if the y vector is set and the player can move
|| playerVectors[1] & map[dest = j + playerVectors[1]] < 2 // if the x vector is set and the player can move
}[tile = map[j]]) // get tile and if a tile handler exists
&& handler() // invoke tile handler and if a move should happen
? map[ // then set tile at new position
map[j] = 0, // empty old position
processed[dest] = processCount, // mark tile at new position as processed
dest // destination tile position
] = tile // set tile
: 0
);
processCount++ // increment process count
}, 99) // render and process every 99 ms