1 ECS Taster 2023 Electronics and Computer Science Taster Course Southampton Snake An Introduction to Programming in JavaScript 2 ECS Taster Lab Notes Southampton Snake An Introduction to Programming in JavaScript Version 1.6 (July 27, 2023) Written by Professor David Millard Web and Internet Science Research Group Electronics and Computer Science The University of Southampton dem@ecs.soton.ac.uk http://www.davidmillard.org @hoosfoos Acknowledgments Southampton Snake is based on original code published by thecodeplayer.com Front image ‘Ouroboros by Zanaq’ (CC BY-SA 2.5) available from https://commons.wikimedia.org/wiki/File:Ouroboros-Zanaq.png Copyright ©2023 The University of Southampton 3 Introduction JavaScript is a programming language for writing code that runs within a web browser, it is used on millions of websites to create dynamic effects (such as menus that open and close, pictures that change, or forms that alter themselves according to what you type). However, it is a much more powerful language than this suggests, and in this lab we are going to look at how it can be used to create a simple game. Javascript is part of the C family of languages (alongside other big names such as C, C++, Objective C, C# and Java) and shares a lot of its syntax with these other languages. The C family of languages contain some of the most popular types of programming language, representing over 50% of all programming activity according to the TIOBE index1. Javascript is the most forgiving of all these languages, as it is weakly typed (does not care about the type of variables), is prototype-based (you can define and declare new objects at the same time), and supports a number of different programming paradigms (styles) including procedural and object-oriented. It means you can get a lot done relatively quickly. Javascript is therefore a fun language to learn and experiment with. It also helps that it requires no specialist tools (you can edit the files in any text editor, and run them in any browser). Introducing Snake Today we are going to look at a simple web browser game written in Javascript. The game is called Snake, and originates from arcade machines in the 1970s. It became well known in the 1990s when it was bundled with some of the early Nokia phones. The game is simple; you guide a snake around a grid. Food appears on the grid and when you collide with it your snake grows and you gain points. However, if you collide with the walls or with yourself the game ends. You can try a simple online version of the game here: 1 The TIOBE Programming Community index is an indicator of the popularity of programming languages, based on the number of skilled engineers world-wide, courses and third party vendors (www.tiobe.com). 4 http://playsnake.org Introducing Southampton Snake To run this lab you will need a web browser to load the web page (.html), and a text editor to edit the javascript file (.js); there is one on the University machines called Notepad++ that you can use for the lab. You should download the two files you need from the site below (you can do this by right clicking on them): http://bit.ly/soton-snake These files are: • snakecore.js – this is a JavaScript file that deals with all the drawing of the game, and sets up the main game loop. We will be looking at this file, but you do not have to edit it to complete the lab • snakepage.html – this is a HTML file that sets up the webpage. It also contains the JavaScript that runs the logic of the game. This logic is incomplete, and our job in this lab is to fix it, and learn some JavaScript along the way. Download both files to the same directory and then open a file explorer and change to that directory. The game should work as long as both files are in the same directory. Double- click snakepage.html to load it into Internet Explorer. You should see the following: 5 This is how our game begins. With a Snake that only moves right (reload the page to see it again as it disappears off the side of the grid), and food we cannot eat. Let’s see if we can change that… 6 Exercise 1: Movement 1.1. Bigger Snakes are Better Snakes Our first job is to increase the size of the Snake. But before we begin lets take a quick look at how this program works. Open snakecore.js in Notepad++ and take a moment to scan the file. Don’t worry if there is a lot of stuff here you don’t understand, but see if the following parts make sense to you: Towards the top of the file you will see some variables defined. Variables are named pieces of data. Southampton Snake has four of them of interest to us: var d; //direction of the snake //(right, left, up, down) var food; //cell representing the current item of //food, has a food.x and food.y position var score; //the players current score var snake_array; //an array of cells to make up the //snake, each has x and y position These are global variables, they can be referenced anywhere in the program, and they contain key bits of information about the game. Because the browser will ignore anything following a // we can use this to write comments in our code. Good comments are an essential part of good programming, and help remind us (and explain to others looking at the code) what the code does. Here they have been used to explain the purpose of each of those global variables. Notice that each atomic piece of code (these are called statements) ends with a ; In JavaScript these are optional as long as there is only one statement per line, but it is good practice to include them anyway. The code in Southampton Snake reads and writes these variables in order to control the game. Now let’s look at snakepage.html where most of this control code lives. Load snakepage.html into Notepad++, you should find a section of the document with script tags, this is where we can write our JavaScript. When the web browser encounters these tags it knows to run whatever is inside them as a script, rather than simply display them in the browser: To make it more manageable JavaScript can be broken down into reusable sections of code called functions. There is a function here that is called to create the snake when the game is first loaded, it currently looks like this: function create_snake() 7 { snake_array = []; //Empty array to start with snake_array[0] = {x: 6, y:0}; snake_array[1] = {x: 5, y:0}; snake_array[2] = {x: 4, y:0}; } You may remember that snake_array was one of the four global variables defined in snakecore.js. It is an array of co-ordinate objects that defines the current position of the snake. The co-ordinate at index 0 represents the head. Explanation: Arrays In JavaScript (and many other languages) it is very useful to be able to create lists of things called arrays. We can refer to a particular item in the array using an index, this is a position in the array, assuming that the first item is always at position 0. So for example, if we wanted to store a shopping list we could write: var shop_list = []; //create it as an empty array shop_list[0] = “cheese”; shop_list[1] = “milk”; shop_list[2] = “bread”; We can then use the same syntax to get the values out: var msg = “Remember to get ” + shop_list[0]; Explanation: Objects In JavaScript (and lots of other languages) it is useful to collect several bits of data together into an object. Javascript is a prototype-based language, so you don’t have to define the possible objects first, you can just create them as you need them. When you define an object you give each variable inside it a name and a value. For example, we might create an object to represent a person like this: var s1 = {name: “Alice”, age: 18, course: “CompSci”}; We can then access them (to read or write) using the dot operator: s1.age = 19; //changes Alice’s age to 19 var msg = “Hello “ + s1.name; In Southampton Snake we use objects to represent co-ordinates. So the snake is really an array of co-ordinate objects, where each co-ordinate is given an x and a y value. So the line of code: snake_array[0] = {x: 6, y:0}; Means we create a new object with an x value of 6, and a y value of 0 and we assign that object to the first position (the 0 index) of the snake_array. Try changing the initial position of the snake, by altering the x and y positions of these three co-ordinate objects. Remember that they should be in sequence, with the head of 8 the snake in position 0. Can you add new lines of code to make the snake longer? Change the code so that the snake starts out being 5 cells long. 1.2. Steering the Snake Our first job in making this into a working program is to make it respond to user input. Southampton Snake is driven using the cursor (sometimes called the arrow) keys. Look in snakepage.html for the following code: //set it so that when the user hits a key the //keystroke function is called document.onkeydown = keystroke function keystroke(evt) { //TODO: fix this so that it changes the value of //direction according to the key } The first statement about document.onkeydown is an instruction to the browser to call the keystroke function whenever the user presses a key. The function is defined immediately afterwards, but currently does nothing, we need to change that by writing some code in the function. We can change the direction of the snake by altering the global variable called d that we saw earlier on. Try adding the following code to the keystroke function: d = “down”; Now reload the snakepage.html in your browser. You need to click inside the snake grid to give it focus (send it all the keystrokes), do this quickly once the game starts and then hit any key. The snake should change direction. Try setting it to one of the other valid directions (“up”, “right”, “left”). Of course the function really needs to respond to the key that was actually pressed, so we need to check what that was and act accordingly. We can use an if statement to do this, if statements are used when we want to check if something is true before we do something, they can also be combined in an if/else statement to choose between two alternatives (we’ll see that in just a moment). Instead of setting the value of d automatically to “down”, put the following code inside the keystroke function: if(evt.keyCode == 37) d = "left"; This looks at the keycode of the key that was pressed (a number that represents the key) and if it was a certain value changes the value of d. Try reloading snakepage.html. Now the snake should only respond when the left arrow key is pressed. 9 Explanation: If statements An if statement is a simple check before another statement is executed. It has the form: if(check) statement; We can put anything as the check as long as it resolves to be either true or false, typically these are comparison operators that compare two variables, for example to check if there is enough money in an account before making a withdrawal: if(balance >= withdrawal) balance = balance – withdrawal; There are several built in comparison operators, including: if(a < b) //if a is less than b if(a <= b) //if a is less than or equal to b if(a > b) //if a is greater than b if(a >= b) //if a is greater than or equal to b if(a == b) //if a is equal to b if(a != b) //if a is not equal to b Add in the extra code to deal with the other three keys. The keycodes are as follows: • 37 is the left arrow • 38 is the up arrow • 39 is the right arrow • 40 is the down arrow Once you have done this you should have a moving snake! Exercise 2: Feeding 2.1 A Hungry Snake is an Unhappy Snake These next steps will turn our program into something playable. We need to add the code that allows the snake to eat the food. This will require you to understand how the snake moves, as we will change this to allow it to grow. Look at snakepage.html, and in particular at the end of the update_snake function, you should see the following code: var newhead = {x: nx, y: ny}; snake_array.unshift(newhead); //adds the newhead as //the first cell snake_array.pop(); //pops out the last cell The first line creates a new co-ordinate object to represent the new head of the snake. The previous code in the function has calculated the positions of this (nx and ny). 10 The second line calls the built in unshift function on the array. This inserts newhead at the start of the array (the head of the snake) and moves everything else along. The third line calls the built in pop function on the array. This removes the last item of the array (the end of the snake’s tail). Together this creates the illusion of movement, as new cells are continuously added to the head of the snake, as old cells are removed from the tail. Old snake: Newhead added (unshift): Tail removed (pop): Explanation: If/else statements An if/else statement is an extension of an if statement, just like an if statement it performs a check, but adds an additional statement that will be executed if the check fails. if(check) statement1; else statement2; Sometimes we want more than one statement to be executed, we can do this by grouping statements together using curly braces: if(check) { statement1; statement2; ... } else { statement3; statement4; ... } So to make the snake grow we have to change the code to check if the newhead is on a food item, and if it is we simply don’t pop the tail. To do this you need to use an if/else statement, with a more powerful check using something called logical operators. Rather than tell you what to type, this time see if you can figure out what to do based on the following written description (good luck, and ask for help if you get stuck!): 1. Check to see whether the newhead is in the same position as the food, you can do this by comparing nx and ny to food.x and food.y 2. If it is we increment the score variable by 1 3. And then call the create_food() function to create a new item of food (at present this will always be in the same place) 4. If it is not then we can forget about the score and food, and instead we can pop the tail (as the code does at the moment) 11 Explanation: Logical conditions Whenever we perform an if, or an if/else we use a conditional operator such as >, <, ==, !=. However we can build more complex conditions using logical operators. For example, if we wanted to check if a variable a was between two values min and max we could use the && operator: if(a > min && a < max) statement; If we wanted to check if the variable b was equal to either c or d we could write: if(b == c || b == d) statement; (the | character is called a vertical bar, and it is normally on the same key as the backslash \) Conditional and logical operators combine in all sorts of ways to give us very powerful checks, you can use parenthesis () to make precedence clear. E.g: if((a > min && a < max) || a == b) statement; 2.2 Food that Moves You should now have a snake that moves around and eats food to grow. There is one last thing to fix in this part, take a look at the create_food function: //Lets create the food now function create_food() { food = { x: 10, y: 10, }; } As you can see the function currently always puts the food in the same cell (10,10). We need to change this to a random cell, but of course that has to be in a given range (there are two variables that store the extent of this range: maxx and maxy, they are defined at the top of snakecore.js if you want to take a look). Try replacing the fixed position with a random position between 0 and maxx, and 0 and maxy. It may help to know the following two built in functions: Math.random()generates a random number between 0 and 1. And Math.round(variable)will round the value in variable to the nearest whole integer (i.e. 2.49 becomes 2, 2.5 becomes 3). Don’t be afraid to ask for help if you get stuck! 12 Exercise 3: The End Game 3.1 Walled Snake Gardens Well done if you have got this far, it is now playable, but it’s not really a game as there is no challenge. The final step to making this a proper snake game is to add the two end game states, hitting a wall (the edge of the playable area) and hitting yourself (the head of the snake runs into the body). Let’s start out with the walls. Look in snakepage.html and find the function called check_collision, it should look like this: //returns true if the snake has hit a wall //or if it has hit itself (x and y are in the array) function check_collision(x, y, array) { return false; } This function is called every time that update_snake is run, take a quick look at how it is called there: //Now check for game over conditions if(check_collision(nx, ny, snake_array)) { //restart game init(); return; } The code uses check_collision as a check within an if statement, just like we did with conditional operators before (a > b etc.). It can do this because check_collision returns true or false. If it returns true this indicates that a collision has happened and that the game should be reset by calling the init function, and ending the update_snake function early using the keyword return. The problem is that the current check_collision function always returns false – indicating that a collision never happens. We need to change this. Alter the check_collision function so that it uses an if statement to see if the snake has hit a wall. Remember that the edges of the play area are defined by 0 and maxx, and 0 and maxy. Helpfully the x and y variables passed into the function will contain the current x and y positions of the snake’s head. Note: It is very easy to get this wrong by one cell! So make sure you playtest your code by running the snake around the edge of the play area. Test all four edges. 3.2 Ouroboros2 2 Google is your friend! 13 Almost there! The last piece of the game is to check to see if the snake’s head has hit its own body. The best way to do this is to use a for loop to check the x and y co-ordinate of each cell in the snake, if it is the same as the x and y co-ordinate of the head (i.e. the x and y variables passed into the check_collision function) then there has been a collision. Try adding a for loop to check_collision to do this. Remember that each slot in the array holds an object with an x and y position, so if i is the index you can use array[i].x and array[i].y to access them. You still need to check for the walls! Explanation: for loops and searching arrays A loop is a simple structure that repeats one or more lines of code over and over until a certain condition is met. There are several different kinds of loop in JavaScript including while loops, but for search through an array the most useful kind is a for loop. These use the following structure: for(initialise; check; change) { statement1; statement2; ... } Where initialise is a statement (usually used to set up a variable for counting) check is a condition that must remain true for the loop to continue to run, and change is a statement (usually to change the counting variable). For example, the following loop will run exactly 10 times: for(var i=0; i < 10; i = i + 1) So for loops are really useful when you know in advance how many times you want the loop to run. This means they are good for searching through arrays, as you know the length of the array in advance. It also means that you can use the counting variable (i in the example above) as the index for the array. For example: for(var i=0; i < array.length; i = i + 1) { if(array[i] == target) alert("Target found!"); } Extension: Game Design Software Engineering is not only about writing code, it is also about understanding the sorts of programs that you are writing, Southampton Snake is a game, so this means having some knowledge of Game Design. If you complete the Exercises in the lab (or if you want to carry on at home) then why not try and do some basic game design and extend Southampton Snake with new game mechanics. We will talk through some of the options in the lab, but if you want help in 14 understanding the impact of your choices why not look at a classic framework for game design called Mechanics, Dynamics, Aesthetics (MDA)3. The core idea is that new Mechanics (rules or extensions you add) give rise to new Dynamics (play and system behavior) and impact on the overall Aesthetic (the user experience). The MDA framework can help you think about how changes you make to the game impact on the user experience. You can get a great overview of MDA from the Extra Credits Channel on YouTube (although this focuses mainly on aesthetics); this channel is a great resource if you are interested in Games Design or the Games Industry: https://www.youtube.com/watch?v=uepAJ-rqJKA Thanks for getting this far, and good luck with building your extensions and exploring the power of programming! 3 Robin Hunicke , Marc Leblanc , Robert Zubek (2004), MDA: A formal approach to game design and game research. In Proceedings of the Challenges in Games AI Workshop, Nineteenth National Conference of Artificial Intelligence