In my last post I showed you guys the minesweeper game I built using JavaScript, and before that I built tic tac toe. For this week’s post I decided to show you guys how to build snake with JavaScript.
Here are the three files necessary for creating the snake game:
Now, the DOM was not originally designed for quickly updating graphics. There are better mediums to work in, but I wanted build something using basic web technologies. Still, I wanted it to be a little more dynamic than the games featured in my last tutorials. The snake game seemed to strike that balance well.
I also wanted to incorporate some of the feedback I received on my last post. I tried to use more of the modern features that JavaScript provides us, and I left jQuery behind.
With that out of the way, let’s get started. If you want to learn how to build snake with JavaScript, the first step is understanding how the game works. Let’s jump right in and talk about the rules.
Rules of the Game
- The board is grid of cells through which the snake can traverse.
- The player controls the head of the snake. It can only move in four directions: left, right, up, or down.
- The snake is always moving, and the body follows the path set by the head.
- Running into a wall or the tail of the snake causes the player to lose the game.
- The goal of the game is to eat as much food as possible.
- The snake grows by one in length whenever it eats a piece of food. Then, the game places a new piece of food in a random cell of the board.
Enums
JavaScript doesn’t really have the concept of an enum built in, but we can make our own version that will suffice. We are going to create two of them. They will prove themselves useful throughout the rest of our programming task.
Cell Type
The first enum to discuss is the CELL_TYPE. This an object containing three possible values: EMPTY, FOOD, and SNAKE. These are the only three values that can be assigned to a cell of the game board.
We declare it using the const keyword, which means we can’t assign the CELL_TYPE variable a different value. We also freeze the object to further lock it down. Our aim here is to create an immutable object. If the purpose of an object is to provide constant values we might as well try to prevent somebody else from changing it. This implementation gets us pretty far down that road while still being readable.
Direction
The second enum to discuss is the DIRECTION. This an object containing five possible values: NONE, LEFT, RIGHT, UP, and DOWN. These represent the directions in which the snake can be traveling. Its structure is very similar to the CELL_TYPE enum.
Objects
Our game consists of four main objects: the cell, the board, the snake, and the game. We have many cell objects that make up our board, but we only have one board, snake, and game object. Let’s discuss the functions we will use to create these objects.
Cell
We call the Cell function to create a very simple object representing one cell of our game board. The board consists of a grid of cells, and each cell has a few properties:
- row: An integer representing which row on the board the cell belongs to.
- column: An integer representing which column on the board the cell belongs to.
- cellType: The value associated with the cell. This could be one of three values: EMPTY, SNAKE, or FOOD. These values come from the CELL_TYPE enum we discussed earlier.
Board
Okay now let’s talk about the board. The Board function is a blueprint for creating our board object. The board object we create should contain all of the logic for representing and modifying our game board.
The function takes two parameters: rowCount and columnCount. We use these two parameters to specify the size of the game board. We initialize our lone internal variable using these parameters, and end up with a two dimensional array named cells. Then we loop through our cells variable and create a cell object for each slot in the array. When starting out, each cell should be EMPTY.
That takes care of the board initialization. Now let’s talk about the methods and data that we publicly expose.
Public Methods of the Board
- cells: Our two dimensional array of cells. We just talked about how to initialize the cells variable, and now we need to allow other parts of our program to access it.
- render: This method handles the graphics of our game. It updates the HTML document on every iteration of the game loop. Each cell has the appropriate CSS class added or removed based on its cell type.
- placeFood: This method randomly places food on the board. It leverages a private function we created, called getAvailableCells. This private function grabs a list of currently empty cells. Then, we place food in a randomly chosen one. We call placeFood as soon as the snake has enjoyed a nice warm meal.
- getColumnCount: A simple method that returns the number of columns. You may be wondering why we don’t just expose the variable itself, like we did with our cells array. The reason is because we pass this number by value, not reference. When we return the cells object, we are passing by value as well, but the value happens to be a reference to the object. For further details about this topic, see this post and this other post.
- getRowCount: A simple method that returns the number of rows.
Now let’s talk about our snake object.
Snake
The Snake function is a blueprint for creating our snake object. The snake object we create should contain all of the logic for representing and modifying our snake.
We pass three arguments to our Snake function: cell, startingLength, and board. The cell is the starting Cell of the head of the snake, the startingLength is the initial size of the snake, and the board is the game board object that we have created by calling the Board function.
We use these three arguments to initialize two internal variables of our snake object: its head cell and its snakeParts array. We simply assign the head variable to the cell that we passed in, and then set its cell type to SNAKE.
For the snakeParts, we first push the head of the snake onto the array. Then, using a for loop, we grab the cell from the next row of the board, set its cell type to SNAKE, and push it onto the array. We do this until the length of our snake matches the startingLength. This results in a vertical snake whose head is closest to the top of the game board. Of course, we are free to change this to suit our preferences.
Before moving on to talk about the publicly exposed methods of our snake, I’d like to point something out explicitly. We have designed the board and snake objects to share the same cell objects. This is an important detail to grasp if you want to understand how these objects work together.
Public Methods of the Snake
- grow: This method simply pushes the internal head variable onto the internal snakeParts array. We call it after the snake eats a piece of delicious food.
- move: This method is the snake’s most complicated one. First, it removes the tail of our snake and sets its cell type to EMPTY. Next, it sets the head of the snake to the cell being moved into, and sets that cell’s type to SNAKE. Finally, it loops through all of the snakeParts and sets the cell type to SNAKE for each. This last step handles the situation where the snake is moving right after growing in size. We call the move method once per iteration of our game loop.
- checkCrash: This method checks if our snake has crashed into a wall or itself. If the cell is undefined then we know the snake has stepped out of the bounds of the game board and crashed. If the cell is not undefined then we check if the snake has collided with itself.
- getHead: A simple getter for our head variable.
Now let’s talk about our game object.
Game
The Game function is a blueprint for creating our game object, and the game object we create should contain all of the logic for handling the interactions between the snake and the game board. We pass the snake and board objects to the Game function as arguments.
While the publicly exposed methods of our game object are quite complex, the initialization of the object is very simple. We have four internal variables: directions, direction, gameOver, and score.
The directions array starts out empty. We use it to queue up directions that the player has pressed. The queue helps us avoid frustrating behavior when a player inputs multiple directions before the game is able to update. We do not want to punish the player for quick fingers! Using a single variable to store the direction would result in a pretty annoying control scheme.
We set the initial direction of the snake to NONE. This makes the snake motionless at the start of a new game. The directions array will be used to feed in new values to this variable.
We initialize the gameOver variable to false. We don’t want to end the game before it has begun.
Finally, we initialize the score variable to zero. We will increase the score as the snake eats food.
Public Methods of the Game
- update: This method puts all of the pieces together. We call it once per iteration of the game loop, and it contains the bulk of the logic needed for the snake and the board to work together. After checking to make sure the game is still going on, this method calls the private getNextCell function. This function figures out which direction the snake is headed, removes that direction from the queue, and returns the cell that the snake is about to enter. Then, the method checks if the snake has crashed. If the snake has crashed, we end the game and display the final score. Otherwise, the snake moves to the next cell. If there was food there, the snake eats it and grows and we place another piece of food on the board.
- addDirection: This method takes a direction as an argument and pushes it onto the directions queue.
- getLastDirection: This method returns the value of the last direction in the directions queue.
- exceededMaxDirections: We set the maximum number of directions allowed in the queue to three. This prevents the player from spamming the arrow keys a huge number of times and building up a list of directions that are no longer relevant.
Snake Game Initialization
So far we have covered the building blocks necessary for putting together a game of snake with JavaScript. However, we still have a few more pieces to talk about that revolve around initializing a new game. These pieces include initializing the HTML, initializing the input handler, and starting a new game.
One thing to keep in mind while reading this code is that we want to be able create a fresh game state with ease. Modifying our objects directly seems cumbersome and error prone if we make more changes later. Instead, we are going to throw away our old objects and create new ones.
Initializing the HTML
The purpose of this function is to initialize the HTML elements that represent our snake game board. We start by grabbing all of the elements containing the “cell” CSS class. This should get us all of the cells. Then, we loop through each one of them.
Each cell has its id set to a value that represents its row and column number. Also, we remove the CSS classes from each cell and then add the “cell” class back. This ensures that we remove any classes that were added by the previous run of the game.
Initializing the Input Handler
The purpose of the listenForInput function is to handle the keyboard input provided by the player. For our game, the controls are the arrow keys. This function takes the game object as an argument.
The bulk of the logic takes place inside the changeDirection function. This function is responsible for determining when to add a direction to the directions queue of the game object. We add an event listener to our document that executes the changeDirection function on every keydown event. We clear and reset it each time we call the listenForInput function.
The first time changeDirection is triggered we add the UP direction to the queue. This helps us ensure that the player will not die on the first move. On subsequent calls to the function we listen for arrow key presses.
When the snake is moving vertically we do not allow the player to add vertical directions, and when the snake is moving horizontally we do not allow the player to add horizontal directions. This prevents the player from building up a queue of redundant directions or killing the snake inadvertently. We also prevent adding directions to the queue if it is already full. The movingHorizontally and movingVertically helper functions handle these checks, and make use of the publicly exposed methods of the game object to do so.
Starting A New Game
Alright we are getting close now. Just a few more steps and you will have all of the pieces for learning how to build snake with JavaScript! Let’s talk about how to start a new game using all of the building blocks we have constructed thus far.
We have a function called newGame that we call to start a new game. Inside this function we need to set up some constants for the row and column count of our board, as well as for the starting length of our snake. We will use the constants to initialize our game objects.
Once we have our constants, the first thing we do is initialize our board by calling the Board function and passing in our row and column counts as arguments. Then, we use those counts to calculate a good starting position for the head of our snake. We could hard-code this value, but I chose to programmatically place the snake’s head in the center of the board. We pass in this center cell, the board, and the starting length value to the Snake function to create our snake object. Finally, we use the board and snake objects to create our game object using the Game function.
Next, we call initializeCells to assign ids and CSS classes to our HTML cells (represented by div tags). Then we placeFood on the board and call render to show it to the player. After that we call listenForInput to start handling the player’s keyboard input. Finally, we start our game loop. On every iteration we update the board and then render the results on the screen.
The last bit of code attaches a click handler to a button in our pop up modal. We use this click handler for starting another game once the last one has ended. It hides the modal, stops the game loop, and calls the newGame function. We call the newGame function as the first thing we do, and then again every time the player clicks the play again button in the modal.
Conclusion
Congrats on learning how to build snake with JavaScript! That was a lot of code, but hopefully it makes sense after breaking it up into modules like this. If you have questions or see ways to improve the code, I’d love to hear from you in the comments!
I hope you guys enjoyed learning about how to build snake with JavaScript. Let me know what other simple games like this you would like to see, and don’t forget to drop me your email so you don’t miss the next one that comes out.
Take care, and God bless!
tҺe website іѕ really good, I really like your site!