/** * 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 !" const imgPortal = new Image(); imgPortal.src = './assets/map/portal.gif' imgPortal.title = "This portal can transport you into the next portal." // 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); mapList.appendChild(imgPortal); } /** * 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>} */ 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([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 TELEPORTATION: playAudio("assets/teleportation.mp3"); // Teleport the snake to the other cell const possibleDestinations = teleportationCells.filter((v) => v[0] != newHead[0] && v[1] != newHead[1]); const chosenDestination = possibleDestinations[randInt(0, possibleDestinations.length)]; newHead[0] = chosenDestination[0] newHead[1] = chosenDestination[1] 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.find((e) => e[0] === oldPos[0] && e[1] === oldPos[1]) != undefined) 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.drawImage(imgPortal, 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 += "
  • " + "
  • " + " Level " + index + "
  • "; } // Stop game stopGameBtn.addEventListener("click", () => { location.href = "#"; }) // Refresh current window changeWindow();