added holes to the game
This commit is contained in:
parent
95751be63e
commit
fc50821ff0
5 changed files with 129 additions and 48 deletions
30
Makefile
30
Makefile
|
@ -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
BIN
assets/hole.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
assets/hole.xcf
Executable file
BIN
assets/hole.xcf
Executable file
Binary file not shown.
|
@ -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,18 +57,37 @@ let assets=Object.create(null);
|
||||||
let ready=false;
|
let ready=false;
|
||||||
let readyListeners=[];
|
let readyListeners=[];
|
||||||
|
|
||||||
Promise
|
(async () => {
|
||||||
.all(
|
let arr=await Promise
|
||||||
assetSpecs.map(a => loadAsset(a))
|
.all(
|
||||||
).then(arr => {
|
assetSpecs.map(a => loadAsset(a))
|
||||||
arr.forEach(([name, value]) => {
|
);
|
||||||
assets[name]=value;
|
|
||||||
});
|
arr.forEach(([name, value]) => {
|
||||||
ready=true;
|
assets[name]=value;
|
||||||
readyListeners.forEach(fn => fn.bind(fn)());
|
|
||||||
readyListeners=null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
readyListeners.forEach(fn => fn.bind(fn)());
|
||||||
|
readyListeners=null;
|
||||||
|
})();
|
||||||
|
|
||||||
const onReady=(fn) => {
|
const onReady=(fn) => {
|
||||||
if(ready) fn.bind(fn)();
|
if(ready) fn.bind(fn)();
|
||||||
else readyListeners.push(fn);
|
else readyListeners.push(fn);
|
||||||
|
|
|
@ -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');
|
||||||
|
const hole=assets.get('hole');
|
||||||
|
|
||||||
|
const putTile=(x, y, tile) => this.ctx.drawImage(
|
||||||
|
tile,
|
||||||
|
offsetX+cellSize*x,
|
||||||
|
offsetY+cellSize*y,
|
||||||
|
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 x=0; x<this.dimensions[0]; x++) {
|
||||||
for(let y=0; y<this.dimensions[1]; y++) {
|
for(let y=0; y<this.dimensions[1]; y++) {
|
||||||
switch(this.world[x][y]) {
|
switch(this.world[x][y]) {
|
||||||
case WALL:
|
case WALL:
|
||||||
this.ctx.drawImage(
|
putTile(x, y, wall);
|
||||||
wall,
|
|
||||||
offsetX+cellSize*x,
|
|
||||||
offsetY+cellSize*y,
|
|
||||||
cellSize,
|
|
||||||
cellSize
|
|
||||||
);
|
|
||||||
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();
|
||||||
this.world[tail[0]][tail[1]]=EMPTY;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
this.world[head[0]][head[1]]=SNAKE;
|
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.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)
|
||||||
|
|
Loading…
Reference in a new issue