This was the first JavaScript project of the Software Engineering Immersive course at General Assembly.
Snake is a single-player game where the player earns points by guiding the snake to eat food randomly placed on the game board. Every time the snake eats, the tail grows and the speed increases. The game is over if the snake hits the walls of the board, or itself.
- Render a game in the browser.
- Be built on a grid: do not use HTML Canvas for this.
- Design logic for winning & visually display which player won.
- Include separate HTML / CSS / JavaScript files.
- Stick with KISS (Keep It Simple Stupid) and DRY (Don’t Repeat Yourself) principles.
- Use JavaScript for DOM manipulation.
- Deploy your game online, where the rest of the world can access it.
- Use semantic markup for HTML and CSS (adhere to best practices).
This was a solo project and the timeframe was 1 week.
- HTML5
- CSS3
- JavaScript ES6+
- VSCode
- Google Fonts
- Git & GitHub
Before coding I created a wireframe in Excalidraw to have an overview of the project and try to understand the pseudo code of the game. Before planning I played the game online in order to understand what elements I needed in HTML, what variables I had to declare and what functions I needed to execute. The first step was to create the grid, then I wanted to show the score every time the snake was eating the food. Next I created the Start button to begin the game. Regarding the variables, in my mind I thought that the snake was going to be an empty array.
While I was coding, some variables and functions had changed and created a different style.
Firstly, I created the grid in JavaScript. The function created an array of divs
within a flex-container. The divs could have classes attached, which were used to change the display.
// ! Generate Grid
function createGrid() {
for (let i = 0; i < cellCount; i++) {
//console.log('Cell Created')
// Create div cell
const cell = document.createElement('div')
// Give an id to the cell
cell.id = i
// Append to grid
grid.appendChild(cell)
// Push cell into cells array
cells.push(cell)
}
}
For adding the snake, I looped the currentSnake variable and used a forEach
loop and added a new class. Through CSS, I was able to add the colour to the snake.
The function removeSnake
when applied removes the class “snake” from each cell in the currentSnake array. The latter contains the indexes of each cell that the snake currently occupies on the game board. The function iterates over the currentSnake array using a loop. For each element the function targets the corresponding cell in the cell array, using the index value and then removes the class “snake” from that cell using the classList.remove()
method. While I was creating the game the colours of the snake would disappear.
function addSnake() {
//doing a forEach loop, I can add the color to the snake and the class
currentSnake.forEach(index => {
//adding the class of snake
cells[index].classList.add('snake')
})
}
function removeSnake() {
// target the currentSnake and remove the snake class
currentSnake.forEach(index => {
cells[index].classList.remove('snake')
})
}
A function was created to handle the creation and placement of food on the grid. Thanks to this function, food will be generated randomly on the grid while the user is playing.
function addFood() {
//generate a random index with Math.floor(Math.random)
randomCellFood = Math.floor(Math.random() * cells.length)
//cells is an array. Doing the below, I can add the class 'food'
cells[randomCellFood].classList.add('food')
//console.log(currentSnake)
}
This function changes the snake’s direction based on the arrow keys that are pressed on the keyboard. I created a global variable called let direction = 1
, which represents the default direction that is right. Then I created an if statement that controls the different directions and the snake direction was captured using an event listener.
To prevent the snake going immediately to the opposite direction, in the if statement I included that if the snake is going right cannot go left and so on.
function direction(e) {
const right = 39
const left = 37
const up = 38
const down = 40
if (e.keyCode === right && snakeDirection !== -1) {
snakeDirection = +1
//console.log('right')
} else if (e.keyCode === left && snakeDirection !== +1) {
snakeDirection = -1
//console.log('left')
} else if (e.keyCode === up && snakeDirection !== +width) {
snakeDirection = -width
//console.log('up')
} else if (e.keyCode === down && snakeDirection !== -width) {
snakeDirection = +width
//console.log('down')
}
}
The crucial function in this game has been the moving function to let the snake move around the grid. This function includes the collision with the wall as well. I have used an if statement for the movements and if the snake hits the wall the game is over.
function moveSnake() {
const head = currentSnake[0]
if (snakeDirection === 1 && head % width === width - 1) {
gameOver()
//clearInterval(timer)
//console.log('die going right')
} else if (snakeDirection === -1 && head % width === 0) {
gameOver()
//clearInterval(timer)
//console.log('die going left')
} else if (snakeDirection === -width && head <= width) {
gameOver()
//clearInterval(timer)
//console.log('die going up')
} else if (snakeDirection === +width && (head + width) >= cellCount) {
gameOver()
However, if the snake doesn’t hit the wall I can move to the second part of the statement, which is the else
. I manipulated the DOM, decrementing the tail and removing the class of ‘snake’. Then I used the method pop()
for removing the tail from the end of the array and used the method unshift()
, which adds the cell to the beginning of the array (the head). Then I added the class of ‘snake’ to make it visible on screen.
} else {
//manipulate the DOM decrementing the tail and removing that class
cells[currentSnake[currentSnake.length - 1]].classList.remove('snake')
//Removing from the end of the array
currentSnake.pop()
//Adding to the start of the array, which is the head
currentSnake.unshift(head + snakeDirection)
//add the class list of snake
cells[currentSnake[0]].classList.add('snake')
//currentSnake.forEach(s => cells[s].classList.add('snake'))
}
A grow tail function was created to grow the tail when the food was eaten. This was done by adding the class ‘snake’ and made the array bigger every time this happened.
function growTail() {
//grow tail when the food is eaten
//grow snake by adding the class of snake
// grow the tail and the array needs to get bigger too
currentSnake.unshift(currentSnake[0] + snakeDirection)
cells[currentSnake[0]].classList.add('snake')
}
To let the snake move independently, a moveSnakeTimer()
function was created. A setInterval method was used in conjunction with the movement of the snake and intervalTime
that was set to half a second.
function moveSnakeTimer() {
timer = setInterval(() => {
removeSnake()
moveSnake()
addSnake()
}, intervalTime)
}
The speed increased every time the snake was eating food. The clearInterval
were required to stop the issue of the snakes’ speed increasing exponentially. The intervalTime
was set to a certain speed every time the snake eats food.
function speed() {
//Select the speed of the snake using setInterval().
clearInterval(timer)
intervalTime *= 0.7
timer = setInterval(moveSnake, intervalTime)
//console.log('interval')
}
Finally, the function reset()
which resets the entire game before clicking on the button start. A computer is not like a human brain, on the contrary I had to code everything that needs to happen. Therefore I had to clear all the previous intervals, such as the speed after eating the food. Remove the snake from the screen, so the user is able to start from a clean board. Reset the snake to the start position once the user clicks on the button start and at the same time remove the food that is generated randomly. The score needs to go back to 0 and remove game-over from the screen.
function reset() {
// Cleanup in case a previous interval is running
// Clear timer interval
clearInterval(timer)
// Remove the snake
removeSnake()
//reset array current snake
currentSnake = [6, 5, 4]
//remove food
removeFood()
//reset score variable
currentScore = 0
//reset the DisplayScore on the screen
scoreDisplay.innerText = 0
//reset Game Over on the screen
killGame.innerText = ''
//remove the the grid shake
grid.classList.remove('shake')
}
- Since this was the first project, initially I found the process rather daunting. Although I had been taught the skills and tools that were needed to build the game, having to start from a blank page was definitely a challenge. Taking the time to think and plan on paper enabled me to face and overcome this challenge. Looking back over my code I can see that some of it could be refactored (i.e. the snake movement), to make it DRY, however, due to the time constraints and given that it was my first project, I was happy with the outcome.
- Without any doubt I learned how to manipulate the DOM and it will stay vivid in my mind. When I added the snake, I could not see it on the screen. I checked the code multiple times and only after some time I realised that I wasn’t manipulating the DOM.
- I am quite proud of the design of the game.
- Getting the game working.
- Consolidated my knowledge of JavaScript, CSS and HTML. Some of the concepts are much clearer now.
- I believe that only with a lot of practice I will be able to improve JavaScript and all the methods.
- The process of making this game from scratch has been very thrilling, especially when the snake started moving.
- Planning and pseudocode is vital. It is very tempting to start coding, however, taking time to plan properly will save you time later.
- If you play the game once, everything is absolutely fine. However, when you play it the second time the snake goes into the direction that went previously. - Fixed 🚀
- Make the game responsive.
- Create more obstacles on the board to make the game slightly harder.