|
|
@ -1,4 +1,4 @@ |
|
|
|
const [EMPTY, FOOD, SUPER_FOOD, DECAY_FOOD, WALL, FIRE, HOLE, HOLE_S, SNAKE]=Array(255).keys(); |
|
|
|
const [EMPTY, FOOD, SUPER_FOOD, DECAY_FOOD, WALL, FIRE, FLAMMABLE, FLAMMABLE_S, HOLE, HOLE_S, SNAKE]=Array(255).keys(); |
|
|
|
|
|
|
|
|
|
|
|
class SnekGame { |
|
|
|
class SnekGame { |
|
|
|
constructor(settings, canvas, rules) { |
|
|
|
constructor(settings, canvas, rules) { |
|
|
@ -22,6 +22,7 @@ class SnekGame { |
|
|
|
case 'w': return WALL; |
|
|
|
case 'w': return WALL; |
|
|
|
case 'o': return HOLE; |
|
|
|
case 'o': return HOLE; |
|
|
|
case 'i': return FIRE; |
|
|
|
case 'i': return FIRE; |
|
|
|
|
|
|
|
case 'I': return FLAMMABLE; |
|
|
|
} |
|
|
|
} |
|
|
|
})(); |
|
|
|
})(); |
|
|
|
} |
|
|
|
} |
|
|
@ -65,8 +66,9 @@ class SnekGame { |
|
|
|
// add the holes
|
|
|
|
// 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]=HOLE); |
|
|
|
|
|
|
|
|
|
|
|
// add the fires
|
|
|
|
// add the fires and flammable tiles
|
|
|
|
if(settings.fires) settings.fires.forEach(([x, y]) => this.world[x][y]=FIRE); |
|
|
|
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); |
|
|
|
|
|
|
|
|
|
|
|
// 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); |
|
|
@ -117,7 +119,7 @@ class SnekGame { |
|
|
|
worldWrap: true, |
|
|
|
worldWrap: true, |
|
|
|
winCondition: 'none', |
|
|
|
winCondition: 'none', |
|
|
|
scoreSystem: 'fruit', |
|
|
|
scoreSystem: 'fruit', |
|
|
|
netPlay: false, |
|
|
|
fireTickSpeed: 10, |
|
|
|
autoSizeGrow: false, |
|
|
|
autoSizeGrow: false, |
|
|
|
autoSpeedIncrease: false |
|
|
|
autoSpeedIncrease: false |
|
|
|
}, rules, settings.rules || {}); |
|
|
|
}, rules, settings.rules || {}); |
|
|
@ -127,6 +129,19 @@ class SnekGame { |
|
|
|
return Date.now()-this.firstStep; |
|
|
|
return Date.now()-this.firstStep; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getTilesOfType(type) { |
|
|
|
|
|
|
|
return this |
|
|
|
|
|
|
|
.world |
|
|
|
|
|
|
|
.map( |
|
|
|
|
|
|
|
(l, x) => l |
|
|
|
|
|
|
|
.map( |
|
|
|
|
|
|
|
(r, y) => r==type?[x,y]:null |
|
|
|
|
|
|
|
).filter( |
|
|
|
|
|
|
|
a => a |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
).flat(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
draw() { |
|
|
|
draw() { |
|
|
|
const assets=require('assets'); |
|
|
|
const assets=require('assets'); |
|
|
|
const config=require('config'); |
|
|
|
const config=require('config'); |
|
|
@ -170,6 +185,7 @@ class SnekGame { |
|
|
|
const wall=assets.get('wall'); |
|
|
|
const wall=assets.get('wall'); |
|
|
|
const hole=assets.get('hole'); |
|
|
|
const hole=assets.get('hole'); |
|
|
|
const fire=assets.get('fire'); |
|
|
|
const fire=assets.get('fire'); |
|
|
|
|
|
|
|
const flammable=assets.get('flammable'); |
|
|
|
const superFruit=assets.get('superFruit'); |
|
|
|
const superFruit=assets.get('superFruit'); |
|
|
|
const decayFruit=assets.get('decayFruit'); |
|
|
|
const decayFruit=assets.get('decayFruit'); |
|
|
|
const putTile=(x, y, tile) => this.ctx.drawImage( |
|
|
|
const putTile=(x, y, tile) => this.ctx.drawImage( |
|
|
@ -221,6 +237,11 @@ class SnekGame { |
|
|
|
// however, the tileset only handles convex shapes
|
|
|
|
// however, the tileset only handles convex shapes
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
case FLAMMABLE: |
|
|
|
|
|
|
|
case FLAMMABLE_S: |
|
|
|
|
|
|
|
putTile(x, y, flammable); |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
|
|
|
|
case SUPER_FOOD: |
|
|
|
case SUPER_FOOD: |
|
|
|
putTileAnim(x, y, superFruit); |
|
|
|
putTileAnim(x, y, superFruit); |
|
|
|
break; |
|
|
|
break; |
|
|
@ -228,12 +249,12 @@ class SnekGame { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// draw our decaying fruits
|
|
|
|
// draw our decaying fruits (they have more information than just XY, so they need to be drawn here
|
|
|
|
this.decayFood.forEach(([x, y, birth]) => |
|
|
|
this.decayFood.forEach(([x, y, birth]) => |
|
|
|
putTileAnimPercent(x, y, decayFruit, (this.playTime-birth)/2000) |
|
|
|
putTileAnimPercent(x, y, decayFruit, (this.playTime-birth)/2000) |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// draw our snake
|
|
|
|
// draw our snake (it gets drawn completely differently, so here it goes)
|
|
|
|
const snake=assets.get('snake'); |
|
|
|
const snake=assets.get('snake'); |
|
|
|
this.ctx.fillStyle=snake.color; |
|
|
|
this.ctx.fillStyle=snake.color; |
|
|
|
this.ctx.strokeStyle=snake.color; |
|
|
|
this.ctx.strokeStyle=snake.color; |
|
|
@ -367,6 +388,9 @@ class SnekGame { |
|
|
|
case HOLE_S: |
|
|
|
case HOLE_S: |
|
|
|
this.world[tail[0]][tail[1]]=HOLE; |
|
|
|
this.world[tail[0]][tail[1]]=HOLE; |
|
|
|
break; |
|
|
|
break; |
|
|
|
|
|
|
|
case FLAMMABLE_S: |
|
|
|
|
|
|
|
this.world[tail[0]][tail[1]]=FLAMMABLE; |
|
|
|
|
|
|
|
break; |
|
|
|
default: |
|
|
|
default: |
|
|
|
this.world[tail[0]][tail[1]]=EMPTY; |
|
|
|
this.world[tail[0]][tail[1]]=EMPTY; |
|
|
|
} |
|
|
|
} |
|
|
@ -416,9 +440,8 @@ class SnekGame { |
|
|
|
|
|
|
|
|
|
|
|
// you eat, you grow
|
|
|
|
// you eat, you grow
|
|
|
|
case FOOD: |
|
|
|
case FOOD: |
|
|
|
// re-grow the snake
|
|
|
|
// re-grow the snake partially (can't hit the tail, but it's there for all other intents and purposes
|
|
|
|
this.snake.push(tail); |
|
|
|
this.snake.push(tail); |
|
|
|
this.world[tail[0]][tail[1]]=SNAKE; |
|
|
|
|
|
|
|
this.length++; |
|
|
|
this.length++; |
|
|
|
|
|
|
|
|
|
|
|
// remove the fruit from existence
|
|
|
|
// remove the fruit from existence
|
|
|
@ -430,21 +453,9 @@ class SnekGame { |
|
|
|
// increase score
|
|
|
|
// increase score
|
|
|
|
this.score++; |
|
|
|
this.score++; |
|
|
|
|
|
|
|
|
|
|
|
// list empty cells
|
|
|
|
|
|
|
|
const getEmptyCells=() => |
|
|
|
|
|
|
|
this.world |
|
|
|
|
|
|
|
.map( |
|
|
|
|
|
|
|
(l, x) => l |
|
|
|
|
|
|
|
.map( |
|
|
|
|
|
|
|
(r, y) => r==EMPTY?[x,y]:null |
|
|
|
|
|
|
|
).filter( |
|
|
|
|
|
|
|
a => a |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
).flat(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// custom rules
|
|
|
|
// custom rules
|
|
|
|
if(this.rules.fruitRegrow) { |
|
|
|
if(this.rules.fruitRegrow) { |
|
|
|
const emptyCells=getEmptyCells(); |
|
|
|
const emptyCells=this.getTilesOfType(EMPTY); |
|
|
|
|
|
|
|
|
|
|
|
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)]; |
|
|
|
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)]; |
|
|
|
this.fruits.push(cell); |
|
|
|
this.fruits.push(cell); |
|
|
@ -453,7 +464,7 @@ class SnekGame { |
|
|
|
|
|
|
|
|
|
|
|
if(this.rules.superFruitGrow) { |
|
|
|
if(this.rules.superFruitGrow) { |
|
|
|
if(Math.random()<.1) { // 10% chance
|
|
|
|
if(Math.random()<.1) { // 10% chance
|
|
|
|
const emptyCells=getEmptyCells(); |
|
|
|
const emptyCells=this.getTilesOfType(EMPTY); |
|
|
|
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)]; |
|
|
|
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)]; |
|
|
|
this.world[cell[0]][cell[1]]=SUPER_FOOD; |
|
|
|
this.world[cell[0]][cell[1]]=SUPER_FOOD; |
|
|
|
} |
|
|
|
} |
|
|
@ -461,7 +472,7 @@ class SnekGame { |
|
|
|
|
|
|
|
|
|
|
|
if(this.rules.decayingFruitGrow) { |
|
|
|
if(this.rules.decayingFruitGrow) { |
|
|
|
if(Math.random()<.2) { // 20% chance
|
|
|
|
if(Math.random()<.2) { // 20% chance
|
|
|
|
const emptyCells=getEmptyCells(); |
|
|
|
const emptyCells=this.getTilesOfType(EMPTY); |
|
|
|
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)]; |
|
|
|
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)]; |
|
|
|
this.world[cell[0]][cell[1]]=DECAY_FOOD; |
|
|
|
this.world[cell[0]][cell[1]]=DECAY_FOOD; |
|
|
|
this.decayFood.push([cell[0], cell[1], this.playTime]); |
|
|
|
this.decayFood.push([cell[0], cell[1], this.playTime]); |
|
|
@ -479,6 +490,9 @@ class SnekGame { |
|
|
|
case HOLE: |
|
|
|
case HOLE: |
|
|
|
this.world[head[0]][head[1]]=HOLE_S; |
|
|
|
this.world[head[0]][head[1]]=HOLE_S; |
|
|
|
break; |
|
|
|
break; |
|
|
|
|
|
|
|
case FLAMMABLE: |
|
|
|
|
|
|
|
this.world[head[0]][head[1]]=FLAMMABLE_S; |
|
|
|
|
|
|
|
break; |
|
|
|
default: |
|
|
|
default: |
|
|
|
this.world[head[0]][head[1]]=SNAKE; |
|
|
|
this.world[head[0]][head[1]]=SNAKE; |
|
|
|
} |
|
|
|
} |
|
|
@ -504,6 +518,21 @@ class SnekGame { |
|
|
|
if(this.tickId%this.rules.autoSizeGrowTicks==0) this.snake.push(tail); |
|
|
|
if(this.tickId%this.rules.autoSizeGrowTicks==0) this.snake.push(tail); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// fire tick
|
|
|
|
|
|
|
|
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] |
|
|
|
|
|
|
|
]; |
|
|
|
|
|
|
|
return surrounding.some(tile => tile==FIRE); |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
if(this.getTilesOfType(FLAMMABLE_S).some(touchingFire)) return this.die(); |
|
|
|
|
|
|
|
this.getTilesOfType(FLAMMABLE).filter(touchingFire).forEach(([x, y]) => this.world[x][y]=FIRE); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// victory condition
|
|
|
|
// victory condition
|
|
|
|
if(this.rules.winCondition=='fruit') { |
|
|
|
if(this.rules.winCondition=='fruit') { |
|
|
|
if(!this.fruits.length) return this.win(); |
|
|
|
if(!this.fruits.length) return this.win(); |
|
|
|