359 lines
9.2 KiB
JavaScript
359 lines
9.2 KiB
JavaScript
/**
|
|
* Game config
|
|
*/
|
|
const number_levels = 2;
|
|
const cell_width = 20;
|
|
const cell_height = 20;
|
|
|
|
/**
|
|
* Cells content
|
|
*/
|
|
let i = 0;
|
|
const EMPTY = i++;
|
|
const SNAKE = i++;
|
|
const WALL = i++;
|
|
const FOOD = i++;
|
|
delete i;
|
|
|
|
/**
|
|
* Get & return an element by its ID
|
|
*
|
|
* @param {String} id The ID of the element to get
|
|
* @return {HTMLElement} The target element
|
|
*/
|
|
function byId(id) {
|
|
return document.getElementById(id);
|
|
}
|
|
|
|
/**
|
|
* Draw a line in a canvas rendering context
|
|
*
|
|
* @param {CanvasRenderingContext2D} ctx Target context
|
|
* @param {Number} x1 Starting x
|
|
* @param {Number} y1 Starting y
|
|
* @param {Number} x2 Ending x
|
|
* @param {Number} y2 Ending y
|
|
*/
|
|
function drawLine(ctx, x1, y1, x2, y2) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(x1, y1);
|
|
ctx.lineTo(x2,y2);
|
|
ctx.stroke();
|
|
}
|
|
|
|
/**
|
|
* Generate a random number
|
|
*
|
|
* @param {Number} min Minimum value
|
|
* @param {Number} max Maximum value
|
|
*/
|
|
function randInt(min, max) {
|
|
return Math.floor((Math.random()*max) + min)
|
|
}
|
|
|
|
// Get elements
|
|
const startScreen = byId("startScreen")
|
|
const gameScreen = byId("gameScreen")
|
|
const levelChoicesTarget = byId("levelChoice")
|
|
const startGameBtn = byId("startGameBtn")
|
|
const stopGameBtn = byId("stopGameBtn")
|
|
const canvasTarget = byId("canvasTarget")
|
|
const scoreTarget = byId("scoreTarget")
|
|
|
|
|
|
/**
|
|
* Show main screen
|
|
*/
|
|
function showMainScreen() {
|
|
startScreen.style.display = "unset";
|
|
gameScreen.style.display = "none";
|
|
}
|
|
|
|
/**
|
|
* Start a new game
|
|
*
|
|
* The scope of the function is the main
|
|
* game scope
|
|
*/
|
|
async function startGame(gameID) {
|
|
startScreen.style.display = "none";
|
|
gameScreen.style.display = "unset";
|
|
|
|
// Fetch level information
|
|
let level;
|
|
try {
|
|
level = await (await fetch("levels/"+gameID+".json")).json();
|
|
} catch(e) {
|
|
console.error(e);
|
|
alert("Could not load game level!");
|
|
return;
|
|
}
|
|
|
|
// Create & apply the canvas
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = cell_width * level.dimensions[1];
|
|
canvas.height = cell_height * level.dimensions[0];
|
|
canvasTarget.appendChild(canvas);
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Initialize the map & snake arrays
|
|
let map = [];
|
|
let snake = [];
|
|
let score = 0;
|
|
|
|
/// First with empty cells...
|
|
for(let i = 0; i < level.dimensions[0]; i++) {
|
|
let cell = [];
|
|
for(let j = 0; j < level.dimensions[1]; j++) {
|
|
cell.push(EMPTY);
|
|
}
|
|
map.push(cell)
|
|
}
|
|
|
|
/// ... then with cells content
|
|
level.walls.forEach((w) => {
|
|
map[w[0]-1][w[1]-1] = WALL
|
|
})
|
|
|
|
level.food.forEach((f) => {
|
|
map[f[0]-1][f[1]-1] = FOOD
|
|
})
|
|
|
|
level.snake.forEach((s) => {
|
|
map[s[0]-1][s[1]-1] = SNAKE
|
|
snake.push([s[0]-1, s[1]-1]);
|
|
})
|
|
|
|
// Initialize pressed key
|
|
let key = level.firstKey;
|
|
|
|
/**
|
|
* Step function
|
|
*
|
|
* I placed this function here to inherit
|
|
* the map, snake, canvas & ctx variables...
|
|
*/
|
|
let interval = setInterval(() => step(), level.delay);
|
|
function step() {
|
|
|
|
// Check if a game was destroyed
|
|
if(!canvas.isConnected)
|
|
clearInterval(interval)
|
|
|
|
// Move the snake if required
|
|
if(key) {
|
|
let newHead = Array.from(snake[snake.length-1]);
|
|
let increaseSize = false;
|
|
|
|
// Make the snake move
|
|
switch(key) {
|
|
case "ArrowDown":
|
|
newHead[0]++;
|
|
break;
|
|
|
|
case "ArrowUp":
|
|
newHead[0]--;
|
|
break;
|
|
|
|
case "ArrowLeft":
|
|
newHead[1]--;
|
|
break;
|
|
|
|
case "ArrowRight":
|
|
newHead[1]++;
|
|
break;
|
|
}
|
|
|
|
if(newHead[0] < 0 || newHead[1] < 0 ||
|
|
newHead[0] >= level.dimensions[0] || newHead[1] >= level.dimensions[1]) {
|
|
gameOver();
|
|
return;
|
|
}
|
|
|
|
// Trigger appropriate action
|
|
switch(map[newHead[0]][newHead[1]]) {
|
|
|
|
case FOOD:
|
|
increaseSize = true;
|
|
score++;
|
|
break;
|
|
|
|
case SNAKE:
|
|
case WALL:
|
|
gameOver();
|
|
break;
|
|
|
|
}
|
|
|
|
// Push new snake position
|
|
snake.push(newHead);
|
|
map[newHead[0]][newHead[1]] = SNAKE
|
|
|
|
// Remove the end of the snake if he has not eaten anything
|
|
if(!increaseSize) {
|
|
const oldPos = snake.shift()
|
|
map[oldPos[0]][oldPos[1]] = EMPTY
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Refresh score
|
|
scoreTarget.innerHTML = score;
|
|
|
|
|
|
// Redraw screen
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
|
|
// First, draw the grid
|
|
for(let i = 0; i <= level.dimensions[1]; i++) {
|
|
drawLine(ctx, i*cell_width, 0, i*cell_width, canvas.height)
|
|
}
|
|
for(let i = 0; i <= level.dimensions[0]; i++) {
|
|
drawLine(ctx, 0, i*cell_height, canvas.width, i*cell_height, canvas.height)
|
|
}
|
|
|
|
|
|
// Now draw the map
|
|
for(let y = 0; y < map.length; y++) {
|
|
for(let x = 0; x < map[y].length; x++) {
|
|
|
|
// Adapt rendering to the element to display
|
|
switch(map[y][x]) {
|
|
|
|
case WALL:
|
|
ctx.fillStyle = "darkRed";
|
|
ctx.fillRect(x*cell_width, y*cell_height, cell_width, cell_height)
|
|
break;
|
|
|
|
case FOOD:
|
|
ctx.fillStyle = "darkGreen";
|
|
ctx.fillRect(x*cell_width, y*cell_height, cell_width, cell_height)
|
|
break;
|
|
|
|
case SNAKE:
|
|
ctx.fillStyle = "orange"
|
|
ctx.fillRect(x*cell_width, y*cell_height, cell_width, cell_height)
|
|
|
|
// Check if it is the head of the snake
|
|
// If it is the case, we draw an eye to the snake
|
|
const headPos = snake[snake.length-1];
|
|
|
|
if(headPos[0] == y && headPos[1] == x) {
|
|
ctx.fillStyle = "darkRed";
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
(x+0.5)*cell_width, // x
|
|
(y+0.5)*cell_height, // y
|
|
3, // width
|
|
0, // startAngle
|
|
2*Math.PI) // End angle
|
|
ctx.fill();
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call this function once the user loose the game
|
|
*/
|
|
function gameOver() {
|
|
clearInterval(interval);
|
|
alert("Game over !!!");
|
|
location.href = "#";
|
|
}
|
|
|
|
// Listen for key press events
|
|
document.body.addEventListener("keydown", (ev) => {
|
|
if(["ArrowDown", "ArrowLeft", "ArrowRight", "ArrowUp"].includes(ev.key))
|
|
key = ev.key;
|
|
});
|
|
|
|
// Automatically generate new map element if required
|
|
if(level.randWallFreq) {
|
|
const int = setInterval(() => {
|
|
if(!canvas.isConnected)
|
|
clearInterval(int);
|
|
else {
|
|
const pos = [randInt(0, map.length), randInt(0, map[0].length)]
|
|
if(map[pos[0]][pos[1]] == EMPTY)
|
|
map[pos[0]][pos[1]] = WALL;
|
|
}
|
|
}, level.randWallFreq);
|
|
}
|
|
|
|
// Automatically generate new map element if required
|
|
if(level.randFoodFreq) {
|
|
const int = setInterval(() => {
|
|
if(!canvas.isConnected)
|
|
clearInterval(int);
|
|
else {
|
|
const pos = [randInt(0, map.length), randInt(0, map[0].length)]
|
|
if(map[pos[0]][pos[1]] == EMPTY)
|
|
map[pos[0]][pos[1]] = FOOD;
|
|
}
|
|
}, level.randFoodFreq);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change the currently active window
|
|
*/
|
|
function changeWindow() {
|
|
|
|
// Make sure there are not canvas left in the background (to make sure
|
|
// no interval is running for an old game)
|
|
canvasTarget.innerHTML = ""
|
|
|
|
// Try to get game ID
|
|
const gameID = Number(window.location.hash.substr(1));
|
|
|
|
if(gameID > 0)
|
|
startGame(gameID);
|
|
else
|
|
showMainScreen();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize page
|
|
|
|
/// Listen to events
|
|
window.addEventListener("hashchange", (e) => changeWindow())
|
|
|
|
|
|
// Make game levels form lives
|
|
for (let index = 1; index < number_levels + 1; index++) {
|
|
levelChoicesTarget.innerHTML += "<li>" +
|
|
"<input type='radio' name='level' value='"+index+"' "+(index == 1 ? "checked" : "")+" />" +
|
|
" Level " + index + "</li>";
|
|
}
|
|
|
|
startGameBtn.addEventListener("click", (ev) => {
|
|
ev.preventDefault();
|
|
|
|
const gameID = document.querySelector("input[name='level']:checked").value
|
|
location.href = "#" + gameID;
|
|
})
|
|
|
|
|
|
// Stop game
|
|
stopGameBtn.addEventListener("click", () => {
|
|
location.href = "#";
|
|
})
|
|
|
|
// Refresh current window
|
|
changeWindow(); |