/** * 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) } /** * 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 gameScreen = byId("gameScreen") const levelChoicesTarget = byId("levelChoice") const stopGameBtn = byId("stopGameBtn") const canvasTarget = byId("canvasTarget") const scoreTarget = byId("scoreTarget") // Get images const imgGrass = new Image(); imgGrass.src = './assets/grass.png'; const imgWall = new Image(); imgWall.src = './assets/wall.jpg'; const imgApple = new Image(); imgApple.src = './assets/apple.png'; 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"; } /** * 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; } // 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>} */ 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 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 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 } // 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 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) } // 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.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 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 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; } } } // 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)) 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 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 += "
  • " + "
  • " + " Level " + index + "
  • "; } // Stop game stopGameBtn.addEventListener("click", () => { location.href = "#"; }) // Refresh current window changeWindow();