refactored game to separate tile management, game code and render code (closes #39)
This commit is contained in:
parent
45e7a0d323
commit
3e8eb69329
3 changed files with 665 additions and 480 deletions
329
src/js/renderer.js
Normal file
329
src/js/renderer.js
Normal file
|
@ -0,0 +1,329 @@
|
|||
const assets=require('assets');
|
||||
const config=require('config');
|
||||
const {tiles: T}=require('tiles');
|
||||
|
||||
// declare our tiles
|
||||
let snake,
|
||||
wall, hole,
|
||||
fire, flammable,
|
||||
fruit, superFruit, decayFruit,
|
||||
portalA, portalB, portalC, portalD,
|
||||
key, door,
|
||||
switchTile, spikes;
|
||||
|
||||
// load our tiles
|
||||
assets.onReady(() => {
|
||||
wall=assets.get('wall');
|
||||
hole=assets.get('hole');
|
||||
fire=assets.get('fire');
|
||||
flammable=assets.get('flammable');
|
||||
superFruit=assets.get('superFruit');
|
||||
decayFruit=assets.get('decayFruit');
|
||||
portalA=assets.get('portalA');
|
||||
portalB=assets.get('portalB');
|
||||
portalC=assets.get('portalC');
|
||||
portalD=assets.get('portalD');
|
||||
key=assets.get('key');
|
||||
door=assets.get('door');
|
||||
switchTile=assets.get('switch');
|
||||
spikes=assets.get('spikes');
|
||||
snake=assets.get('snake');
|
||||
fruit=assets.get('fruit');
|
||||
});
|
||||
|
||||
const draw=(game, canvas=game.canvas, ctx=canvas.getContext('2d')) => {
|
||||
// clear the canvas, because it's easier than having to deal with everything
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// get the cell size and offset
|
||||
const cellSize=Math.min(
|
||||
canvas.width/game.dimensions[0],
|
||||
canvas.height/game.dimensions[1]
|
||||
);
|
||||
const offsetX=(canvas.width-cellSize*game.dimensions[0])/2;
|
||||
const offsetY=(canvas.height-cellSize*game.dimensions[1])/2;
|
||||
|
||||
// tile draw functions
|
||||
const putTile=(x, y, tile) => ctx.drawImage(
|
||||
tile,
|
||||
offsetX+cellSize*x,
|
||||
offsetY+cellSize*y,
|
||||
cellSize,
|
||||
cellSize
|
||||
);
|
||||
const putTileAnim=(x, y, tile) => putTile(x, y, tile[
|
||||
Math.floor(Date.now()/1000*60+x+y)%tile.length
|
||||
]);
|
||||
const putTileAnimPercent=(x, y, tile, percent) => putTile(x, y, tile[
|
||||
Math.min(Math.round(percent*tile.length), tile.length-1)
|
||||
]);
|
||||
|
||||
// adjascence check
|
||||
const checkAdj=(x, y) => {
|
||||
let adj={};
|
||||
adj.u=game.world[x][y-1];
|
||||
adj.d=game.world[x][y+1];
|
||||
adj.l=(game.world[x-1] || [])[y];
|
||||
adj.r=(game.world[x+1] || [])[y];
|
||||
adj.ul=(game.world[x-1] || [])[y-1];
|
||||
adj.ur=(game.world[x+1] || [])[y-1];
|
||||
adj.dl=(game.world[x-1] || [])[y+1];
|
||||
adj.dr=(game.world[x+1] || [])[y+1];
|
||||
return adj;
|
||||
};
|
||||
|
||||
// draw a grid/checkerboard if requested
|
||||
if(config.getS('appearance.grid')=='grid') {
|
||||
ctx.strokeStyle='rgba(0, 0, 0, 50%)';
|
||||
ctx.lineCap='square';
|
||||
ctx.lineWidth=1;
|
||||
ctx.beginPath();
|
||||
for(let x=1; x<game.dimensions[0]; x++) {
|
||||
ctx.moveTo(offsetX+x*cellSize, offsetY);
|
||||
ctx.lineTo(offsetX+x*cellSize, canvas.height-offsetY);
|
||||
}
|
||||
for(let y=1; y<game.dimensions[1]; y++) {
|
||||
ctx.moveTo(offsetX, offsetY+y*cellSize);
|
||||
ctx.lineTo(canvas.width-offsetX, offsetY+y*cellSize);
|
||||
}
|
||||
ctx.stroke();
|
||||
} else if(config.getS('appearance.grid')=='checkerboard') {
|
||||
ctx.fillStyle='rgba(0, 0, 0, 10%)';
|
||||
for(let x=0; x<game.dimensions[0]; x++) {
|
||||
for(let y=(x+1)%2; y<game.dimensions[1]; y+=2) {
|
||||
ctx.fillRect(offsetX+x*cellSize, offsetY+y*cellSize, cellSize, cellSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// draw our tiles
|
||||
for(let x=0; x<game.dimensions[0]; x++) {
|
||||
for(let y=0; y<game.dimensions[1]; y++) {
|
||||
switch(game.world[x][y]) {
|
||||
case T.WALL:
|
||||
putTile(x, y, wall);
|
||||
break;
|
||||
|
||||
case T.FIRE:
|
||||
putTileAnim(x, y, fire);
|
||||
break;
|
||||
|
||||
case T.HOLE:
|
||||
case T.HOLE_S: {
|
||||
putTile(x, y, hole.base);
|
||||
let adj=checkAdj(x, y);
|
||||
Object
|
||||
.keys(adj)
|
||||
.filter(k => adj[k]==T.HOLE || adj[k]==T.HOLE_S)
|
||||
.forEach(k => putTile(x, y, hole[k]));
|
||||
} break;
|
||||
|
||||
case T.FLAMMABLE:
|
||||
case T.FLAMMABLE_S:
|
||||
putTile(x, y, flammable);
|
||||
break;
|
||||
|
||||
case T.SUPER_FOOD:
|
||||
putTileAnim(x, y, superFruit);
|
||||
break;
|
||||
|
||||
case T.PORTAL_A:
|
||||
case T.PORTAL_A_S:
|
||||
putTileAnim(x, y, portalA);
|
||||
break;
|
||||
case T.PORTAL_B:
|
||||
case T.PORTAL_B_S:
|
||||
putTileAnim(x, y, portalB);
|
||||
break;
|
||||
case T.PORTAL_C:
|
||||
case T.PORTAL_C_S:
|
||||
putTileAnim(x, y, portalC);
|
||||
break;
|
||||
case T.PORTAL_D:
|
||||
case T.PORTAL_D_S:
|
||||
putTileAnim(x, y, portalD);
|
||||
break;
|
||||
|
||||
case T.KEY:
|
||||
putTile(x, y, key);
|
||||
break;
|
||||
case T.DOOR:
|
||||
putTile(x, y, door);
|
||||
break;
|
||||
|
||||
case T.SWITCH_ON:
|
||||
case T.SWITCH_ON_S:
|
||||
putTile(x, y, switchTile.on);
|
||||
break;
|
||||
case T.SWITCH_OFF:
|
||||
case T.SWITCH_OFF_S:
|
||||
putTile(x, y, switchTile.off);
|
||||
break;
|
||||
|
||||
case T.SPIKES_ON:
|
||||
putTile(x, y, spikes.on);
|
||||
break;
|
||||
case T.SPIKES_OFF:
|
||||
case T.SPIKES_OFF_S:
|
||||
putTile(x, y, spikes.off);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// draw our decaying fruits (they have more information than just XY, so they need to be drawn here
|
||||
game.decayFood.forEach(([x, y, birth]) =>
|
||||
putTileAnimPercent(x, y, decayFruit, (game.playTime-birth)/2000)
|
||||
);
|
||||
|
||||
// draw the lines between portals
|
||||
if(Object.keys(game.portals).length) {
|
||||
ctx.strokeStyle='rgba(128, 128, 128, 20%)';
|
||||
ctx.lineCap='round';
|
||||
ctx.lineWidth=cellSize*.15;
|
||||
const drawTunnel=([xa, ya], [xb, yb]) => {
|
||||
const angle=(Math.floor(Date.now()/10)%360)*(Math.PI/180);
|
||||
for(let i=0; i<=1; i++) {
|
||||
const dx=cellSize/3*Math.cos(angle+i*Math.PI);
|
||||
const dy=cellSize/3*Math.sin(angle+i*Math.PI);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(
|
||||
offsetX+cellSize*(xa+1/2)+dx,
|
||||
offsetY+cellSize*(ya+1/2)+dy
|
||||
);
|
||||
ctx.lineTo(
|
||||
offsetX+cellSize*(xb+1/2)+dx,
|
||||
offsetY+cellSize*(yb+1/2)+dy
|
||||
);
|
||||
ctx.stroke();
|
||||
}
|
||||
};
|
||||
if(game.portals.a && game.portals.b) drawTunnel(game.portals.a, game.portals.b);
|
||||
if(game.portals.c && game.portals.d) drawTunnel(game.portals.c, game.portals.d);
|
||||
}
|
||||
|
||||
// draw our snake (it gets drawn completely differently, so here it goes)
|
||||
{
|
||||
ctx.fillStyle=snake.color;
|
||||
ctx.strokeStyle=snake.color;
|
||||
ctx.lineCap=snake.cap;
|
||||
ctx.lineJoin=snake.join;
|
||||
ctx.lineWidth=cellSize*snake.tailSize;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(
|
||||
offsetX+cellSize*(game.snake[0][0]+1/2),
|
||||
offsetY+cellSize*(game.snake[0][1]+1/2),
|
||||
cellSize/2*snake.headSize,
|
||||
cellSize/2*snake.headSize,
|
||||
0,
|
||||
0,
|
||||
Math.PI*2
|
||||
);
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
game.snake.forEach(([x, y], i, a) => {
|
||||
ctx.lineTo(
|
||||
offsetX+cellSize*(x+1/2),
|
||||
offsetY+cellSize*(y+1/2)
|
||||
);
|
||||
if(i!=0 && Math.hypot(x-a[i-1][0], y-a[i-1][1])>1) {
|
||||
ctx.lineWidth=cellSize*snake.tailWrapSize;
|
||||
} else {
|
||||
ctx.lineWidth=cellSize*snake.tailSize;
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(
|
||||
offsetX+cellSize*(x+1/2),
|
||||
offsetY+cellSize*(y+1/2)
|
||||
);
|
||||
});
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// our fruit has a nice animation to it between .8 and 1.2 scale
|
||||
{
|
||||
const ms=Date.now();
|
||||
const fruitScale=Math.sin(ms/400*Math.PI)*.2+1
|
||||
game.fruits.forEach(([x, y]) => {
|
||||
ctx.drawImage(
|
||||
fruit,
|
||||
offsetX+cellSize*x+(1-fruitScale)*cellSize/2,
|
||||
offsetY+cellSize*y+(1-fruitScale)*cellSize/2,
|
||||
cellSize*fruitScale,
|
||||
cellSize*fruitScale
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// show the timer
|
||||
if(game.rules.winCondition=='time') {
|
||||
if(config.getS('appearance.timer')=='border' || config.getS('appearance.timer')=='both') {
|
||||
let remaining=(game.rules.gameDuration-game.playTime)/game.rules.gameDuration;
|
||||
const w=game.dimensions[0]*cellSize;
|
||||
const h=game.dimensions[1]*cellSize;
|
||||
const p=w*2+h*2;
|
||||
|
||||
const wp=w/p;
|
||||
const hp=h/p;
|
||||
|
||||
const pdst=(st, ed, frac) =>
|
||||
(ed-st)*frac+st;
|
||||
|
||||
ctx.strokeStyle='#930a16';
|
||||
ctx.lineJoin='miter';
|
||||
ctx.lineCap='round';
|
||||
ctx.lineWidth=5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(canvas.width/2, offsetY+2);
|
||||
|
||||
let sp=Math.min(wp/2, remaining);
|
||||
remaining-=sp;
|
||||
ctx.lineTo(pdst(canvas.width/2, w+offsetX-2, sp/wp*2), offsetY+2);
|
||||
if(remaining) {
|
||||
sp=Math.min(hp, remaining);
|
||||
remaining-=sp;
|
||||
ctx.lineTo(w+offsetX-2, pdst(offsetY+2, offsetY+h-2, sp/hp));
|
||||
}
|
||||
if(remaining) {
|
||||
sp=Math.min(wp, remaining);
|
||||
remaining-=sp;
|
||||
ctx.lineTo(pdst(w+offsetX-2, offsetX+2, sp/wp), offsetY+h-2);
|
||||
}
|
||||
if(remaining) {
|
||||
sp=Math.min(hp, remaining);
|
||||
remaining-=sp;
|
||||
ctx.lineTo(offsetX+2, pdst(offsetY+h-2, offsetY+2, sp/hp));
|
||||
}
|
||||
if(remaining) {
|
||||
ctx.lineTo(pdst(offsetX+2, canvas.width/2, remaining/wp*2), offsetY+2);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
if(config.getS('appearance.timer')=='number' || config.getS('appearance.timer')=='both') {
|
||||
let remaining=''+Math.ceil((game.rules.gameDuration-game.playTime)/1000);
|
||||
while(remaining.length<(''+game.rules.gameDuration/1000).length) remaining='0'+remaining;
|
||||
|
||||
ctx.fillStyle='#930a16';
|
||||
ctx.textAlign='center';
|
||||
ctx.textBaseline='middle';
|
||||
ctx.font='4rem "Fira Code"';
|
||||
ctx.fillText(remaining, canvas.width/2, canvas.height/2);
|
||||
}
|
||||
}
|
||||
|
||||
// draw the border around our game area
|
||||
{
|
||||
ctx.fillStyle='black';
|
||||
ctx.fillRect(0, 0, canvas.width, offsetY);
|
||||
ctx.fillRect(0, 0, offsetX, canvas.height);
|
||||
ctx.fillRect(offsetX+cellSize*game.dimensions[0], 0, offsetX, canvas.height);
|
||||
ctx.fillRect(0, offsetY+cellSize*game.dimensions[1], canvas.width, offsetY);
|
||||
}
|
||||
};
|
||||
|
||||
return module.exports={
|
||||
draw
|
||||
};
|
595
src/js/snek.js
595
src/js/snek.js
|
@ -1,13 +1,10 @@
|
|||
const [
|
||||
EMPTY, SNAKE,
|
||||
FOOD, SUPER_FOOD, DECAY_FOOD,
|
||||
WALL,
|
||||
FIRE, FLAMMABLE, FLAMMABLE_S,
|
||||
HOLE, HOLE_S,
|
||||
PORTAL_A, PORTAL_A_S, PORTAL_B, PORTAL_B_S, PORTAL_C, PORTAL_C_S, PORTAL_D, PORTAL_D_S,
|
||||
KEY, DOOR,
|
||||
SWITCH_ON, SWITCH_ON_S, SWITCH_OFF, SWITCH_OFF_S, SPIKES_OFF, SPIKES_OFF_S, SPIKES_ON
|
||||
]=Array(255).keys();
|
||||
const {
|
||||
tiles: T,
|
||||
forChar,
|
||||
getType,
|
||||
isPortal,
|
||||
snakeVersion, nonSnakeVersion
|
||||
}=require('tiles');
|
||||
|
||||
class SnekGame {
|
||||
constructor(settings, canvas, rules) {
|
||||
|
@ -25,27 +22,7 @@ class SnekGame {
|
|||
for(let x=0; x<this.world.length; x++) {
|
||||
this.world[x]=Array(settings.world.length);
|
||||
for(let y=0; y<this.world[x].length; y++) {
|
||||
this.world[x][y]=(() => {
|
||||
switch(settings.world[y][x]) {
|
||||
case ' ': return EMPTY;
|
||||
case 'f': return FOOD;
|
||||
case 'F': return SUPER_FOOD;
|
||||
case 'd': return DECAY_FOOD;
|
||||
case 'w': return WALL;
|
||||
case 'o': return HOLE;
|
||||
case 'i': return FIRE;
|
||||
case 'I': return FLAMMABLE;
|
||||
case 'A': return PORTAL_A;
|
||||
case 'B': return PORTAL_B;
|
||||
case 'C': return PORTAL_C;
|
||||
case 'D': return PORTAL_D;
|
||||
case 'k': return KEY;
|
||||
case 'K': return DOOR;
|
||||
case 's': return SWITCH_OFF;
|
||||
case 'S': return SPIKES_ON;
|
||||
case 't': return SPIKES_OFF;
|
||||
}
|
||||
})();
|
||||
this.world[x][y]=forChar(settings.world[y][x]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,19 +30,19 @@ class SnekGame {
|
|||
this.dimensions=[this.world.length, this.world[0].length];
|
||||
|
||||
// extract the fruits
|
||||
this.fruits=this.getTilesOfType(FOOD);
|
||||
this.fruits=this.getTilesOfType(T.FOOD);
|
||||
|
||||
// extract the decaying fruits
|
||||
this.decayFood=this.getTilesOfType(DECAY_FOOD);
|
||||
this.decayFood=this.getTilesOfType(T.DECAY_FOOD);
|
||||
|
||||
// extract the portals
|
||||
this.portals={};
|
||||
this.world.forEach((l, x) =>
|
||||
l.forEach((c, y) => {
|
||||
if(c==PORTAL_A) this.portals.a=[x, y];
|
||||
if(c==PORTAL_B) this.portals.b=[x, y];
|
||||
if(c==PORTAL_C) this.portals.c=[x, y];
|
||||
if(c==PORTAL_D) this.portals.d=[x, y];
|
||||
if(c==T.PORTAL_A) this.portals.a=[x, y];
|
||||
if(c==T.PORTAL_B) this.portals.b=[x, y];
|
||||
if(c==T.PORTAL_C) this.portals.c=[x, y];
|
||||
if(c==T.PORTAL_D) this.portals.d=[x, y];
|
||||
})
|
||||
);
|
||||
} else { // dimension and objects
|
||||
|
@ -77,29 +54,29 @@ class SnekGame {
|
|||
this.world=Array(settings.dimensions[0]);
|
||||
for(let i=0; i<settings.dimensions[0]; i++) {
|
||||
this.world[i]=Array(settings.dimensions[1]);
|
||||
this.world[i].fill(EMPTY);
|
||||
this.world[i].fill(T.EMPTY);
|
||||
}
|
||||
|
||||
// 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]=T.WALL);
|
||||
|
||||
// add the holes
|
||||
if(settings.holes) settings.holes.forEach(([x, y]) => this.world[x][y]=HOLE);
|
||||
if(settings.holes) settings.holes.forEach(([x, y]) => this.world[x][y]=T.HOLE);
|
||||
|
||||
// add the fires and flammable tiles
|
||||
if(settings.fires) settings.fires.forEach(([x, y]) => this.world[x][y]=FIRE);
|
||||
if(settings.flammable) settings.flammable.forEach(([x, y]) => this.world[x][y]=FLAMMABLE);
|
||||
if(settings.fires) settings.fires.forEach(([x, y]) => this.world[x][y]=T.FIRE);
|
||||
if(settings.flammable) settings.flammable.forEach(([x, y]) => this.world[x][y]=T.FLAMMABLE);
|
||||
|
||||
// add the food
|
||||
settings.food.forEach(([x, y]) => this.world[x][y]=FOOD);
|
||||
settings.food.forEach(([x, y]) => this.world[x][y]=T.FOOD);
|
||||
this.fruits=[...settings.food];
|
||||
|
||||
// add the super food
|
||||
if(settings.superFood) settings.superFood.forEach(([x, y]) => this.world[x][y]=SUPER_FOOD);
|
||||
if(settings.superFood) settings.superFood.forEach(([x, y]) => this.world[x][y]=T.SUPER_FOOD);
|
||||
|
||||
// add the decaying food
|
||||
if(settings.decayFood) {
|
||||
settings.decayFood.forEach(([x, y]) => this.world[x][y]=DECAY_FOOD);
|
||||
settings.decayFood.forEach(([x, y]) => this.world[x][y]=T.DECAY_FOOD);
|
||||
this.decayFood=settings.decayFood.map(([x, y]) => [x, y, 0]);
|
||||
} else {
|
||||
this.decayFood=[];
|
||||
|
@ -107,31 +84,31 @@ class SnekGame {
|
|||
|
||||
// add the portals
|
||||
if(settings.portals) {
|
||||
if(settings.portals.a) this.world[settings.portals.a[0]][settings.portals.a[1]]=PORTAL_A;
|
||||
if(settings.portals.b) this.world[settings.portals.b[0]][settings.portals.b[1]]=PORTAL_B;
|
||||
if(settings.portals.c) this.world[settings.portals.c[0]][settings.portals.c[1]]=PORTAL_C;
|
||||
if(settings.portals.d) this.world[settings.portals.d[0]][settings.portals.d[1]]=PORTAL_D;
|
||||
if(settings.portals.a) this.world[settings.portals.a[0]][settings.portals.a[1]]=T.PORTAL_A;
|
||||
if(settings.portals.b) this.world[settings.portals.b[0]][settings.portals.b[1]]=T.PORTAL_B;
|
||||
if(settings.portals.c) this.world[settings.portals.c[0]][settings.portals.c[1]]=T.PORTAL_C;
|
||||
if(settings.portals.d) this.world[settings.portals.d[0]][settings.portals.d[1]]=T.PORTAL_D;
|
||||
this.portals={...settings.portals};
|
||||
} else {
|
||||
this.portals={};
|
||||
}
|
||||
|
||||
// add the keys
|
||||
if(settings.keys) settings.keys.forEach(([x, y]) => this.world[x][y]=KEY);
|
||||
if(settings.keys) settings.keys.forEach(([x, y]) => this.world[x][y]=T.KEY);
|
||||
|
||||
// add the doors
|
||||
if(settings.doors) settings.doors.forEach(([x, y]) => this.world[x][y]=DOOR);
|
||||
if(settings.doors) settings.doors.forEach(([x, y]) => this.world[x][y]=T.DOOR);
|
||||
|
||||
// add the switches
|
||||
if(settings.switches) settings.switches.forEach(([x, y]) => this.world[x][y]=SWITCH_OFF);
|
||||
if(settings.switches) settings.switches.forEach(([x, y]) => this.world[x][y]=T.SWITCH_OFF);
|
||||
|
||||
// add the spikes
|
||||
if(settings.spikesOn) settings.spikesOn.forEach(([x, y]) => this.world[x][y]=SPIKES_ON);
|
||||
if(settings.spikesOff) settings.spikesOff.forEach(([x, y]) => this.world[x][y]=SPIKES_OFF);
|
||||
if(settings.spikesOn) settings.spikesOn.forEach(([x, y]) => this.world[x][y]=T.SPIKES_ON);
|
||||
if(settings.spikesOff) settings.spikesOff.forEach(([x, y]) => this.world[x][y]=T.SPIKES_OFF);
|
||||
}
|
||||
|
||||
// add the snake to the world
|
||||
settings.snake.forEach(([x, y]) => this.world[x][y]=SNAKE);
|
||||
settings.snake.forEach(([x, y]) => this.world[x][y]=T.SNAKE);
|
||||
|
||||
|
||||
// get the head and initial direction
|
||||
|
@ -189,6 +166,19 @@ class SnekGame {
|
|||
return Math.round(1000/this.delay);
|
||||
}
|
||||
|
||||
getTile(x, y) {
|
||||
return (this.world[x]||[])[y];
|
||||
}
|
||||
getTileA([x, y]) {
|
||||
return (this.world[x]||[])[y];
|
||||
}
|
||||
putTile(x, y, t) {
|
||||
this.world[x][y]=t;
|
||||
}
|
||||
putTileA([x, y], t) {
|
||||
this.world[x][y]=t;
|
||||
}
|
||||
|
||||
getTilesOfType(type) {
|
||||
return this
|
||||
.world
|
||||
|
@ -201,310 +191,16 @@ class SnekGame {
|
|||
)
|
||||
).flat();
|
||||
}
|
||||
replaceTilesOfType(type, newType) {
|
||||
this.world.forEach(l =>
|
||||
l.forEach((t, i) => {
|
||||
if(t==type) l[i]=newType;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
draw() {
|
||||
const assets=require('assets');
|
||||
const config=require('config');
|
||||
|
||||
// clear the canvas, because it's easier than having to deal with everything
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// get the cell size and offset
|
||||
const cellSize=Math.min(
|
||||
this.canvas.width/this.dimensions[0],
|
||||
this.canvas.height/this.dimensions[1]
|
||||
);
|
||||
const offsetX=(this.canvas.width-cellSize*this.dimensions[0])/2;
|
||||
const offsetY=(this.canvas.height-cellSize*this.dimensions[1])/2;
|
||||
|
||||
// draw a grid/checkerboard if requested
|
||||
if(config.getS('appearance.grid')=='grid') {
|
||||
this.ctx.strokeStyle='rgba(0, 0, 0, 50%)';
|
||||
this.ctx.lineCap='square';
|
||||
this.ctx.lineWidth=1;
|
||||
this.ctx.beginPath();
|
||||
for(let x=1; x<this.dimensions[0]; x++) {
|
||||
this.ctx.moveTo(offsetX+x*cellSize, offsetY);
|
||||
this.ctx.lineTo(offsetX+x*cellSize, this.canvas.height-offsetY);
|
||||
}
|
||||
for(let y=1; y<this.dimensions[1]; y++) {
|
||||
this.ctx.moveTo(offsetX, offsetY+y*cellSize);
|
||||
this.ctx.lineTo(this.canvas.width-offsetX, offsetY+y*cellSize);
|
||||
}
|
||||
this.ctx.stroke();
|
||||
} else if(config.getS('appearance.grid')=='checkerboard') {
|
||||
this.ctx.fillStyle='rgba(0, 0, 0, 10%)';
|
||||
for(let x=0; x<this.dimensions[0]; x++) {
|
||||
for(let y=(x+1)%2; y<this.dimensions[1]; y+=2) {
|
||||
this.ctx.fillRect(offsetX+x*cellSize, offsetY+y*cellSize, cellSize, cellSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// draw our tiles
|
||||
const wall=assets.get('wall');
|
||||
const hole=assets.get('hole');
|
||||
const fire=assets.get('fire');
|
||||
const flammable=assets.get('flammable');
|
||||
const superFruit=assets.get('superFruit');
|
||||
const decayFruit=assets.get('decayFruit');
|
||||
const portalA=assets.get('portalA');
|
||||
const portalB=assets.get('portalB');
|
||||
const portalC=assets.get('portalC');
|
||||
const portalD=assets.get('portalD');
|
||||
const key=assets.get('key');
|
||||
const door=assets.get('door');
|
||||
const switchTile=assets.get('switch');
|
||||
const spikes=assets.get('spikes');
|
||||
const putTile=(x, y, tile) => this.ctx.drawImage(
|
||||
tile,
|
||||
offsetX+cellSize*x,
|
||||
offsetY+cellSize*y,
|
||||
cellSize,
|
||||
cellSize
|
||||
);
|
||||
const putTileAnim=(x, y, tile) => putTile(x, y, tile[
|
||||
Math.floor(Date.now()/1000*60+x+y)%tile.length
|
||||
]);
|
||||
const putTileAnimPercent=(x, y, tile, percent) => putTile(x, y, tile[
|
||||
Math.min(Math.round(percent*tile.length), tile.length-1)
|
||||
]);
|
||||
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;
|
||||
|
||||
case FIRE:
|
||||
putTileAnim(x, y, fire);
|
||||
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
|
||||
}
|
||||
|
||||
case FLAMMABLE:
|
||||
case FLAMMABLE_S:
|
||||
putTile(x, y, flammable);
|
||||
break;
|
||||
|
||||
case SUPER_FOOD:
|
||||
putTileAnim(x, y, superFruit);
|
||||
break;
|
||||
|
||||
case PORTAL_A:
|
||||
case PORTAL_A_S:
|
||||
putTileAnim(x, y, portalA);
|
||||
break;
|
||||
case PORTAL_B:
|
||||
case PORTAL_B_S:
|
||||
putTileAnim(x, y, portalB);
|
||||
break;
|
||||
case PORTAL_C:
|
||||
case PORTAL_C_S:
|
||||
putTileAnim(x, y, portalC);
|
||||
break;
|
||||
case PORTAL_D:
|
||||
case PORTAL_D_S:
|
||||
putTileAnim(x, y, portalD);
|
||||
break;
|
||||
|
||||
case KEY:
|
||||
putTile(x, y, key);
|
||||
break;
|
||||
case DOOR:
|
||||
putTile(x, y, door);
|
||||
break;
|
||||
|
||||
case SWITCH_ON:
|
||||
case SWITCH_ON_S:
|
||||
putTile(x, y, switchTile.on);
|
||||
break;
|
||||
case SWITCH_OFF:
|
||||
case SWITCH_OFF_S:
|
||||
putTile(x, y, switchTile.off);
|
||||
break;
|
||||
|
||||
case SPIKES_ON:
|
||||
putTile(x, y, spikes.on);
|
||||
break;
|
||||
case SPIKES_OFF:
|
||||
case SPIKES_OFF_S:
|
||||
putTile(x, y, spikes.off);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// draw our decaying fruits (they have more information than just XY, so they need to be drawn here
|
||||
this.decayFood.forEach(([x, y, birth]) =>
|
||||
putTileAnimPercent(x, y, decayFruit, (this.playTime-birth)/2000)
|
||||
);
|
||||
|
||||
// draw the lines between portals
|
||||
if(Object.keys(this.portals).length) {
|
||||
this.ctx.strokeStyle='rgba(128, 128, 128, 20%)';
|
||||
this.ctx.lineCap='round';
|
||||
this.ctx.lineWidth=cellSize*.15;
|
||||
const drawTunnel=([xa, ya], [xb, yb]) => {
|
||||
const angle=(Math.floor(Date.now()/10)%360)*(Math.PI/180);
|
||||
for(let i=0; i<=1; i++) {
|
||||
const dx=cellSize/3*Math.cos(angle+i*Math.PI);
|
||||
const dy=cellSize/3*Math.sin(angle+i*Math.PI);
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(
|
||||
offsetX+cellSize*(xa+1/2)+dx,
|
||||
offsetY+cellSize*(ya+1/2)+dy
|
||||
);
|
||||
this.ctx.lineTo(
|
||||
offsetX+cellSize*(xb+1/2)+dx,
|
||||
offsetY+cellSize*(yb+1/2)+dy
|
||||
);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
};
|
||||
if(this.portals.a && this.portals.b) drawTunnel(this.portals.a, this.portals.b);
|
||||
if(this.portals.c && this.portals.d) drawTunnel(this.portals.c, this.portals.d);
|
||||
}
|
||||
|
||||
// draw our snake (it gets drawn completely differently, so here it goes)
|
||||
const snake=assets.get('snake');
|
||||
this.ctx.fillStyle=snake.color;
|
||||
this.ctx.strokeStyle=snake.color;
|
||||
this.ctx.lineCap=snake.cap;
|
||||
this.ctx.lineJoin=snake.join;
|
||||
this.ctx.lineWidth=cellSize*snake.tailSize;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.ellipse(
|
||||
offsetX+cellSize*(this.snake[0][0]+1/2),
|
||||
offsetY+cellSize*(this.snake[0][1]+1/2),
|
||||
cellSize/2*snake.headSize,
|
||||
cellSize/2*snake.headSize,
|
||||
0,
|
||||
0,
|
||||
Math.PI*2
|
||||
);
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.snake.forEach(([x, y], i, a) => {
|
||||
this.ctx.lineTo(
|
||||
offsetX+cellSize*(x+1/2),
|
||||
offsetY+cellSize*(y+1/2)
|
||||
);
|
||||
if(i!=0 && Math.hypot(x-a[i-1][0], y-a[i-1][1])>1) {
|
||||
this.ctx.lineWidth=cellSize*snake.tailWrapSize;
|
||||
} else {
|
||||
this.ctx.lineWidth=cellSize*snake.tailSize;
|
||||
}
|
||||
this.ctx.stroke();
|
||||
this.ctx.beginPath()
|
||||
this.ctx.moveTo(
|
||||
offsetX+cellSize*(x+1/2),
|
||||
offsetY+cellSize*(y+1/2)
|
||||
);
|
||||
});
|
||||
this.ctx.stroke();
|
||||
|
||||
// our fruit has a nice animation to it between .8 and 1.2 scale
|
||||
const ms=Date.now();
|
||||
const fruitScale=Math.sin(ms/400*Math.PI)*.2+1
|
||||
const fruit=assets.get('fruit');
|
||||
this.fruits.forEach(([x, y]) => {
|
||||
this.ctx.drawImage(
|
||||
fruit,
|
||||
offsetX+cellSize*x+(1-fruitScale)*cellSize/2,
|
||||
offsetY+cellSize*y+(1-fruitScale)*cellSize/2,
|
||||
cellSize*fruitScale,
|
||||
cellSize*fruitScale
|
||||
);
|
||||
});
|
||||
|
||||
// show the timer
|
||||
if(this.rules.winCondition=='time') {
|
||||
if(config.getS('appearance.timer')=='border' || config.getS('appearance.timer')=='both') {
|
||||
let remaining=(this.rules.gameDuration-this.playTime)/this.rules.gameDuration;
|
||||
const w=this.dimensions[0]*cellSize;
|
||||
const h=this.dimensions[1]*cellSize;
|
||||
const p=w*2+h*2;
|
||||
|
||||
const wp=w/p;
|
||||
const hp=h/p;
|
||||
|
||||
const pdst=(st, ed, frac) =>
|
||||
(ed-st)*frac+st;
|
||||
|
||||
this.ctx.strokeStyle='#930a16';
|
||||
this.ctx.lineJoin='miter';
|
||||
this.ctx.lineCap='round';
|
||||
this.ctx.lineWidth=5;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(this.canvas.width/2, offsetY+2);
|
||||
|
||||
let sp=Math.min(wp/2, remaining);
|
||||
remaining-=sp;
|
||||
this.ctx.lineTo(pdst(this.canvas.width/2, w+offsetX-2, sp/wp*2), offsetY+2);
|
||||
if(remaining) {
|
||||
sp=Math.min(hp, remaining);
|
||||
remaining-=sp;
|
||||
this.ctx.lineTo(w+offsetX-2, pdst(offsetY+2, offsetY+h-2, sp/hp));
|
||||
}
|
||||
if(remaining) {
|
||||
sp=Math.min(wp, remaining);
|
||||
remaining-=sp;
|
||||
this.ctx.lineTo(pdst(w+offsetX-2, offsetX+2, sp/wp), offsetY+h-2);
|
||||
}
|
||||
if(remaining) {
|
||||
sp=Math.min(hp, remaining);
|
||||
remaining-=sp;
|
||||
this.ctx.lineTo(offsetX+2, pdst(offsetY+h-2, offsetY+2, sp/hp));
|
||||
}
|
||||
if(remaining) {
|
||||
this.ctx.lineTo(pdst(offsetX+2, this.canvas.width/2, remaining/wp*2), offsetY+2);
|
||||
}
|
||||
this.ctx.stroke();
|
||||
}
|
||||
if(config.getS('appearance.timer')=='number' || config.getS('appearance.timer')=='both') {
|
||||
let remaining=''+Math.ceil((this.rules.gameDuration-this.playTime)/1000);
|
||||
while(remaining.length<(''+this.rules.gameDuration/1000).length) remaining='0'+remaining;
|
||||
|
||||
this.ctx.fillStyle='#930a16';
|
||||
this.ctx.textAlign='center';
|
||||
this.ctx.textBaseline='middle';
|
||||
this.ctx.font='4rem "Fira Code"';
|
||||
this.ctx.fillText(remaining, this.canvas.width/2, this.canvas.height/2);
|
||||
}
|
||||
}
|
||||
|
||||
// draw the border around our game area
|
||||
this.ctx.fillStyle='black';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, offsetY);
|
||||
this.ctx.fillRect(0, 0, offsetX, this.canvas.height);
|
||||
this.ctx.fillRect(offsetX+cellSize*this.dimensions[0], 0, offsetX, this.canvas.height);
|
||||
this.ctx.fillRect(0, offsetY+cellSize*this.dimensions[1], this.canvas.width, offsetY);
|
||||
require('renderer').draw(this, this.canvas, this.ctx);
|
||||
}
|
||||
|
||||
step() {
|
||||
|
@ -513,12 +209,12 @@ class SnekGame {
|
|||
|
||||
// compute our new head
|
||||
let head;
|
||||
if(!this.portaled && [PORTAL_A_S, PORTAL_B_S, PORTAL_C_S, PORTAL_D_S].includes(this.world[this.snake[0][0]][this.snake[0][1]])) {
|
||||
const tile=this.world[this.snake[0][0]][this.snake[0][1]];
|
||||
if(tile==PORTAL_A_S) head=this.portals.b;
|
||||
if(tile==PORTAL_B_S) head=this.portals.a;
|
||||
if(tile==PORTAL_C_S) head=this.portals.d;
|
||||
if(tile==PORTAL_D_S) head=this.portals.c;
|
||||
if(!this.portaled && isPortal(this.getTileA(this.snake[0]))) {
|
||||
const tile=this.getTileA(this.snake[0]);
|
||||
if(tile==T.PORTAL_A_S) head=this.portals.b;
|
||||
if(tile==T.PORTAL_B_S) head=this.portals.a;
|
||||
if(tile==T.PORTAL_C_S) head=this.portals.d;
|
||||
if(tile==T.PORTAL_D_S) head=this.portals.c;
|
||||
this.portaled=true;
|
||||
} else {
|
||||
head=[
|
||||
|
@ -530,37 +226,7 @@ class SnekGame {
|
|||
|
||||
// get our tail out of the way
|
||||
const tail=this.snake.pop();
|
||||
switch(this.world[tail[0]][tail[1]]) {
|
||||
case HOLE_S:
|
||||
this.world[tail[0]][tail[1]]=HOLE;
|
||||
break;
|
||||
case FLAMMABLE_S:
|
||||
this.world[tail[0]][tail[1]]=FLAMMABLE;
|
||||
break;
|
||||
case PORTAL_A_S:
|
||||
this.world[tail[0]][tail[1]]=PORTAL_A;
|
||||
break;
|
||||
case PORTAL_B_S:
|
||||
this.world[tail[0]][tail[1]]=PORTAL_B;
|
||||
break;
|
||||
case PORTAL_C_S:
|
||||
this.world[tail[0]][tail[1]]=PORTAL_C;
|
||||
break;
|
||||
case PORTAL_D_S:
|
||||
this.world[tail[0]][tail[1]]=PORTAL_D;
|
||||
break;
|
||||
case SWITCH_ON_S:
|
||||
this.world[tail[0]][tail[1]]=SWITCH_ON;
|
||||
break;
|
||||
case SWITCH_OFF_S:
|
||||
this.world[tail[0]][tail[1]]=SWITCH_OFF;
|
||||
break;
|
||||
case SPIKES_OFF_S:
|
||||
this.world[tail[0]][tail[1]]=SPIKES_OFF;
|
||||
break;
|
||||
default:
|
||||
this.world[tail[0]][tail[1]]=EMPTY;
|
||||
}
|
||||
this.putTileA(tail, nonSnakeVersion(this.getTileA(tail)));
|
||||
|
||||
// check for out of world conditions
|
||||
if(head[0]<0 || head[0]>=this.dimensions[0] || head[1]<0 || head[1]>=this.dimensions[1]) {
|
||||
|
@ -572,74 +238,72 @@ class SnekGame {
|
|||
}
|
||||
}
|
||||
|
||||
switch(this.world[head[0]][head[1]]) {
|
||||
let tile=this.getTileA(head);
|
||||
switch(getType(tile)) {
|
||||
// you hit, you die
|
||||
case WALL: return this.die("thought walls were edible", "hit a wall");
|
||||
case FIRE: return this.die("burned to a crisp", "hit fire");
|
||||
case DOOR: return this.die("forgot to OPEN the door", "hit a door");
|
||||
case SPIKES_ON: return this.die("thought they were a girl's drink in a nightclub", "hit spikes");
|
||||
case 'wall':
|
||||
switch(tile) {
|
||||
case T.WALL: return this.die("thought walls were edible", "hit a wall");
|
||||
case T.FIRE: return this.die("burned to a crisp", "hit fire");
|
||||
case T.DOOR: return this.die("forgot to OPEN the door", "hit a door");
|
||||
case T.SPIKES_ON: return this.die("thought they were a girl's drink in a nightclub", "hit spikes");
|
||||
}
|
||||
|
||||
// congratilations, you played yourself!
|
||||
case SNAKE:
|
||||
case HOLE_S:
|
||||
case FLAMMABLE_S:
|
||||
case PORTAL_A_S:
|
||||
case PORTAL_B_S:
|
||||
case PORTAL_C_S:
|
||||
case PORTAL_D_S:
|
||||
case SWITCH_OFF_S:
|
||||
case SWITCH_ON_S:
|
||||
case SPIKES_OFF_S:
|
||||
case 'snake':
|
||||
return this.die("achieved every dog's dream", "ate their own tail");
|
||||
|
||||
// if either 3 consecutive segments or the whole snake is on a hole, you die
|
||||
case HOLE:
|
||||
case 'hole':
|
||||
if(
|
||||
this.snake.length==0 ||
|
||||
this.snake.length==1 &&
|
||||
this.world[this.snake[0][0]][this.snake[0][1]]==HOLE_S ||
|
||||
this.getTileA(this.snake[0])==T.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
|
||||
this.getTileA(this.snake[0])==T.HOLE_S &&
|
||||
this.getTileA(this.snake[1])==T.HOLE_S
|
||||
) return this.die("fell harder than their grades", "fell in a hole");
|
||||
break;
|
||||
|
||||
// you eat, you get a massive score boost
|
||||
case SUPER_FOOD:
|
||||
this.score+=10;
|
||||
break;
|
||||
case 'bonus':
|
||||
this.putTileA(head, T.EMPTY);
|
||||
switch(tile) {
|
||||
case T.SUPER_FOOD:
|
||||
this.score+=10;
|
||||
|
||||
// you eat, you get a small score boost
|
||||
case DECAY_FOOD:
|
||||
this.score+=5;
|
||||
this.decayFood=this.decayFood.filter(
|
||||
([x, y, _]) => !(x==head[0] && y==head[1])
|
||||
);
|
||||
break;
|
||||
// you eat, you get a small score boost
|
||||
case T.DECAY_FOOD:
|
||||
this.score+=5;
|
||||
this.decayFood=this.decayFood.filter(
|
||||
([x, y, _]) => !(x==head[0] && y==head[1])
|
||||
);
|
||||
} break;
|
||||
|
||||
// you eat, you destroy all doors
|
||||
case KEY:
|
||||
this.getTilesOfType(DOOR).forEach(([x, y]) => this.world[x][y]=EMPTY);
|
||||
case 'key':
|
||||
this.putTileA(head, T.EMPTY);
|
||||
this.replaceTilesOfType(T.DOOR, T.EMPTY);
|
||||
break;
|
||||
|
||||
// you step on, you trigger
|
||||
case SWITCH_ON:
|
||||
case SWITCH_OFF: {
|
||||
this.world[head[0]][head[1]]=this.world[head[0]][head[1]]==SWITCH_ON?SWITCH_OFF:SWITCH_ON;
|
||||
if(this.getTilesOfType(SPIKES_OFF_S).length) return this.die("spiked themselves", "activated spikes");
|
||||
const oldSpikes=this.getTilesOfType(SPIKES_ON);
|
||||
this.getTilesOfType(SPIKES_OFF).forEach(([x, y]) => this.world[x][y]=SPIKES_ON);
|
||||
oldSpikes.forEach(([x, y]) => this.world[x][y]=SPIKES_OFF);
|
||||
case 'switch': {
|
||||
this.putTileA(head, tile==T.SWITCH_ON?T.SWITCH_OFF:T.SWITCH_ON);
|
||||
if(this.getTilesOfType(T.SPIKES_OFF_S).length) return this.die("spiked themselves", "activated spikes");
|
||||
const oldSpikes=this.getTilesOfType(T.SPIKES_ON);
|
||||
this.replaceTilesOfType(T.SPIKES_OFF, T.SPIKES_ON);
|
||||
oldSpikes.forEach(pos => this.putTileA(pos, T.SPIKES_OFF));
|
||||
} break;
|
||||
|
||||
// you eat, you grow
|
||||
case FOOD:
|
||||
// re-grow the snake partially (can't hit the tail, but it's there for all other intents and purposes
|
||||
case 'food':
|
||||
// re-grow the snake
|
||||
this.snake.push(tail);
|
||||
this.putTileA(tail, snakeVersion(this.getTileA(tail)));
|
||||
this.length++;
|
||||
|
||||
// remove the fruit from existence
|
||||
this.world[head[0]][head[1]]=SNAKE;
|
||||
this.putTileA(head, T.EMPTY);
|
||||
this.fruits=this.fruits.filter(
|
||||
([x, y]) => !(x==head[0] && y==head[1])
|
||||
);
|
||||
|
@ -649,26 +313,26 @@ class SnekGame {
|
|||
|
||||
// custom rules
|
||||
if(this.rules.fruitRegrow) {
|
||||
const emptyCells=this.getTilesOfType(EMPTY);
|
||||
const emptyCells=this.getTilesOfType(T.EMPTY);
|
||||
|
||||
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
||||
this.fruits.push(cell);
|
||||
this.world[cell[0]][cell[1]]=FOOD;
|
||||
this.putTileA(cell, T.FOOD);
|
||||
}
|
||||
|
||||
if(this.rules.superFruitGrow) {
|
||||
if(Math.random()<.1) { // 10% chance
|
||||
const emptyCells=this.getTilesOfType(EMPTY);
|
||||
const emptyCells=this.getTilesOfType(T.EMPTY);
|
||||
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
||||
this.world[cell[0]][cell[1]]=SUPER_FOOD;
|
||||
this.putTileA(cell, T.SUPER_FOOD);
|
||||
}
|
||||
}
|
||||
|
||||
if(this.rules.decayingFruitGrow) {
|
||||
if(Math.random()<.2) { // 20% chance
|
||||
const emptyCells=this.getTilesOfType(EMPTY);
|
||||
const emptyCells=this.getTilesOfType(T.EMPTY);
|
||||
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
||||
this.world[cell[0]][cell[1]]=DECAY_FOOD;
|
||||
this.putTileA(cell, T.DECAY_FOOD);
|
||||
this.decayFood.push([cell[0], cell[1], this.playTime]);
|
||||
}
|
||||
}
|
||||
|
@ -680,43 +344,14 @@ class SnekGame {
|
|||
}
|
||||
|
||||
// move our head forward
|
||||
switch(this.world[head[0]][head[1]]) {
|
||||
case HOLE:
|
||||
this.world[head[0]][head[1]]=HOLE_S;
|
||||
break;
|
||||
case FLAMMABLE:
|
||||
this.world[head[0]][head[1]]=FLAMMABLE_S;
|
||||
break;
|
||||
case PORTAL_A:
|
||||
this.world[head[0]][head[1]]=PORTAL_A_S;
|
||||
break;
|
||||
case PORTAL_B:
|
||||
this.world[head[0]][head[1]]=PORTAL_B_S;
|
||||
break;
|
||||
case PORTAL_C:
|
||||
this.world[head[0]][head[1]]=PORTAL_C_S;
|
||||
break;
|
||||
case PORTAL_D:
|
||||
this.world[head[0]][head[1]]=PORTAL_D_S;
|
||||
break;
|
||||
case SWITCH_ON:
|
||||
this.world[head[0]][head[1]]=SWITCH_ON_S;
|
||||
break;
|
||||
case SWITCH_OFF:
|
||||
this.world[head[0]][head[1]]=SWITCH_OFF_S;
|
||||
break;
|
||||
case SPIKES_OFF:
|
||||
this.world[head[0]][head[1]]=SPIKES_OFF_S;
|
||||
break;
|
||||
default:
|
||||
this.world[head[0]][head[1]]=SNAKE;
|
||||
}
|
||||
tile=this.getTileA(head);
|
||||
this.putTileA(head, snakeVersion(tile));
|
||||
this.snake.unshift(head);
|
||||
|
||||
// decay decaying food
|
||||
this.decayFood.forEach(
|
||||
([x, y, birth]) => {
|
||||
if(this.playTime>=birth+2000) this.world[x][y]=EMPTY;
|
||||
if(this.playTime>=birth+2000) this.putTile(x, y, T.EMPTY);
|
||||
}
|
||||
);
|
||||
this.decayFood=this.decayFood.filter(
|
||||
|
@ -737,15 +372,15 @@ class SnekGame {
|
|||
if(this.tickId%this.rules.fireTickSpeed==0) {
|
||||
const touchingFire=([x, y]) => {
|
||||
const surrounding=[
|
||||
this.world[x][y-1],
|
||||
this.world[x][y+1],
|
||||
(this.world[x-1]||[])[y],
|
||||
(this.world[x+1]||[])[y]
|
||||
this.getTile(x, y-1),
|
||||
this.getTile(x, y+1),
|
||||
this.getTile(x-1, y),
|
||||
this.getTile(x-1, y)
|
||||
];
|
||||
return surrounding.some(tile => tile==FIRE);
|
||||
return surrounding.some(tile => tile==T.FIRE);
|
||||
};
|
||||
if(this.getTilesOfType(FLAMMABLE_S).some(touchingFire)) return this.die("didn't know oil was flammable", "stood on oil when it caught on fire");
|
||||
this.getTilesOfType(FLAMMABLE).filter(touchingFire).forEach(([x, y]) => this.world[x][y]=FIRE);
|
||||
if(this.getTilesOfType(T.FLAMMABLE_S).some(touchingFire)) return this.die("didn't know oil was flammable", "stood on oil when it caught on fire");
|
||||
this.getTilesOfType(T.FLAMMABLE).filter(touchingFire).forEach(pos => this.putTileA(pos, T.FIRE));
|
||||
}
|
||||
|
||||
// THE WORLD!
|
||||
|
|
221
src/js/tiles.js
Normal file
221
src/js/tiles.js
Normal file
|
@ -0,0 +1,221 @@
|
|||
const [
|
||||
EMPTY, SNAKE,
|
||||
FOOD, SUPER_FOOD, DECAY_FOOD,
|
||||
WALL,
|
||||
FIRE, FLAMMABLE, FLAMMABLE_S,
|
||||
HOLE, HOLE_S,
|
||||
PORTAL_A, PORTAL_A_S, PORTAL_B, PORTAL_B_S, PORTAL_C, PORTAL_C_S, PORTAL_D, PORTAL_D_S,
|
||||
KEY, DOOR,
|
||||
SWITCH_ON, SWITCH_ON_S, SWITCH_OFF, SWITCH_OFF_S, SPIKES_OFF, SPIKES_OFF_S, SPIKES_ON
|
||||
]=Array(255).keys();
|
||||
|
||||
const tiles={
|
||||
EMPTY, SNAKE,
|
||||
FOOD, SUPER_FOOD, DECAY_FOOD,
|
||||
WALL,
|
||||
FIRE, FLAMMABLE, FLAMMABLE_S,
|
||||
HOLE, HOLE_S,
|
||||
PORTAL_A, PORTAL_A_S, PORTAL_B, PORTAL_B_S, PORTAL_C, PORTAL_C_S, PORTAL_D, PORTAL_D_S,
|
||||
KEY, DOOR,
|
||||
SWITCH_ON, SWITCH_ON_S, SWITCH_OFF, SWITCH_OFF_S, SPIKES_OFF, SPIKES_OFF_S, SPIKES_ON
|
||||
};
|
||||
|
||||
const tileNames=(() => {
|
||||
let tileNames=[];
|
||||
Object.keys(tiles).forEach(key => tileNames[tiles[key]]=key);
|
||||
return tileNames;
|
||||
})();
|
||||
|
||||
const getName=t =>
|
||||
tileNames[t] || `Unknown tile ${t}`;
|
||||
|
||||
const forChar=c => {
|
||||
switch(c) {
|
||||
case ' ': return EMPTY;
|
||||
case 'f': return FOOD;
|
||||
case 'F': return SUPER_FOOD;
|
||||
case 'd': return DECAY_FOOD;
|
||||
case 'w': return WALL;
|
||||
case 'o': return HOLE;
|
||||
case 'i': return FIRE;
|
||||
case 'I': return FLAMMABLE;
|
||||
case 'A': return PORTAL_A;
|
||||
case 'B': return PORTAL_B;
|
||||
case 'C': return PORTAL_C;
|
||||
case 'D': return PORTAL_D;
|
||||
case 'k': return KEY;
|
||||
case 'K': return DOOR;
|
||||
case 's': return SWITCH_OFF;
|
||||
case 'S': return SPIKES_ON;
|
||||
case 't': return SPIKES_OFF;
|
||||
}
|
||||
throw TypeError(`'${c}' doesn't correspond to any tile`);
|
||||
};
|
||||
|
||||
const charFor=t => {
|
||||
switch(t) {
|
||||
case EMPTY: return ' ';
|
||||
case FOOD: return 'f';
|
||||
case SUPER_FOOD: return 'F';
|
||||
case DECAY_FOOD: return 'd';
|
||||
case WALL: return 'w';
|
||||
case HOLE: return 'o';
|
||||
case FIRE: return 'i';
|
||||
case FLAMMABLE: return 'I';
|
||||
case PORTAL_A: return 'A';
|
||||
case PORTAL_B: return 'B';
|
||||
case PORTAL_C: return 'C';
|
||||
case PORTAL_D: return 'D';
|
||||
case KEY: return 'k';
|
||||
case DOOR: return 'K';
|
||||
case SWITCH_OFF: return 's';
|
||||
case SPIKES_ON: return 'S';
|
||||
case SPIKES_OFF: return 't';
|
||||
}
|
||||
throw TypeError(`'${getName(t)}' doesn't have a corresponding character'`);
|
||||
};
|
||||
|
||||
const snakeVersion=t => {
|
||||
switch(t) {
|
||||
case EMPTY: return SNAKE;
|
||||
case HOLE: return HOLE_S;
|
||||
case FLAMMABLE: return FLAMMABLE_S;
|
||||
case PORTAL_A: return PORTAL_A_S;
|
||||
case PORTAL_B: return PORTAL_B_S;
|
||||
case PORTAL_C: return PORTAL_C_S;
|
||||
case PORTAL_D: return PORTAL_D_S;
|
||||
case SWITCH_OFF: return SWITCH_OFF_S;
|
||||
case SWITCH_ON: return SWITCH_ON_S;
|
||||
case SPIKES_OFF: return SPIKES_OFF_S;
|
||||
}
|
||||
throw TypeError(`'${getName(t)}' doesn't have a snake version'`);
|
||||
};
|
||||
|
||||
const nonSnakeVersion=t => {
|
||||
switch(t) {
|
||||
case SNAKE: return EMPTY;
|
||||
case HOLE_S: return HOLE;
|
||||
case FLAMMABLE_S: return FLAMMABLE;
|
||||
case PORTAL_A_S: return PORTAL_A;
|
||||
case PORTAL_B_S: return PORTAL_B;
|
||||
case PORTAL_C_S: return PORTAL_C;
|
||||
case PORTAL_D_S: return PORTAL_D;
|
||||
case SWITCH_OFF_S: return SWITCH_OFF;
|
||||
case SWITCH_ON_S: return SWITCH_ON;
|
||||
case SPIKES_OFF_S: return SPIKES_OFF;
|
||||
}
|
||||
throw TypeError(`'${getName(t)}' doesn't have a non-snake version'`);
|
||||
};
|
||||
|
||||
const isSnakeVersion=t => {
|
||||
switch(t) {
|
||||
case SNAKE:
|
||||
case HOLE_S:
|
||||
case FLAMMABLE_S:
|
||||
case PORTAL_A_S: case PORTAL_B_S: case PORTAL_C_S: case PORTAL_D_S:
|
||||
case SWITCH_OFF_S: case SWITCH_ON_S:
|
||||
case SPIKES_OFF_S:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isNonSnakeVersion=t => {
|
||||
switch(t) {
|
||||
case EMPTY:
|
||||
case HOLE:
|
||||
case FLAMMABLE:
|
||||
case PORTAL_A: case PORTAL_B: case PORTAL_C: case PORTAL_D:
|
||||
case SWITCH_OFF: case SWITCH_ON:
|
||||
case SPIKES_OFF:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isSafe=t => {
|
||||
switch(t) {
|
||||
case EMPTY:
|
||||
case HOLE:
|
||||
case FLAMMABLE:
|
||||
case PORTAL_A: case PORTAL_B: case PORTAL_C: case PORTAL_D:
|
||||
case SWITCH_OFF: case SWITCH_ON:
|
||||
case SPIKES_OFF:
|
||||
case FOOD: case SUPER_FOOD: case DECAY_FOOD:
|
||||
case KEY:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isWall=t => {
|
||||
switch(t) {
|
||||
case WALL:
|
||||
case FIRE:
|
||||
case DOOR:
|
||||
case SPIKES_ON:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const getType=t => {
|
||||
if(isSnakeVersion(t)) return 'snake';
|
||||
if(isWall(t)) return 'wall';
|
||||
|
||||
switch(t) {
|
||||
case EMPTY:
|
||||
case FLAMMABLE:
|
||||
case SPIKES_OFF:
|
||||
return 'empty';
|
||||
|
||||
case FOOD:
|
||||
return 'food';
|
||||
|
||||
case SUPER_FOOD:
|
||||
case DECAY_FOOD:
|
||||
return 'bonus';
|
||||
|
||||
case HOLE:
|
||||
return 'hole';
|
||||
|
||||
case PORTAL_A:
|
||||
case PORTAL_B:
|
||||
case PORTAL_C:
|
||||
case PORTAL_D:
|
||||
return 'portal';
|
||||
|
||||
case KEY:
|
||||
return 'key';
|
||||
|
||||
case SWITCH_ON:
|
||||
case SWITCH_OFF:
|
||||
return 'switch';
|
||||
}
|
||||
|
||||
throw TypeError(`'${getName(t)}' isn't a valid tile`);
|
||||
};
|
||||
|
||||
const isPortal=t => {
|
||||
switch(t) {
|
||||
case PORTAL_A:
|
||||
case PORTAL_B:
|
||||
case PORTAL_C:
|
||||
case PORTAL_D:
|
||||
case PORTAL_A_S:
|
||||
case PORTAL_B_S:
|
||||
case PORTAL_C_S:
|
||||
case PORTAL_D_S:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return module.exports={
|
||||
tiles,
|
||||
tileNames, getName,
|
||||
forChar, charFor,
|
||||
snakeVersion, nonSnakeVersion, isSnakeVersion, isNonSnakeVersion,
|
||||
isSafe, isWall, isPortal,
|
||||
getType
|
||||
};
|
Loading…
Reference in a new issue