Compare commits

..

2 commits

6 changed files with 168 additions and 48 deletions

View file

@ -1,30 +1,21 @@
.PHONY: all clean .PHONY: all clean
IMAGES = $(foreach name, apple wall, public/assets/$(name)32.png)
TILESETS = $(foreach name, hole, public/assets/$(name)-ts.png)
JSON = $(foreach name, snake levelList config, public/assets/$(name).json)
ICON = public/assets/icon32.png public/assets/icon256.png public/favicon.ico ICON = public/assets/icon32.png public/assets/icon256.png public/favicon.ico
APPLE = public/assets/apple32.png
WALL = public/assets/wall32.png
SNAKE = public/assets/snake.json
LEVEL_LIST = public/assets/levelList.json
CONFIG = public/assets/config.json
CSS = public/css/snek.css CSS = public/css/snek.css
JS = public/js/snek.js JS = public/js/snek.js
OUTPUT = $(ICON) $(APPLE) $(WALL) $(SNAKE) $(LEVEL_LIST) $(CONFIG) $(CSS) $(JS) OUTPUT = $(IMAGES) $(TILESETS) $(JSON) $(ICON) $(CSS) $(JS)
all: icon apple wall snake levelList config css js all: images tilesets json icon css js
images: $(IMAGES)
tilesets: $(TILESETS)
json: $(JSON)
icon: $(ICON) icon: $(ICON)
apple: $(APPLE)
wall: $(WALL)
snake: $(SNAKE)
levelList: $(LEVEL_LIST)
config: $(CONFIG)
css: $(CSS) css: $(CSS)
js: $(JS) js: $(JS)
public/favicon.ico: assets/icon.jpg public/favicon.ico: assets/icon.jpg
@ -40,6 +31,9 @@ public/assets/%32.png: assets/%.jpg
public/assets/%256.png: assets/%.jpg public/assets/%256.png: assets/%.jpg
convert $^ -resize 256x $@ convert $^ -resize 256x $@
public/assets/%-ts.png: assets/%.png
convert $^ -scale 32x $@
public/assets/%.json: assets/%.json public/assets/%.json: assets/%.json
cp $^ $@ cp $^ $@

BIN
assets/hole.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/hole.xcf Executable file

Binary file not shown.

39
levels/level4.json Normal file
View file

@ -0,0 +1,39 @@
{
"world": [
" o ",
" o ",
" ooooo o ooooo ",
" f ",
" o o ",
" o ooooooooooooo o ",
" o o ",
" o f o ",
" o o o ",
" o o o ",
" o o o ",
" o o o ",
" o o o ",
" o o o ",
" o o ",
"ooof o foooooo oooooof o fooo",
" o o ",
" o o o ",
" o o o ",
" o o o ",
" o o o ",
" o o o ",
" o o o ",
" o f o ",
" o o ",
" o ooooooooooooo o ",
" o o ",
" f ",
" ooooo o ooooo ",
" o ",
" o "
],
"delay": 100,
"snake": [
[16, 4]
]
}

View file

@ -3,18 +3,23 @@ const ProgressBar=require('progress');
const assetSpecs=[ const assetSpecs=[
{ name: 'fruit', filename: 'apple32.png', type: 'image' }, { name: 'fruit', filename: 'apple32.png', type: 'image' },
{ name: 'wall', filename: 'wall32.png', type: 'image' }, { name: 'wall', filename: 'wall32.png', type: 'image' },
{ name: 'hole', filename: 'hole-ts.png', type: 'image' },
{ name: 'snake', filename: 'snake.json', type: 'json' }, { name: 'snake', filename: 'snake.json', type: 'json' },
{ name: 'levelList', filename: 'levelList.json', type: 'json' }, { name: 'levelList', filename: 'levelList.json', type: 'json' },
{ name: 'config', filename: 'config.json', type: 'json' } { name: 'config', filename: 'config.json', type: 'json' }
]; ];
const tasks=[
{ from: 'hole', type: 'tileset', tiles: ['base', 'ul', 'dr', 'dl', 'ur', 'l', 'r', 'd', 'u'], steps: 9 }
];
const cvs=document.createElement('canvas'); const cvs=document.createElement('canvas');
cvs.width=400; cvs.width=400;
cvs.height=50; cvs.height=50;
cvs.classList.add('progressBar'); cvs.classList.add('progressBar');
cvs.classList.add('hiddenBottom'); cvs.classList.add('hiddenBottom');
const bar=new ProgressBar(assetSpecs.length*2); const bar=new ProgressBar(assetSpecs.length*2+tasks.reduce((a, t) => a+t.steps, 0));
bar.addUpdateListener(() => bar.draw(cvs)); bar.addUpdateListener(() => bar.draw(cvs));
bar.draw(cvs); bar.draw(cvs);
@ -27,11 +32,11 @@ bar.addReadyListener(() => {
}); });
//XXX purposefully slow down asset loading //XXX purposefully slow down asset loading
const sleep=(ms) => new Promise(ok => setTimeout(ok, ms)); const sleep=(ms=1000) => new Promise(ok => setTimeout(ok, ms*Math.random()));
const loadAsset=async (asset) => { const loadAsset=async (asset) => {
const response=await fetch('assets/'+asset.filename); const response=await fetch('assets/'+asset.filename);
await sleep(1000*Math.random()); await sleep();
bar.update(); bar.update();
let result; let result;
switch(asset.type) { switch(asset.type) {
@ -43,7 +48,7 @@ const loadAsset=async (asset) => {
result=await createImageBitmap(await response.blob()); result=await createImageBitmap(await response.blob());
break; break;
} }
await sleep(1000*Math.random()); await sleep();
bar.update(); bar.update();
return [asset.name, result]; return [asset.name, result];
}; };
@ -52,17 +57,36 @@ let assets=Object.create(null);
let ready=false; let ready=false;
let readyListeners=[]; let readyListeners=[];
Promise (async () => {
let arr=await Promise
.all( .all(
assetSpecs.map(a => loadAsset(a)) assetSpecs.map(a => loadAsset(a))
).then(arr => { );
arr.forEach(([name, value]) => { arr.forEach(([name, value]) => {
assets[name]=value; assets[name]=value;
}); });
for(let task of tasks) {
const source=assets[task.from];
switch(task.type) {
case 'tileset': {
let asset=assets[task.from]=Object.create(null);
for(let tId in task.tiles) {
const tName=task.tiles[tId];
asset[tName]=await createImageBitmap(source, 0, source.width*tId, source.width, source.width);
await sleep(100);
bar.update();
}
break;
}
}
}
ready=true; ready=true;
readyListeners.forEach(fn => fn.bind(fn)()); readyListeners.forEach(fn => fn.bind(fn)());
readyListeners=null; readyListeners=null;
}); })();
const onReady=(fn) => { const onReady=(fn) => {
if(ready) fn.bind(fn)(); if(ready) fn.bind(fn)();

View file

@ -1,6 +1,4 @@
const [EMPTY, FOOD, WALL, SNAKE]=Array(4).keys(); const [EMPTY, FOOD, WALL, HOLE, HOLE_S, SNAKE]=Array(6).keys();
const ifNaN=(v, r) => isNaN(v)?r:v;
class SnekGame { class SnekGame {
constructor(settings, canvas, rules) { constructor(settings, canvas, rules) {
@ -20,11 +18,12 @@ class SnekGame {
case ' ': return EMPTY; case ' ': return EMPTY;
case 'f': return FOOD; case 'f': return FOOD;
case 'w': return WALL; case 'w': return WALL;
case 'o': return HOLE;
} }
})(); })();
} }
} }
//
// extract the dimensions // extract the dimensions
this.dimensions=[this.world.length, this.world[0].length]; this.dimensions=[this.world.length, this.world[0].length];
@ -51,6 +50,9 @@ class SnekGame {
// add the walls // add the walls
if(settings.walls) settings.walls.forEach(([x, y]) => this.world[x][y]=WALL); if(settings.walls) settings.walls.forEach(([x, y]) => this.world[x][y]=WALL);
// add the holes
if(settings.holes) settings.holes.forEach(([x, y]) => this.world[x][y]=HOLE);
// add the food // add the food
settings.food.forEach(([x, y]) => this.world[x][y]=FOOD); settings.food.forEach(([x, y]) => this.world[x][y]=FOOD);
this.fruits=[...settings.food]; this.fruits=[...settings.food];
@ -62,9 +64,13 @@ class SnekGame {
// get the head and initial direction // get the head and initial direction
this.head=[...settings.snake[0]]; this.head=[...settings.snake[0]];
this.direction=[ if(settings.snake.length>=2) this.direction=[
ifNaN(settings.snake[0][0]-settings.snake[1][0], 1), settings.snake[0][0]-settings.snake[1][0],
ifNaN(settings.snake[0][1]-settings.snake[1][1], 0) settings.snake[0][1]-settings.snake[1][1]
];
else this.direction=[
1,
0
]; ];
this.lastDirection=this.direction this.lastDirection=this.direction
@ -115,18 +121,46 @@ class SnekGame {
// draw our walls // draw our walls
const wall=assets.get('wall'); const wall=assets.get('wall');
for(let x=0; x<this.dimensions[0]; x++) { const hole=assets.get('hole');
for(let y=0; y<this.dimensions[1]; y++) {
switch(this.world[x][y]) { const putTile=(x, y, tile) => this.ctx.drawImage(
case WALL: tile,
this.ctx.drawImage(
wall,
offsetX+cellSize*x, offsetX+cellSize*x,
offsetY+cellSize*y, offsetY+cellSize*y,
cellSize, cellSize,
cellSize cellSize
); );
const checkAdj=(x, y) => {
let adj={};
adj.u=this.world[x][y-1];
adj.d=this.world[x][y+1];
adj.l=(this.world[x-1] || [])[y];
adj.r=(this.world[x+1] || [])[y];
adj.ul=(this.world[x-1] || [])[y-1];
adj.ur=(this.world[x+1] || [])[y-1];
adj.dl=(this.world[x-1] || [])[y+1];
adj.dr=(this.world[x+1] || [])[y+1];
return adj;
};
for(let x=0; x<this.dimensions[0]; x++) {
for(let y=0; y<this.dimensions[1]; y++) {
switch(this.world[x][y]) {
case WALL:
putTile(x, y, wall);
break; break;
case HOLE:
case HOLE_S: {
putTile(x, y, hole.base);
let adj=checkAdj(x, y);
Object
.keys(adj)
.filter(k => adj[k]==HOLE || adj[k]==HOLE_S)
.forEach(k => putTile(x, y, hole[k]));
break;
// technically, this works for all shapes
// however, the tileset only handles convex shapes
}
} }
} }
} }
@ -198,7 +232,13 @@ class SnekGame {
// get our tail out of the way // get our tail out of the way
const tail=this.snake.pop(); const tail=this.snake.pop();
switch(this.world[tail[0]][tail[1]]) {
case HOLE_S:
this.world[tail[0]][tail[1]]=HOLE;
break;
default:
this.world[tail[0]][tail[1]]=EMPTY; this.world[tail[0]][tail[1]]=EMPTY;
}
// check for out of world conditions // check for out of world conditions
if(head[0]<0 || head[0]>=this.dimensions[0] || head[1]<0 || head[1]>=this.dimensions[1]) { if(head[0]<0 || head[0]>=this.dimensions[0] || head[1]<0 || head[1]>=this.dimensions[1]) {
@ -214,8 +254,21 @@ class SnekGame {
// you hit, you die // you hit, you die
case WALL: case WALL:
case SNAKE: case SNAKE:
case HOLE_S:
return this.die(); return this.die();
// if either 3 consecutive segments or the whole snake is on a hole, you die
case HOLE:
if(
this.snake.length==0 ||
this.snake.length==1 &&
this.world[this.snake[0][0]][this.snake[0][1]]==HOLE_S ||
this.snake.length>=2 &&
this.world[this.snake[0][0]][this.snake[0][1]]==HOLE_S &&
this.world[this.snake[1][0]][this.snake[1][1]]==HOLE_S
) return this.die();
break;
// you eat, you don't die // you eat, you don't die
case FOOD: case FOOD:
// re-grow the snake // re-grow the snake
@ -251,7 +304,13 @@ class SnekGame {
} }
// move our head forward // move our head forward
switch(this.world[head[0]][head[1]]) {
case HOLE:
this.world[head[0]][head[1]]=HOLE_S;
break;
default:
this.world[head[0]][head[1]]=SNAKE; this.world[head[0]][head[1]]=SNAKE;
}
this.snake.unshift(head); this.snake.unshift(head);
// automatic speed increase // automatic speed increase
@ -301,6 +360,7 @@ class SnekGame {
} }
handleInputs(inputs) { handleInputs(inputs) {
// change direction if the input is valid
const trySet=(dir) => { const trySet=(dir) => {
if(!dir.every((e, i) => e==this.lastDirection[i] || e==-this.lastDirection[i])) { if(!dir.every((e, i) => e==this.lastDirection[i] || e==-this.lastDirection[i])) {
this.direction=dir; this.direction=dir;
@ -308,6 +368,7 @@ class SnekGame {
} }
} }
// reduce buffer duration
Object Object
.keys(inputs) .keys(inputs)
.forEach(k => { .forEach(k => {
@ -318,11 +379,13 @@ class SnekGame {
else inputs[k]=v; else inputs[k]=v;
}); });
// try all inputs in order and unbuffer them if valid
if(inputs.left && trySet([-1, 0])) return delete inputs.left; if(inputs.left && trySet([-1, 0])) return delete inputs.left;
else if(inputs.right && trySet([ 1, 0])) return delete inputs.right; else if(inputs.right && trySet([ 1, 0])) return delete inputs.right;
else if(inputs.up && trySet([ 0,-1])) return delete inputs.up; else if(inputs.up && trySet([ 0,-1])) return delete inputs.up;
else if(inputs.down && trySet([ 0, 1])) return delete inputs.down; else if(inputs.down && trySet([ 0, 1])) return delete inputs.down;
// buffering might be disabled
if(inputs.clearBuffer) { if(inputs.clearBuffer) {
Object Object
.keys(inputs) .keys(inputs)