|
|
|
@ -1,9 +1,12 @@ |
|
|
|
|
const [EMPTY, FOOD, SUPER_FOOD, DECAY_FOOD, WALL, FIRE, FLAMMABLE, FLAMMABLE_S, HOLE, HOLE_S, SNAKE]=Array(255).keys(); |
|
|
|
|
const [EMPTY, 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, SNAKE]=Array(255).keys(); |
|
|
|
|
|
|
|
|
|
class SnekGame { |
|
|
|
|
constructor(settings, canvas, rules) { |
|
|
|
|
// setup the delay
|
|
|
|
|
this.delay=settings.delay; |
|
|
|
|
this.delay=settings.delay || Infinity; |
|
|
|
|
|
|
|
|
|
// score starts at 0
|
|
|
|
|
this.score=0; |
|
|
|
|
|
|
|
|
|
// world is given in the level
|
|
|
|
|
if(settings.world) { // explicitly
|
|
|
|
@ -23,6 +26,10 @@ class SnekGame { |
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
})(); |
|
|
|
|
} |
|
|
|
@ -32,22 +39,21 @@ class SnekGame { |
|
|
|
|
this.dimensions=[this.world.length, this.world[0].length]; |
|
|
|
|
|
|
|
|
|
// extract the fruits
|
|
|
|
|
this.fruits=[]; |
|
|
|
|
this.world |
|
|
|
|
.forEach((l, x) => l.forEach( |
|
|
|
|
(c, y) => { |
|
|
|
|
if(c==FOOD) this.fruits.push([x, y]); |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
this.fruits=this.getTilesOfType(FOOD); |
|
|
|
|
|
|
|
|
|
// extract the decaying fruits
|
|
|
|
|
this.decayFood=[]; |
|
|
|
|
this.world |
|
|
|
|
.forEach((l, x) => l.forEach( |
|
|
|
|
(c, y) => { |
|
|
|
|
if(c==DECAY_FOOD) this.decaying.push([x, y, 0]); |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
this.decayFood=this.getTilesOfType(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]; |
|
|
|
|
}) |
|
|
|
|
); |
|
|
|
|
} else { // dimension and objects
|
|
|
|
|
|
|
|
|
|
// get the dimensions
|
|
|
|
@ -84,6 +90,17 @@ class SnekGame { |
|
|
|
|
} else { |
|
|
|
|
this.decayFood=[]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 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; |
|
|
|
|
this.portals={...settings.portals}; |
|
|
|
|
} else { |
|
|
|
|
this.portals={}; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// add the snake to the world
|
|
|
|
@ -121,8 +138,20 @@ class SnekGame { |
|
|
|
|
scoreSystem: 'fruit', |
|
|
|
|
fireTickSpeed: 10, |
|
|
|
|
autoSizeGrow: false, |
|
|
|
|
autoSpeedIncrease: false |
|
|
|
|
autoSpeedIncrease: false, |
|
|
|
|
timeFlow: true |
|
|
|
|
}, rules, settings.rules || {}); |
|
|
|
|
|
|
|
|
|
// reset direction if time doesn't flow
|
|
|
|
|
if(!this.rules.timeFlow) { |
|
|
|
|
this.lastDirection=[0, 0]; |
|
|
|
|
this.direction=[0, 0]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// set score if move-based
|
|
|
|
|
if(this.rules.scoreSystem=='moves') { |
|
|
|
|
this.score=this.rules.moveCount; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
get playTime() { |
|
|
|
@ -192,6 +221,10 @@ class SnekGame { |
|
|
|
|
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 putTile=(x, y, tile) => this.ctx.drawImage( |
|
|
|
|
tile, |
|
|
|
|
offsetX+cellSize*x, |
|
|
|
@ -249,6 +282,23 @@ class SnekGame { |
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
@ -258,6 +308,32 @@ class SnekGame { |
|
|
|
|
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; |
|
|
|
@ -381,10 +457,21 @@ class SnekGame { |
|
|
|
|
this.lastDirection=this.direction; |
|
|
|
|
|
|
|
|
|
// compute our new head
|
|
|
|
|
const head=[ |
|
|
|
|
this.snake[0][0]+this.direction[0], |
|
|
|
|
this.snake[0][1]+this.direction[1] |
|
|
|
|
]; |
|
|
|
|
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; |
|
|
|
|
this.portaled=true; |
|
|
|
|
} else { |
|
|
|
|
head=[ |
|
|
|
|
this.snake[0][0]+this.direction[0], |
|
|
|
|
this.snake[0][1]+this.direction[1] |
|
|
|
|
]; |
|
|
|
|
this.portaled=false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// get our tail out of the way
|
|
|
|
|
const tail=this.snake.pop(); |
|
|
|
@ -395,6 +482,18 @@ class SnekGame { |
|
|
|
|
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; |
|
|
|
|
default: |
|
|
|
|
this.world[tail[0]][tail[1]]=EMPTY; |
|
|
|
|
} |
|
|
|
@ -416,6 +515,10 @@ class SnekGame { |
|
|
|
|
case SNAKE: |
|
|
|
|
case HOLE_S: |
|
|
|
|
case FLAMMABLE_S: |
|
|
|
|
case PORTAL_A_S: |
|
|
|
|
case PORTAL_B_S: |
|
|
|
|
case PORTAL_C_S: |
|
|
|
|
case PORTAL_D_S: |
|
|
|
|
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
|
|
|
|
@ -498,6 +601,18 @@ class SnekGame { |
|
|
|
|
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; |
|
|
|
|
default: |
|
|
|
|
this.world[head[0]][head[1]]=SNAKE; |
|
|
|
|
} |
|
|
|
@ -538,6 +653,12 @@ class SnekGame { |
|
|
|
|
this.getTilesOfType(FLAMMABLE).filter(touchingFire).forEach(([x, y]) => this.world[x][y]=FIRE); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// THE WORLD!
|
|
|
|
|
if(!this.rules.timeFlow) { |
|
|
|
|
this.lastDirection=[0, 0]; |
|
|
|
|
this.direction=[0, 0]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// victory condition
|
|
|
|
|
if(this.rules.winCondition=='fruit') { |
|
|
|
|
if(!this.fruits.length) return this.win(); |
|
|
|
@ -548,6 +669,9 @@ class SnekGame { |
|
|
|
|
if(this.rules.winCondition=='score') { |
|
|
|
|
if(this.score>=this.rules.scoreObjective) return this.win(); |
|
|
|
|
} |
|
|
|
|
if(this.rules.scoreSystem=='moves') { |
|
|
|
|
if(this.score) this.score--; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
tick() { |
|
|
|
@ -555,10 +679,13 @@ class SnekGame { |
|
|
|
|
if(!this.lastStep) this.lastStep=this.firstStep; |
|
|
|
|
this.draw(); |
|
|
|
|
if(this.callback) this.callback('tick'); |
|
|
|
|
if(this.lastStep+this.delay<Date.now()) { |
|
|
|
|
if(this.rules.timeFlow && this.lastStep+this.delay<Date.now()) { |
|
|
|
|
this.lastStep+=this.delay; |
|
|
|
|
this.step(); |
|
|
|
|
} |
|
|
|
|
if(!this.rules.timeFlow && (this.direction[0]!=0 || this.direction[1]!=0)) { |
|
|
|
|
this.step(); |
|
|
|
|
} |
|
|
|
|
requestAnimationFrame(() => this.tick()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -615,7 +742,6 @@ class SnekGame { |
|
|
|
|
this.firstStep=Date.now(); |
|
|
|
|
this.tickId=0; |
|
|
|
|
this.playing=true; |
|
|
|
|
this.score=0; |
|
|
|
|
requestAnimationFrame(() => this.tick()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|