SnakeGame/script.js

548 lines
16 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++;
2020-03-29 14:59:56 +00:00
const ICE = i++;
const ICE_SNAKE = i++; // If the snake is on the ice
2020-04-01 11:11:32 +00:00
const TELEPORTATION = i++;
2020-03-23 13:46:35 +00:00
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 17:33:33 +00:00
/**
* Play an audio file
*
* @param {String} url The URL of the audio file to play
2020-03-23 17:42:31 +00:00
* @returns {HTMLAudioElement} Generated audio element
2020-03-23 17:33:33 +00:00
*/
function playAudio(url) {
const audio = document.createElement("audio");
audio.src = url
audio.play();
2020-03-23 17:42:31 +00:00
return audio;
2020-03-23 17:33:33 +00:00
}
2020-03-23 13:09:28 +00:00
// Get elements
const startScreen = byId("startScreen")
2020-03-27 16:39:47 +00:00
const rulesScreen = byId("rulesScreen")
const rulesBtn = byId("rulesBtn")
const foodList = byId("foodList")
const wallList = byId("wallList")
const gameScreen = byId("gameScreen")
2020-03-23 13:09:28 +00:00
const levelChoicesTarget = byId("levelChoice")
2020-03-27 16:39:47 +00:00
const stopGameBtn = byId("menuBtn")
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
2020-03-27 16:39:47 +00:00
// Get map texture
const imgGrass = new Image();
2020-03-31 14:30:07 +00:00
imgGrass.src = './assets/map/grass.png'
imgGrass.title = "It is just grass.."
const imgIce = new Image();
2020-03-31 14:30:07 +00:00
imgIce.src = './assets/map/ice.jpg'
imgIce.title = "Ice is a slippery slope.. You can not turn on it !"
2020-04-15 13:01:55 +00:00
const imgPortal = new Image();
imgPortal.src = './assets/map/portal.gif'
imgPortal.title = "This portal can transport you into the next portal."
2020-03-27 16:39:47 +00:00
// Get wall texture
2020-03-25 17:17:27 +00:00
const imgWall = new Image();
2020-03-31 14:30:07 +00:00
imgWall.src = './assets/wall/wall.jpg'
imgWall.title = "It is a brick wall.. Do not try to destroy it with your snake !"
2020-03-27 16:39:47 +00:00
// Get food texture
2020-03-25 17:17:27 +00:00
const imgApple = new Image();
2020-03-31 14:30:07 +00:00
imgApple.src = './assets/food/apple.png'
imgApple.title = "Food is food, go get the apple !"
const imgCherry = new Image();
2020-03-31 14:30:07 +00:00
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..."
2020-03-27 15:17:15 +00:00
2020-03-27 16:39:47 +00:00
// Get snake texture
2020-03-27 15:17:15 +00:00
let imgSnakeHeadUp = new Image();
2020-03-31 14:30:07 +00:00
imgSnakeHeadUp.src = './assets/snake/snake_head_up.png'
2020-03-27 15:17:15 +00:00
let imgSnakeHeadLeft = new Image();
2020-03-31 14:30:07 +00:00
imgSnakeHeadLeft.src = './assets/snake/snake_head_left.png'
2020-03-27 15:17:15 +00:00
let imgSnakeHeadRight = new Image();
2020-03-31 14:30:07 +00:00
imgSnakeHeadRight.src = './assets/snake/snake_head_right.png'
2020-03-27 15:17:15 +00:00
let imgSnakeHeadDown = new Image();
2020-03-31 14:30:07 +00:00
imgSnakeHeadDown.src = './assets/snake/snake_head_down.png'
const imgSnakeBody = new Image();
2020-03-31 14:30:07 +00:00
imgSnakeBody.src = './assets/snake/snake_body.png'
2020-03-25 17:17:27 +00:00
2020-03-23 13:09:28 +00:00
/**
* Show main screen
*/
function showMainScreen() {
startScreen.style.display = "unset";
gameScreen.style.display = "none";
2020-03-27 16:39:47 +00:00
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);
2020-03-31 13:50:06 +00:00
// Display map texture
2020-03-31 14:30:07 +00:00
mapList.appendChild(imgGrass)
2020-03-31 13:50:06 +00:00
mapList.appendChild(imgIce);
2020-04-15 13:01:55 +00:00
mapList.appendChild(imgPortal);
2020-03-23 13:09:28 +00:00
}
2020-03-27 16:39:47 +00:00
2020-03-23 13:09:28 +00:00
/**
* 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-27 16:39:47 +00:00
rulesScreen.style.display = "none";
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;
}
2020-03-23 17:42:31 +00:00
// Start audio
const audioEl = playAudio("/assets/game.mp3")
audioEl.loop = true;
2020-03-23 13:16:25 +00:00
// 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
2020-03-25 15:10:51 +00:00
/** @type {Array<Array<Number>>} */
2020-03-23 13:46:35 +00:00
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
2020-04-01 11:11:32 +00:00
// Walls
2020-03-23 13:46:35 +00:00
level.walls.forEach((w) => {
map[w[0]-1][w[1]-1] = WALL
})
2020-04-01 11:11:32 +00:00
// Food
2020-03-23 13:46:35 +00:00
level.food.forEach((f) => {
map[f[0]-1][f[1]-1] = FOOD
})
2020-04-01 11:11:32 +00:00
// Ice
2020-03-29 14:59:56 +00:00
if(level.hasOwnProperty("ice"))
level.ice.forEach((f) => {
map[f[0]-1][f[1]-1] = ICE
})
2020-04-01 11:11:32 +00:00
// Teleportation
const teleportationCells = []
2020-04-01 11:11:32 +00:00
if(level.hasOwnProperty("teleportation"))
level.teleportation.forEach((f) => {
map[f[0]-1][f[1]-1] = TELEPORTATION
2020-04-01 11:25:21 +00:00
teleportationCells.push([f[0]-1,f[1]-1]);
2020-04-01 11:11:32 +00:00
})
2020-03-29 14:59:56 +00:00
2020-03-23 13:46:35 +00:00
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...
*/
2020-03-23 17:04:59 +00:00
let currDelay = level.delay;
setTimeout(() => step(), currDelay);
2020-03-23 13:46:35 +00:00
function step() {
2020-03-23 14:37:56 +00:00
// Check if a game was destroyed
2020-03-23 17:42:31 +00:00
if(!canvas.isConnected) {
audioEl.pause();
2020-03-23 17:04:59 +00:00
return;
2020-03-23 17:42:31 +00:00
}
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 17:35:49 +00:00
playAudio("assets/eat.mp3")
2020-03-23 16:39:20 +00:00
score++;
2020-03-23 14:37:56 +00:00
break;
2020-04-01 11:25:21 +00:00
case TELEPORTATION:
2020-04-01 11:32:53 +00:00
playAudio("assets/teleportation.mp3");
2020-04-01 11:25:21 +00:00
// 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;
2020-03-29 14:59:56 +00:00
case ICE_SNAKE:
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);
2020-03-29 14:59:56 +00:00
map[newHead[0]][newHead[1]] = map[newHead[0]][newHead[1]] == ICE ? ICE_SNAKE : SNAKE
2020-03-23 14:37:56 +00:00
// Remove the end of the snake if he has not eaten anything
if(!increaseSize) {
const oldPos = snake.shift()
2020-03-29 14:59:56 +00:00
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
2020-04-01 11:25:21 +00:00
if(teleportationCells.find((e) => e[0] === oldPos[0] && e[1] === oldPos[1]) != undefined)
map[oldPos[0]][oldPos[1]] = TELEPORTATION
2020-03-23 14:37:56 +00:00
}
2020-03-25 15:10:51 +00:00
// 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;
}
2020-03-23 14:37:56 +00:00
}
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)
let pattern = ctx.createPattern(imgGrass, 'repeat');
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, canvas.width, canvas.height);
2020-03-23 13:46:35 +00:00
// First 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:
2020-03-25 17:17:27 +00:00
ctx.drawImage(imgWall, x*cell_width, y*cell_height, cell_width, cell_height);
2020-03-23 13:46:35 +00:00
break;
case FOOD:
2020-03-25 17:17:27 +00:00
ctx.drawImage(imgApple, x*cell_width, y*cell_height, cell_width, cell_height);
2020-03-23 13:46:35 +00:00
break;
2020-04-01 11:11:32 +00:00
case TELEPORTATION:
2020-04-15 13:01:55 +00:00
ctx.drawImage(imgPortal, x*cell_width, y*cell_height, cell_width, cell_height);
2020-04-01 11:11:32 +00:00
break;
2020-03-29 14:59:56 +00:00
case ICE:
case ICE_SNAKE:
ctx.drawImage(imgIce, x*cell_width, y*cell_height, cell_width, cell_height);
2020-03-29 14:59:56 +00:00
2020-03-29 15:05:54 +00:00
// If the snake is on the cell, we must render it too.
// That's why I put a condition to this break
2020-03-29 14:59:56 +00:00
if(map[y][x] == ICE)
break;
2020-03-23 13:46:35 +00:00
case SNAKE:
ctx.drawImage(imgSnakeBody, x*cell_width, y*cell_height, cell_width, cell_height);
2020-03-23 14:17:50 +00:00
// Check if it is the head of the snake
// If it is the case, we clear the rectangle and put its head instead
2020-03-23 14:17:50 +00:00
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-27 15:41:17 +00:00
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);
2020-03-27 15:17:15 +00:00
2020-03-27 15:41:17 +00:00
// Head Orientation
2020-03-27 15:17:15 +00:00
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);
2020-03-23 14:17:50 +00:00
}
break;
2020-03-23 13:46:35 +00:00
}
}
}
// 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)
}
2020-03-25 15:10:51 +00:00
2020-03-23 17:04:59 +00:00
2020-03-23 17:25:46 +00:00
// Manage automated acceleration of snake
2020-03-23 17:04:59 +00:00
if(level.hasOwnProperty("acceleration"))
currDelay -= level.acceleration;
if(level.hasOwnProperty("minDelay") && currDelay <= level.minDelay)
currDelay = level.minDelay
2020-03-23 17:25:46 +00:00
2020-03-23 17:04:59 +00:00
setTimeout(() => step(), currDelay);
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() {
2020-03-23 17:42:31 +00:00
audioEl.pause();
2020-03-23 17:33:33 +00:00
playAudio("assets/gameOver.mp3");
2020-03-25 19:46:59 +00:00
alert("Game over !!! (Score: " + score + ")");
2020-03-23 14:37:56 +00:00
location.href = "#";
}
2020-03-25 15:10:51 +00:00
/**
* Call this function when the user win the game
*/
function winGame() {
audioEl.pause();
playAudio("assets/win.mp3");
alert("You win !!!");
location.href = "#";
}
2020-03-23 14:37:56 +00:00
// Listen for key press events
document.body.addEventListener("keydown", (ev) => {
2020-03-25 16:57:55 +00:00
if(!canvas.isConnected)
return;
2020-03-29 15:05:54 +00:00
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;
2020-03-23 14:37:56 +00:00
key = ev.key;
2020-03-29 15:05:54 +00:00
}
2020-03-23 16:56:08 +00:00
2020-03-23 17:42:31 +00:00
if(ev.key == "p") {
audioEl.pause()
2020-03-23 16:56:08 +00:00
alert("Game paused. Close this dialog to resume");
2020-03-23 17:42:31 +00:00
audioEl.play()
}
2020-03-23 14:37:56 +00:00
});
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);
2020-03-27 16:39:47 +00:00
else{
if(gameID == -1)
showRulesScreen();
else
showMainScreen();
}
2020-03-23 13:09:28 +00:00
}
// 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>" +
2020-03-23 17:25:46 +00:00
"<li><a href='#"+index+"'>" +
" Level " + index + "</a></li>";
2020-03-23 13:09:28 +00:00
}
// Stop game
stopGameBtn.addEventListener("click", () => {
location.href = "#";
})
// Refresh current window
changeWindow();