SnakeGame/script.js

534 lines
15 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++;
const ICE = i++;
const ICE_SNAKE = i++; // If the snake is on the ice
const TELEPORTATION = 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)
}
/**
* Play an audio file
*
* @param {String} url The URL of the audio file to play
* @returns {HTMLAudioElement} Generated audio element
*/
function playAudio(url) {
const audio = document.createElement("audio");
audio.src = url
audio.play();
return audio;
}
// Get elements
const startScreen = byId("startScreen")
const rulesScreen = byId("rulesScreen")
const rulesBtn = byId("rulesBtn")
const foodList = byId("foodList")
const wallList = byId("wallList")
const gameScreen = byId("gameScreen")
const levelChoicesTarget = byId("levelChoice")
const stopGameBtn = byId("menuBtn")
const canvasTarget = byId("canvasTarget")
const scoreTarget = byId("scoreTarget")
// Get map texture
const imgGrass = new Image();
imgGrass.src = './assets/map/grass.png'
imgGrass.title = "It is just grass.."
const imgIce = new Image();
imgIce.src = './assets/map/ice.jpg'
imgIce.title = "Ice is a slippery slope.. You can not turn on it !"
// Get wall texture
const imgWall = new Image();
imgWall.src = './assets/wall/wall.jpg'
imgWall.title = "It is a brick wall.. Do not try to destroy it with your snake !"
// Get food texture
const imgApple = new Image();
imgApple.src = './assets/food/apple.png'
imgApple.title = "Food is food, go get the apple !"
const imgCherry = new Image();
imgCherry.src = './assets/food/cherry.png'
imgCherry.title = "Do you want to eat some cherries ? Too bad, it is not in the game for now..."
// Get snake texture
let imgSnakeHeadUp = new Image();
imgSnakeHeadUp.src = './assets/snake/snake_head_up.png'
let imgSnakeHeadLeft = new Image();
imgSnakeHeadLeft.src = './assets/snake/snake_head_left.png'
let imgSnakeHeadRight = new Image();
imgSnakeHeadRight.src = './assets/snake/snake_head_right.png'
let imgSnakeHeadDown = new Image();
imgSnakeHeadDown.src = './assets/snake/snake_head_down.png'
const imgSnakeBody = new Image();
imgSnakeBody.src = './assets/snake/snake_body.png'
/**
* Show main screen
*/
function showMainScreen() {
startScreen.style.display = "unset";
gameScreen.style.display = "none";
rulesScreen.style.display = "none";
}
/**
* Show rules screen
*/
function showRulesScreen() {
startScreen.style.display = "none";
gameScreen.style.display = "none";
rulesScreen.style.display = "unset";
// Display all food
foodList.appendChild(imgApple);
foodList.appendChild(imgCherry);
// Display all wall
wallList.appendChild(imgWall);
// Display map texture
mapList.appendChild(imgGrass)
mapList.appendChild(imgIce);
}
/**
* 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";
rulesScreen.style.display = "none";
// 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;
}
// Start audio
const audioEl = playAudio("/assets/game.mp3")
audioEl.loop = true;
// 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
/** @type {Array<Array<Number>>} */
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
// Walls
level.walls.forEach((w) => {
map[w[0]-1][w[1]-1] = WALL
})
// Food
level.food.forEach((f) => {
map[f[0]-1][f[1]-1] = FOOD
})
// Ice
if(level.hasOwnProperty("ice"))
level.ice.forEach((f) => {
map[f[0]-1][f[1]-1] = ICE
})
// Teleportation
const teleportationCells = []
if(level.hasOwnProperty("teleportation"))
level.teleportation.forEach((f) => {
map[f[0]-1][f[1]-1] = TELEPORTATION
teleportationCells.push(JSON.stringify([f[0]-1,f[1]-1]));
})
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 currDelay = level.delay;
setTimeout(() => step(), currDelay);
function step() {
// Check if a game was destroyed
if(!canvas.isConnected) {
audioEl.pause();
return;
}
// 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;
playAudio("assets/eat.mp3")
score++;
break;
case ICE_SNAKE:
case SNAKE:
case WALL:
gameOver();
break;
}
// Push new snake position
snake.push(newHead);
map[newHead[0]][newHead[1]] = map[newHead[0]][newHead[1]] == ICE ? ICE_SNAKE : SNAKE
// Remove the end of the snake if he has not eaten anything
if(!increaseSize) {
const oldPos = snake.shift()
map[oldPos[0]][oldPos[1]] = map[oldPos[0]][oldPos[1]] == ICE_SNAKE ? ICE : EMPTY
// If the cell was a teleportation cell, then revert it back to the right state
if(teleportationCells.includes(JSON.stringify(oldPos)))
map[oldPos[0]][oldPos[1]] = TELEPORTATION
}
// Check if the user has won
// Note : The user win the game if there is no food left
// So we search for a line in the map where there is food
// If none is found, it means that the user has won the game
//
// Optimization : We do it only if the user has just increazed its
// size => the snake has eaten a piece of food
if(increaseSize && map.find((row) => row.find((el) => el == FOOD) != undefined) == undefined) {
winGame()
return;
}
}
// Refresh score
scoreTarget.innerHTML = score;
// Redraw screen
ctx.clearRect(0, 0, canvas.width, canvas.height)
let pattern = ctx.createPattern(imgGrass, 'repeat');
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// First 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.drawImage(imgWall, x*cell_width, y*cell_height, cell_width, cell_height);
break;
case FOOD:
ctx.drawImage(imgApple, x*cell_width, y*cell_height, cell_width, cell_height);
break;
case TELEPORTATION:
ctx.fillStyle = "gray";
ctx.fillRect(x*cell_width, y*cell_height, cell_width, cell_height)
break;
case ICE:
case ICE_SNAKE:
ctx.drawImage(imgIce, x*cell_width, y*cell_height, cell_width, cell_height);
// If the snake is on the cell, we must render it too.
// That's why I put a condition to this break
if(map[y][x] == ICE)
break;
case SNAKE:
ctx.drawImage(imgSnakeBody, 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 clear the rectangle and put its head instead
const headPos = snake[snake.length-1];
if(headPos[0] == y && headPos[1] == x) {
ctx.clearRect(x*cell_width+1, y*cell_height+1, cell_width-2, cell_height-2);
// Restore the background
if(map[y][x] == ICE_SNAKE)
ctx.drawImage(imgIce, x*cell_width+1, y*cell_height+1, cell_width-2, cell_height-2);
else
ctx.drawImage(imgGrass, x*cell_width+1, y*cell_height+1, cell_width-2, cell_height-2);
// Head Orientation
if(key == "ArrowDown")
ctx.drawImage(imgSnakeHeadDown, x*cell_width, y*cell_height, cell_width, cell_height);
if(key == "ArrowUp" || !key)
ctx.drawImage(imgSnakeHeadUp, x*cell_width, y*cell_height, cell_width, cell_height);
if(key == "ArrowLeft")
ctx.drawImage(imgSnakeHeadLeft, x*cell_width, y*cell_height, cell_width, cell_height);
if(key == "ArrowRight")
ctx.drawImage(imgSnakeHeadRight, x*cell_width, y*cell_height, cell_width, cell_height);
}
break;
}
}
}
// Now, draw the grid
ctx.lineWidth = 0.3;
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)
}
// Manage automated acceleration of snake
if(level.hasOwnProperty("acceleration"))
currDelay -= level.acceleration;
if(level.hasOwnProperty("minDelay") && currDelay <= level.minDelay)
currDelay = level.minDelay
setTimeout(() => step(), currDelay);
}
/**
* Call this function once the user loose the game
*/
function gameOver() {
audioEl.pause();
playAudio("assets/gameOver.mp3");
alert("Game over !!! (Score: " + score + ")");
location.href = "#";
}
/**
* Call this function when the user win the game
*/
function winGame() {
audioEl.pause();
playAudio("assets/win.mp3");
alert("You win !!!");
location.href = "#";
}
// Listen for key press events
document.body.addEventListener("keydown", (ev) => {
if(!canvas.isConnected)
return;
if(["ArrowDown", "ArrowLeft", "ArrowRight", "ArrowUp"].includes(ev.key)) {
// Check if the snake is on some ice
const headPos = snake[snake.length-1];
if(map[headPos[0]][headPos[1]] == ICE_SNAKE)
return;
key = ev.key;
}
if(ev.key == "p") {
audioEl.pause()
alert("Game paused. Close this dialog to resume");
audioEl.play()
}
});
// 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{
if(gameID == -1)
showRulesScreen();
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>" +
"<li><a href='#"+index+"'>" +
" Level " + index + "</a></li>";
}
// Stop game
stopGameBtn.addEventListener("click", () => {
location.href = "#";
})
// Refresh current window
changeWindow();