;(() => {
const randInt = (min = 2, max = min + (min = 0)) => Math.random() * (max - min) + min | 0;
const randItem = array => array[Math.random() * array.length | 0];
const point = (() => {
function Point(x = 0, y = 0) { this.x = x; this.y = y }
Point.prototype = { // p is used to denote a point, Can have post fix number
isSame(p) { return this.x === p.x && this.y === p.y },
as(x, y = x.y + ((x = x.x), 0)) { return (this.x = x, this.y = y, this) },
add(p) { this.x += p.x; this.y += p.y },
}
return (x, y) => new Point(x, y);
})();
const arrayAny = (predicate, array, count = array.length) => {
for (const item of array) {
if ( predicate(item) === true ) { return true }
if (!(--count)) { break }
}
return false;
}
const settings = { // settings should alwayalways have the units and limits in the comments
tiles : 10, // Number ofin gamepixels tilessize MUSTof BEa evenblock. AKA multiplier from question
frameTime: 200, // in ms (1/1000th) Must be > 1000 / 60 aka speed from question
snakeBodyMin : 5, // start number body parts
snakeMoveEvery : 15, // number of frames between snake moves
newGameWaitFrameCount : 60, // in frames 1/60th second
foodScore : 1,
foodColor : "yellow",
backgroundColor : "gray",
snakeColor : "white",
directions : "up,down,left,right".split(","),
moves : { // mapping
up : {name : "up", key : "ArrowUp", op : "down", vec : point(0, -1)},
down : {name : "down", key : "ArrowDown", op : "up", vec : point(0, 1)},
left : {name : "left", key : "ArrowLeft", op : "right", vec : point(-1, 0)},
right : {name : "right", key : "ArrowRight", op : "left", vec : point(1, 0)},
},
}
const keys = (() => {
const keys = {any : false};
for (const name of settings.directions) { keys[settings.moves[name].key] = false }
function keyEvent(event) {
if (keys[event.code] !== undefined) {
keys[event.code] = event.type === "keydown";
event.preventDefault();
if (event.type === "keydown") { keys.any = true }
}
}
addEventListener("keydown", keyEvent);
addEventListener("keyup", keyEvent);
focus(); // get focus
return keys;
})();
keys.reset = () => {
for (const keyName of settings.direction) { keys[settings.moves[keyName].key] = false }
};
canvas.style.background = settings.backgroundColor;
const ctx = canvas.getContext("2d");
const playfield = (() => {
const size = settings.tiles;
const API = {
columns : canvas.width / size,
rows : canvas.height / size,
fillBox(point, col = ctx.fillStyle) {
ctx.fillStyle = col;
ctx.fillRect(point.x * size, point.y * size, size, size);
},
isInside(p) { return ! (p.x < 0 || p.x >= API.rows || p.y < 0 || p.y >= API.columns) },
};
return API;
})();
const food = (() => {
const col = settings.foodColor;
const pos = point();
return {
reset() { // for performance dont use recursion
var searching = true; // Though not needed, some optimiser do not optimise code wrapped in a
// loop with not exit condition eg while(true) so always include
// an exit condition even if you dont need or use it
while (searching) {
pos.x = randInt(playfield.columns);
pos.y = randInt(playfield.rows);
if (! snake.isOverPoint(pos)) {
searching = false;
break;
}
}
},
draw() { playfield.fillBox(pos, col) },
isAt(point) { return pos.isSame(point) },
};
})();
const snake = (() => {
const col = settings.snakeColor;
const body = [point()];
const head = point(); // just a temp to do tests
var length;
const moves = settings.moves;
var dir;
var index = 0;
const each = cb => {index = 0; while (index < length) { cb(body[index++]) } };
return {
isOverPoint(point) { return arrayAny(point.isSame, body, length) },
set direction(value) { dir = dir.op !== value ? moves[value] : dir },
reset() {
var i;
dir = moves[randItem(settings.directions)];
length = settings.snakeBodyMin;
body[0].as(playfield.columns / 2 | 0, playfield.rows / 2 | 0);
for (i = 1; i < length ; i++) {
if (body[i] === undefined) { body[i] = point() }
body[i].as(body[i-1]).add(moves[dir.op].vec);
}
},
draw() {
ctx.fillStyle = settings.snakeColor;
each(playfield.fillBox);
},
update(canMove) {
for (const dirName of settings.directions) { if (keys[moves[dirName].key]) { snake.direction = dirName } }
if (!canMove) { return }
head.as(body[0]).add(dir.vec);
if (!snake.isOverPoint(head) && playfield.isInside(head)) {
if (food.isAt(head)) {
length += 1;
if (body.length <= length) { body.push(point()) }
game.score = settings.foodScore;
body.unshift(body.pop().as(head));
food.reset();
} else { body.unshift(body.pop().as(head)) }
} else { game.state = "gameOver" }
},
};
})();
const game = (() => {
var frameCount;
var score = 0;
var started = false;
var currentName = ""; // not used ATM
var current = null;
var nextState;
const game = {
set score(value) {
if (value === 0) { score = 0 }
else { score += value }
scoreElement.textContent = score;
},
set text(value){
if (value === "") {
textElement.classList.add("hide");
} else {
textElement.classList.remove("hide");
textElement.textContent = value;
}
},
newGame() {
game.score = 0;
snake.reset();
food.reset();
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
food.draw();
snake.draw();
nextState = "play";
game.state = "waitForFrames";
game.text = "Get ready";
},
gameOver() {
nextState = "newGame";
game.state = "waitForFrames";
game.text = "Game Over";
},
play() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
snake.update(frameCount % settings.snakeMoveEvery === settings.snakeMoveEvery -1);
food.draw();
snake.draw();
},
waitForFrames() {
if (frameCount >= settings.newGameWaitFrameCount) { game.state = nextState }
},
set state(stateName) {
frameCount = 0;
if (game[stateName] !== undefined) {
current = game[stateName];
currentName = stateName;
} else {
current = game.newGame;
currentName = "newGame";
}
game.text = "";
if (!started) {
requestAnimationFrame(game.mainLoop);
started = true;
}
},
mainLoop() {
current();
frameCount += 1;
requestAnimationFrame(game.mainLoop)
}
}
return game;
})();
// all done so start a new game;
game.state = "newGame";
})();
body {
font-family : arial;
}
canvas {
position : absolute;
top : 0px;
left : 0px;
background : gray;
z-index :-1;
}
#scoreElement {
position : absolute;
top : 4px;
left : 4px;
}
#textElement {
position : absolute;
top : 100px;
left : 0px;
width :320px;
text-align : center;
}
.hide {
display : none;
}
<canvas id="canvas" width="320" height="320"> </canvas>
<div id="scoreElement"></div>
<div class ="hide" id="textElement">dsdfsdfsf</div>