Classic Snake Game
Table of Contents
- Part 1 - HTML & CSS
- Part 2 - Modeling Data & jQuery
- Part 3 - Completing the Game's Logic
- TODO 6: Change the snake's direction
- TODO 7: Make the head move
- TODO 8: Check for collisions with the wall
- TODO 9: Check for collisions with the apple
- TODO 10: Handle Apple Collisions
- TODO 11: Move the body
- TODO 12: Check for snake collisions with itself
- TODO 13: Make sure our Apple is placed only in available positions
- Build an app from start to finish including writing HTML, CSS, and JavaScript
- Manipulate HTML elements using the jQuery JavaScript library
- Use keyboard inputs
- Organize code using Functions
To push to GitHub, enter the following commands in bash:
git add -A
git commit -m "saving data shapes"
git push
- Learn how the various files in a program are linked together in an index.html file
- Use jQuery to dynamically create HTML elements and modify their properties
- Practice using Objects and Arrays to model game components
- Learn about keyboard inputs
- Learn about Asynchronous function calls
- Learn how to organize code in a program
This Snake program is simple. It only has a few visual components:
- The board
- The score display
- The high score display
- The snake
- The apple
Which of these components are static and which will be dynamic?
For now, we can set up the stationary elements first. When we get to the JavaScript portion of this project we'll deal with the snake and apple.
- 1a) Between the
<body> </body>
tags add the following elements:- A
<div></div>
element with anid = "board"
attribute - An
<h1></h1>
element with anid = "score"
attribute - Another
<h1></h1>
element with anid = "highScore"
attribute
- A
<div id="board"></div>
<h1 id="score">Score: 0</h1>
<h1 id="highScore">High Score: 0</h1>
Save your code and run the index.html
file (or start your server, or refresh your running application page if it is already running).
At this point your screen should be blank except for the score and the high score.
READ: Open up the file
index.css
. Here, you'll find that we've already defined some CSS rules for the various components of the game. But why is our page unstyled?Since our server is only running the
index.html
file, it doesn't have access to theindex.css
file by default - we need to link it!
-
2a) Inside the
<head>
element, add the following HTML:<link rel="stylesheet" type="text/css" href="index.css" />
Save your code and run the index.html
file (or start your server, or refresh your running application page if it is already running).
Now, we can see the board as a square with a border! Feel free to modify this CSS to suit your own style, but be careful if you change the width or height of anything as that may cause problems later.
Now that our CSS has been linked to our HTML, we need to do the same to add JavaScript. To connect our index.js
file, we have to use the <script>
tag.
-
3a) At the bottom of the
index.html
file, below the<body></body>
tags and above the closing</html>
tag, add the following HTML:<script src="index.js"></script>
Why are we not adding this tag in the <head>
?
READ: When the browser loads our
index.html
file, it starts at the top of the file and works its way down. When it gets to an externally loaded file, it must first read through that entire file before moving on to the next line in our HTML.This becomes a problem if our JavaScript file relies on manipulating existing HTML content on the page. If those elements have not yet been loaded in yet, our JavaScript file will throw some errors. As such, we need to load this JavaScript in after our HTML
Save your code and run the index.html
file (or start your server, or refresh your running application page if it is already running).
If you open up the console, you will notice that we have some errors. This is because our index.js
file uses jQuery which we haven't loaded into the index.html
file yet.
-
3b) The jQuery code has been downloaded for you in the file
jQuery.min.js
. Back in the<head>
element, add the following HTML:<script src="jquery.min.js"></script>
Technically, this tag can be added anywhere above the index.js
file. However it is best practice to load in external scripts in the <head>
aside from the index.js
file.
Save your code and run the index.html
file (or start your server, or refresh your running application page if it is already running).
If you open the console you shouldn't see any errors now relating to jQuery (the $
symbol).
The first step in designing a piece of software is deciding on how to model the various components of the program.
At the top of the index.js
file is a setup section. All variables that your program will need should be declared there. By declaring them at the top of the program, we can easily see what data will be important to keep track of for the code to follow.
- 4a) Declare
snake
andapple
variables as empty Objects in theindex.js
file just below the"Game Variables"
comment. Also declare ascore
variable and give it the initial value of0
at this same location.
Objects are the go-to Data Type for modeling game components due to their easy-to-use key:value properties and the highly readable nature of Dot Notation.
To model the Snake and Apple, we need to ask ourselves, "what information does the snake need to know about itself to create this program?".
Modeling the Apple is quite easy. It will need the following properties:
apple.element
: A reference to the HTML element that represents the Apple.apple.row
: A reference to the row where the Apple currently exists.apple.column
: A reference to the column where the Apple currently exists.
The apple.element
Object will be needed in order to make any modifications to the Apple's HTML element using jQuery. The apple.row
and apple.column
properties will be useful for determining when the Snake collides with the Apple.
4b) Let's go ahead and create the apple right now. To do so, do the following:
1. Copy the below function into your program down in the helper functions section. The
makeApple()
function already exists, so you just need to fill in the code block for the function:/* Create an HTML element for the apple using jQuery. Then find a random * position on the board that is not occupied and position the apple there. */ function makeApple() { // make the apple jQuery Object and append it to the board apple.element = $("<div>").addClass("apple").appendTo(board); // get a random available row/column on the board var randomPosition = getRandomAvailablePosition(); // initialize the row/column properties on the Apple Object apple.row = randomPosition.row; apple.column = randomPosition.column; // position the apple on the screen repositionSquare(apple); }If you take a look at the definition of the
makeApple
function you'll see that it finds a random position for the apple by calling the functiongetRandomAvailablePosition()
. We'll get to that much later.2. Up in the
init()
function at TODO 4b-2, call themakeApple()
function.
Modeling the snake will be a bit trickier. Since the snake occupies multiple rows and columns, we will need multiple Objects to represent each part of the Snake.
We can refer to each part of the snake as a snakeSquare
Object which will have the following properties:
snakeSquare.element
: A reference to the HTML element that represents a part of the snake.snakeSquare.row
: A reference to the row where thesnakeSquare
currently exists.snakeSquare.column
: A reference to the column where thesnakeSquare
currently exists.snakeSquare.direction
: A reference to the direction that this particularsnakeSquare
is currently moving in.
Because the Snake is made up of multiple snakeSquares
that are in a particular order, we can model the Snake's body as an Array. It will also be useful to have a quick reference for the head and tail of the snake.
This data will be stored in the snake
Object:
snake.body
: An Array containing allsnakeSquare
Objects.snake.head
: Reference to the jQuerysnakeSquare
Object at the head of the snake. Duplicate ofsnake.body[0]
snake.tail
: Reference to the jQuerysnakeSquare
Object at the end of the snake. Duplicate ofsnake.body[snake.body.length - 1]
Most of this will be handled automatically, but first you'll have to create and call the functions that do that.
4c) Let's initialize the snake for the beginning of the program. To do so, do the following:
1. Copy the below function into your program down in the helper functions section. The
makeSnakeSquare()
function already exists, so you just need to fill in the code block for the function:function makeSnakeSquare(row, column) { // initialize a new snakeSquare Object var snakeSquare = {}; // make the snakeSquare.element Object and append it to the board snakeSquare.element = $("<div>").addClass("snake").appendTo(board); // initialize the row and column properties on the snakeSquare Object snakeSquare.row = row; snakeSquare.column = column; // set the position of the snake on the screen repositionSquare(snakeSquare); // if this is the head, add the snake-head id if (snake.body.length === 0) { snakeSquare.element.attr("id", "snake-head"); } // add snakeSquare to the end of the body Array and set it as the new tail snake.body.push(snakeSquare); snake.tail = snakeSquare; }Take note of two lines in particular:
snakeSquare.element = $('<div>').addClass('snake').appendTo(board);
creates a new<div>
element using jQuery and adds it to the board. Notice how we have$(<div>)
written instead of$(div)
. This is an important distinction to make, as the<>
tells jQuery to create a new element entirely.
repositionSquare(snakeSquare)
calls an already existing helper function; the function is located directly below themakeSnakeSquare()
function, and its job is to basically draw the new squae at the correct location on your screen.2. Put the following lines of code in the
init()
function at the TODO 4c-2 location:// initialize the snake's body as an empty Array snake.body = []; // make the first snakeSquare and set it as the head makeSnakeSquare(10, 10); snake.head = snake.body[0];
Most videogames are essentially animations that are interactive. Animation can be easily achieved by rapidly making changes to the appearance of the visual components on our screen.
One common method for doing this is by using a function called setInterval()
. Let's go ahead and make use of that function, then we'll talk about what it's doing.
-
5a) Copy the following code into your
init()
function at the TODO 5a prompt:// start update interval updateInterval = setInterval(update, 100);
In this case, the function
setInterval()
works by calling theupdate()
function every100
milliseconds (10 frames / second). Each time the function is called we will modify the appearance of our game. (See https://www.w3schools.com/jsref/met_win_setinterval.asp)
Next, we need to make sure that the update()
function actually has something to do. Otherwise it doesn't matter how often the setInterval()
function calls update()
, because nothing will happen if the update()
function is empty.
-
5b) Copy the body of the
update()
function into your program. Theupdate()
function already exists, so find it and fill in its code block.function update() { moveSnake(); if (hasHitWall() || hasCollidedWithSnake()) { endGame(); } if (hasCollidedWithApple()) { handleAppleCollision(); } }
On each tick of the timer we move the snake. We'll check if it has collided with the wall, itself, or the apple.
If it collides with the Apple, handle that collision (this includes lengthening the snake, adding a new apple, and increasing the score).
If it collides with itself or the wall, end the game.
As you can see, we are using a lot of functions to write out the logic. This is a strategy to make the main logic of the program readable. We'll have to work on those functions in later TODOs.
Every game provides some way for the user to have control. In this game, we will be using the keyboard to control our snake.
READ: To make our game react to keyboard inputs, we can again use jQuery! Find this line of code in the Setup section towards the top of the program:
$("body").on("keydown", handleKeyDown);This makes the
body
HTML element listen for akeydown
event, and when that event occurs, thehandleKeyDown()
function will be called in response. It is also important to note that this function will not be called as part of the same animation cycle in which theupdate()
function is called.
-
6a) Now,find the
handleKeyDown
function in the Helper Functions section towards the bottom. Then, plug the following two lines of code into the function's body:activeKey = event.which; console.log(activeKey);
What this code will do is save the key that has been pressed for processing later (in the
activeKey
variable), and it also prints the pressed key so that you can see in the console if the event is working.As for the
event
parameter, this contains details about the event that occurred. In the case of thekeydown
event, theevent.which
property specifically tells us the keycode of the key that was pressed.
With the activeKey
variable saving the last pressed key we can start using that information to move our snake. At first, we might think that pressing a key should move the snake. A pseudocode solution for this might look like this:
if (activeKey is the left Arrow) {
move the snake one column left
}
However, in the classic Snake game, the Arrow keys only change the direction of the snake.
Find the definition of the Function checkForNewDirection
and you'll see the comment for // TODO 6b
. We've started a solution for this function for you. You should see this code:
if (activeKey === KEY.LEFT) {
snake.head.direction = "left";
}
// console.log(snake.head.direction);
Uncomment the console.log()
, save your code and refresh your game. Then open the console
Right now, if you were to press the left arrow you would see the direction "left"
printed to the console over and over again. This is because this function is called every time the moveSnake()
function is called which is called 10 times per second by the update
function.
Try using the other arrow keys. See anything?
No? Well we haven't programmed our game to react to the other arrow keys!
- 6b) Using the
activeKey
variable and theKEY
Object (located in the "Constant Variables" section near the top of your program), programsnake.head.direction
to change according to the arrow key that is currently being pressed.
When you are finished, save your code, refresh your game and try pressing the arrow keys. Make sure that all four directions work! Finally, comment out the console.log
. We don't want to clutter the Console.
Now that we can control the direction our Snake should move in, we can actually start moving the snake. Find // TODO 7
in the moveSnake
function definition.
On each call to update
, moveSnake()
will be called. After the checkForNewDirection()
function is called (which you just programmed in TODO 6), snake.head.direction
will either be "left"
, "right"
, "down"
, or "down"
. Now, we will need to move the snake exactly 1 square in the direction that it is facing.
- 7a) Below
// TODO 7
add these lines of code:
if (snake.head.direction === "left") {
snake.head.column = snake.head.column - 1;
}
repositionSquare(snake.head);
The repositionSquare
function accepts a snakeSquare
Object and positions it on the screen according to that snakeSquare
's .row
and .column
properties. So, before we can reposition the square, we need to change either the row or column it currently is in! If we subtract 1
from the current snake.head.column
value, our snake will move 1
square to the left. *This works for one direction, but we need to do more work to handle the other directions.**
- 7b) Add three more conditional statements to determine the snake head's next row and column position based on its current row, column, and direction; you need to handle the
"right"
,"up"
, and"down"
directions.**
HINT: The top row in the board is row
0
and row numbers increase as you move down. The left-most column in the board is column0
and column numbers increase as you move to the right.
Now that our snake can move freely, we need to put some constraints on it. We don't want our snake to leave the confines of the board (sorry snake).
The next step in our game's update
logic is to check if the snake has either collided with the walls or with itself. Let's start with the walls.
Find the function hasHitWall()
.
8a) Program the function to return
true
if the snake's head has collided with any of the four walls of the board andfalse
otherwise.Use the following pieces of data to determine if the snake's head has collided with one of the walls.
ROWS; // the total number of ROWS in the board COLUMNS; // the total number of COLUMNS in the board snake.head.row; // the current row of snake.head snake.head.column; // the current column of snake.head
IMPORTANT: Test your snake's movement before moving on. It should be able to get right up against all four walls, but not go past them without dying. If it can't reach the walls, or if it can go outside of the walls, then adjust your conditions so that it can go anywhere within the box but nowhere outside of it. DO NOT move onto the next step until this is the case.
Now that our apple
has been added to the board, we need the snake to actually eat it!
Within the update
function we can see the logic for doing this:
if (hasCollidedWithApple()) {
handleAppleCollision();
}
If the snake hasCollidedWithApple
then we can handleAppleCollision
. Makes sense! Let's start with detecting collisions with the apple.
Find the definition for the function hasCollidedWithApple()
.
9a) Make the function return
true
if the snake's head has collided with the apple andfalse
otherwise.Use the following pieces of data to determine if the snake's head has collided with the apple.
apple.row; // the current row of the apple apple.column; // the current column of the apple snake.head.row; // the current row of snake.head snake.head.column; // the current column of snake.head
Save your code, refresh your game, and observe the changes! If you did this step properly then your snake should be able to eat the Apple.
You may notice that our apple eating behavior isn't perfect. Find the function
handleAppleCollision
. At this point it should have the following logic:
// increase the score and update the score DOM element
score++;
scoreElement.text("Score: " + score);
// Remove existing Apple and create a new one
apple.remove();
makeApple();
Some things are working fine - the score is increasing, the eaten apple disappears and a new one is created - however our snake isn't getting any bigger! Instead a random green square is being added in the top left corner of the screen. Why?
At the bottom of the function you can find this logic:
var row = 0;
var column = 0;
// code to determine the row and column of the snakeSquare to add to the snake
makeSnakeSquare(row, column);
As we can see, right now we are creating a new snakeSquare at position (0, 0).
-
10a) determine the
row
andcolumn
where the next snakeSquare should be placed so that it is added on to the tail of the snakeUse the
snake.tail.direction
with conditional statements to decide which way the tail is moving. Then, usesnake.tail.row
, andsnake.tail.column
(plus an offset of 1 depending on the snake's direction) to determine the correct row and column of the new piece.
HINT: If the snake's tail is moving right, the next snakeSquare should be one column to the left. If the column is moving up, the next snakeSquare should be one row below. Use this logic to figure out the location of the new piece for all four directions that the snake might be moving.
NOTE: Completing this TODO will not make your snake grow properly. It will simply create each new snakeSquare behind the snake's tail, but they won't follow it yet. Complete the next TODO to make your snake properly grow.
Find the function definition for moveSnake()
.
Our program is still not working properly. When our snake eats an apple, a new snakeSquare is added to the board in the correct location. However, each new snakeSquare does not follow the snake!
- 11a) Add this code below the comment for
TODO: 11
:
for ( /* code to loop through the indexes of the snake.body Array*/ ) {
var snakeSquare = "???";
var nextSnakeSquare = "???";
var nextRow = "???";
var nextColumn = "???";
var nextDirection = "???";
snakeSquare.direction = nextDirection;
snakeSquare.row = nextRow;
snakeSquare.column = nextColumn;
repositionSquare(snakeSquare);
}
In order for the snake to follow the head, each snakeSquare must learn the position and direction of the snakeSquare that is in front of it. Since we want to apply this same logic to every snakeSquare in the snake.body
Array, iteration using a for
loop will be very helpful!
Hint 1: The
for
loop will need to be set up in a particular way to make sure that each snakeSquare can follow the snake that comes before it without any data being prematurely overwritten. It may be beneficial to loop backwards. TO REITERATE: Whether your loop iterates from front-to-back or back-to-front is a critical consideration for this step.
Hint 2: Remember that the snake's head is the first entry in
snake.body
so make sure that your loop doesn't include index0
!
HINT 3: After making the basis your loop, think through how this process will work. How can you access each
snakeSquare
in thesnake.body
Array? How do you think we can use the index of eachsnakeSquare
to figure out what thenextSnakeSquare
should be?
- 11b) Reposition each snakeSquare in the
snake.body
Array and update the direction for each snakeSquare.
After eating a few apples your snake will be long enough to potentially run into its own body! Try doing this and you'll notice that your snake will just breeze right through. This is not what you want.
According to our logic in the update()
function, when this happens, the game is supposed to end! We need to fill out the hasCollidedWithSnake()
function so that it properly detects this collision.
Find the hasCollidedWithSnake()
function.
-
12a) Make this function return
true
if thesnake.head
has overlapped with any snakeSquare insnake.body
.What data will you need to use to solve this problem?
Hint: Remember that the snake's head is the first entry in
snake.body
so make sure that you aren't comparingsnake.head
withsnake.body[0]
!
IMPORTANT: Do NOT go past this step until it is definitely working. The code for this collision is very similar to the code you'll need to write for the last TODO, so getting this working first will really help with figuring out how to handle the final TODO.
The final problem is to make sure that when our apple is regenerated, it is positioned in a square on our screen that is actually available, and not accidentally on top of a piece of snake.
Find the function getRandomAvailablePosition()
.
Currently, this is its logic:
var spaceIsAvailable;
var randomPosition = {};
while (!spaceIsAvailable) {
randomPosition.column = Math.floor(Math.random() * COLUMNS);
randomPosition.row = Math.floor(Math.random() * ROWS);
spaceIsAvailable = true;
}
return randomPosition;
READ: Notice that spaceIsAvailable
needs to be false
for the while loop to run, and part of what the loop does is set it to be true
every time. As such, the program makes a single guess for where to put the apple and calls it a day.
You won't want to change that, but you'll need to use conditionals to set the value to false
if the apple ends up on top of the snake, as that will make the loop try placing the apple again.
-
13b) Modify the code block in the
while
loop so that if the randomly generated position is occupied by any part of the snake's body, it loops again.HINT 1: You need to make sure that the apple doesn't appear on any piece of your snake. How did you check for collisions between the head and the body? A similar approach would be fitting here, but with two differences.
- You should be comparing the
apple
to the snake pieces instead of the head. - This check should check the entire snake and not ignore the head.
HINT 2: If the first hint wasn't enough, you need to use iteration within the
while
loop. You are more than allowed to put afor
loop inside of awhile
loop if you want to, and you can put a conditional statement inside of that as well if needed. - You should be comparing the