SnakeGame/script.js

359 lines
9.2 KiB
JavaScript
Raw Normal View History

2020-03-23 13:16:25 +00:00
/**
* Game config
*/
2020-03-23 13:09:28 +00:00
const number_levels = 2;
2020-03-23 13:46:35 +00:00
const cell_width = 20;
const cell_height = 20;
2020-03-23 13:09:28 +00:00
2020-03-23 13:46:35 +00:00
/**
* 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();
2020-03-23 13:09:28 +00:00
}
2020-03-23 16:50:42 +00:00
/**
* 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)
}
2020-03-23 13:09:28 +00:00
// Get elements
const startScreen = byId("startScreen")
const gameScreen = byId("gameScreen")
const levelChoicesTarget = byId("levelChoice")
const startGameBtn = byId("startGameBtn")
const stopGameBtn = byId("stopGameBtn")
2020-03-23 13:16:25 +00:00
const canvasTarget = byId("canvasTarget")
2020-03-23 16:39:20 +00:00
const scoreTarget = byId("scoreTarget")
2020-03-23 13:09:28 +00:00
/**
* Show main screen
*/
function showMainScreen() {
startScreen.style.display = "unset";
gameScreen.style.display = "none";
}
/**
* Start a new game
2020-03-23 13:46:35 +00:00
*
* The scope of the function is the main
* game scope
2020-03-23 13:09:28 +00:00
*/
async function startGame(gameID) {
startScreen.style.display = "none";
gameScreen.style.display = "unset";
2020-03-23 13:16:25 +00:00
// 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");
2020-03-23 13:46:35 +00:00
canvas.width = cell_width * level.dimensions[1];
canvas.height = cell_height * level.dimensions[0];
2020-03-23 13:16:25 +00:00
canvasTarget.appendChild(canvas);
2020-03-23 13:46:35 +00:00
const ctx = canvas.getContext('2d');
// Initialize the map & snake arrays
let map = [];
let snake = [];
2020-03-23 16:39:20 +00:00
let score = 0;
2020-03-23 13:46:35 +00:00
/// 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]);
})
2020-03-23 14:37:56 +00:00
// Initialize pressed key
let key = level.firstKey;
2020-03-23 13:46:35 +00:00
/**
* Step function
*
* I placed this function here to inherit
* the map, snake, canvas & ctx variables...
*/
let interval = setInterval(() => step(), level.delay);
function step() {
2020-03-23 14:37:56 +00:00
// Check if a game was destroyed
2020-03-23 14:17:50 +00:00
if(!canvas.isConnected)
clearInterval(interval)
2020-03-23 14:37:56 +00:00
// 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;
2020-03-23 16:39:20 +00:00
score++;
2020-03-23 14:37:56 +00:00
break;
2020-03-23 14:43:33 +00:00
case SNAKE:
2020-03-23 14:37:56 +00:00
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
}
}
2020-03-23 16:39:20 +00:00
// Refresh score
scoreTarget.innerHTML = score;
2020-03-23 14:37:56 +00:00
2020-03-23 13:46:35 +00:00
// 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
2020-03-23 14:43:33 +00:00
for(let y = 0; y < map.length; y++) {
for(let x = 0; x < map[y].length; x++) {
2020-03-23 13:46:35 +00:00
2020-03-23 14:17:50 +00:00
// Adapt rendering to the element to display
2020-03-23 14:43:33 +00:00
switch(map[y][x]) {
2020-03-23 13:46:35 +00:00
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:
2020-03-23 14:17:50 +00:00
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];
2020-03-23 13:46:35 +00:00
2020-03-23 14:43:33 +00:00
if(headPos[0] == y && headPos[1] == x) {
2020-03-23 14:17:50 +00:00
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;
2020-03-23 13:46:35 +00:00
}
}
}
}
2020-03-23 14:37:56 +00:00
/**
* 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;
});
2020-03-23 16:50:42 +00:00
// 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);
}
2020-03-23 13:09:28 +00:00
}
/**
* Change the currently active window
*/
function changeWindow() {
2020-03-23 14:17:50 +00:00
// Make sure there are not canvas left in the background (to make sure
// no interval is running for an old game)
canvasTarget.innerHTML = ""
2020-03-23 13:09:28 +00:00
// 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();