In this lab, you'll be introduced to jQuery, a popular library that helps speed up JavaScript web development to build interactive web pages.
The goal of this lab is to create a web app that will let us play the game "Galumphing Banderwoozles." I didn't come up with the name: the Spring 2014 15-251 TA's did.
Nontheless, the rules of the game are as follows:
- You are given a board composed of 15251 by 15251 tiles.
- Any tile can be one of three colors: red, green, or blue.
- Initially, all tiles are blue, except for the tile in the top left corner, which is green.
- The object is to turn the entire board red.
- Green tiles can be clicked to turn them red. Once a tile is red, it stays red.
- Clicking a green tile also affects it's neighbors to top, right, bottom, and left.
- When a green tile is clicked, all neighbors that are currently blue become green, and all those that are green become blue.
- Recall that the red tiles stay red.
These rules are pretty confusing. If you want to get a feel for how the game works, you can play a working demo of the game here. Simply enter a board size (say, 5 rows by 5 columns) and start clicking on the green tiles.
15251 by 15251 is pretty big, so for our purposes, we're going to let the player decide how big to make the board. Going one step further, this tutorial in general requires you to recall the rules of this game very rarely. As long as you know what goal we're trying to achieve (which will almost always be stated), the only struggle should be figuring out the JavaScript required to implement it.
The code and tutorial for this lab will be hosted on GitHub. Explanations of each step are written in the git commits, and you can also see the code diffs for each step to see what changed. For simplicity, each step will be listed below with a link to the code for that step. Most of the explanations will not make sense if you do not also take a look at the corresponding code.
For more information about how to create things like this, check out the Web Dev Weeks talk on HTML & CSS.
Skimming over this step's code, you can see that we've included our styles.css file containing all of our CSS, as well as an external file called normalize.min.css which will also help make things appear correctly.
You can also see a rough sketch of how the app will look: we've got a
couple of text inputs to get the board size, we've got a couple of
counters to keep track of some game stats, and we've got the
tile-wrapper, which will hold the code for all of our tiles.
If you're interested in this, you should definitely check out the [HTML & CSS workshop]({{ site.baseurl }}/html/), which shows you how to come up with the styles and content of a web page.
Now that we have all the HTML & CSS in place, we need to tell the
browswer where to look to load the JavaScript files that we'll be using.
This is done with the <script> tag.
For this project, we'll be using two different JavaScript sources.
First, we'll load the jQuery, a JavaScript library that helps out
tremendously with adding event listeneres, manipulating the DOM, and
doing other routine tasks. jQuery is a library written and hosted by
someone else: this means we need to go grap an external link to it. To
find it, search the Web for "Google jQuery CDN". (A CDN, or content
delivery network, is basically a super-fast network designed for hosting
commonly-requested files.) On the first result page, you should see a
large list of Google hosted libraries. Find the one titled JavaScript,
copy the <script> tag under it, and paste it into the file index.html.
Now that we've loaded our helper JavaScript library, we need to load the
file which will contain our core application code. We'll be calling this
file main.js, so add a <script> tag that looks similar to the one
you just copied, but with a source of src="main.js" instead:
<script src="main.js"></script>With these lines in place in index.html, we should be good to go! Now
we can start writing the core JavaScript code for our game.
We're finally at the point where we can add some code to our main.js
file!
With the first few lines, we get the opportunity to see one of the many quirks about JavaScript: something that's not really necessary but that makes life a little bit easier.
We're going to add the following lines to our JavaScript file:
;(function() {
})();What these lines do is terminate any previous, dangling JavaScript
statement (with the first semi-colon), create an anonymous function
(with the function keyword), and then immediately call it (with the
() towards the end). This ensures that we have our own execution
context for all the code that we write. You can read more about it
here,
but you're almost always going to want to follow this convention when
writing JavaScript files.
If you're familiar with a language like C/C++ or Java, you'll know that
every application must have an entry point that's usually called main.
In Python there's a similar concept using globally defined variables
like __name__.
JavaScript has no such entry point. When JavaScript is run in a
browswer, each line is run as it is encountered. While this is powerful,
there are many times when being able to run a particular command when
the page is ready for it to be run is useful. For this purpose, we use
event listeners to run a particular function once a corresponding
event occurs. Events are generated for many different actions and
conditions within the browser; one of these is the ready event, which
is fired on the document object whenever the page is, well, ready for
us.
We can then attach a listener, or a specialized function, to this event, effectively allowing us to run a particular piece of code whenever the event fires. This function can either have a name or be anonymous. We'll chose the latter:
$(document).ready(function() {
});What this does is attach an empty function to the ready event of the
document element. This syntax is actually a call to a jQuery function:
the $ is the variable name for all the things that jQuery provides,
and the .ready() method is a utility method for attaching listeners to
ready events.
This concept that we've used here of attaching a function to an event and having it be run at a later point is incredibly common in JavaScript. In fact, it might be JavaScript's most powerful feature. When used in this manner, the function that we attached to this event is called a callback.
Now that the "ready" event listener is in place, we can add some boilerplate functions for where we'll write the brunt of our game code.
First, we'll need a function called play, which for right now will
remain empty. This function is also going to be an event listener, so
it'll take a single argument representing the event that triggered the
function to be called. You may notice that the anonymous function we
registered on the ready event also is an event listener, and so it
receives an event object as well. However, since we don't use it (and
almost always won't on the ready event), we omit it for simplicity.
This is possible because in JavaScript, the number of arguments passed
to a function does not have to equal the number declared. For more
information, see here.
Now that we've got our empty play function in place, we're going to
add it as an event listener on the click event of the submit button.
This button has an id of submit, so we can use jQuery to select that
element and bind the click event on that element to our play function.
So now we have a function that will be executed every time the user clicks on our submit button. Let's start filling it in with something useful.
We said one of the things we'd have to do in order to set up the game state was to let the user choose how many rows and columns there should be. Using the two text that are present in the HTML of the page, we can do just this.
By the time the user clicks on our submit button and fires off the
play function, the text inputs with id's of rows and cols will
be holding values corresponding to the number of rows and columns that
our board should have. We can use jQuery to extract these values, using
the .val() function. We'll go ahead and store these values in global
variables.
Now is also a good time to deal with the circumstance when the user
accidentally hits submit twice. What should happen? Should we start a
fresh game? Add or remove rows from the game in progress? There are
many ways we could handle this situation. For sake of simplicity, we're
just going to remove the submit button and both text inputs entirely,
thereby ensuring that we don't have to deal with this weird
cirtumstance. After selecting the form with id args that wraps the
text inputs and submit button, we can use the .detach() method to
remove them from the DOM.
Once make the changes introduced in the previous step, go ahead and see
if clicking the button removes the #args form. (The best way to
preview your work is to use a simple Python webserver running in your
project's directory, then opening http://localhost:8000 in your browser.
If you need help with this, flag down a mentor! Otherwise, you should
also be able to simply open the index.html file in your browser.)
What you'll probably notice is that the form didn't disappear, and it
looked like the website reloaded the page. If you look carefully, you'll
probably also see that the URL changed to something with variable names,
question marks, and ampersands. This happened because of something
called bubbling which is another JavaScript feature which can seem
like a quirk at times.
Whenever there is a button inside a form and the button is clicked, the
browser thinks that the values of the inputs within that form need to be
submitted. If you don't specify where to submit this information on the
<form> tag, then it assumes that you want it to be submitted to the
current URL, effectively reloading the page.
This is pretty annoying, because we'd actually like to run our own code
instead of having the browser do something for us. To turn this feature
off, we have to understand what's actually going on. Nearly every action
a user has with a page generates some sort of event. Even though an
event might have a particular element from which it originated, the
event it self propagates up through the DOM tree from that element to
it's parent element all the way until it reaches the <html> tag. This
is what's known as bubbling, because the event bubbles up the DOM.
Our particular event handler is listening on the button itself, whereas
the form submission event is attached to the form element; we want to
shut off the bubbling before it can get there. To do this, we can use
the event argument e that will be passed into our play function. By
calling e.preventDefault(), we can stop the "default" action (the
event bubbling upwards) from occuring.
Try adding this line and see what happens. You should see that the
#args form detaches and stays detached. If not, flag down a mentor!
By now, you're probably starting to understand that there are a lot of idiosyncrasies in JavaScript who's solutions are not obvious. In cases like these, your best debugging tool is the ability to carefully describe what problem you're having and searching for it on Google. At least when you're starting out, it's likely the problems you will experience will have relatively simple, though non-intuitive, solutions. Being able to Google effectively is invaluable in times like these.
Now that we can accept the user's input with our form, we can actually begin setting up the initial board configuration. Specifically, the user has told us how many rows and columns to use, so we can use a nested for loop to set up the grid with the right dimensions.
HTML elements are display and drawn across and then down, we're going to
draw one row at a time (as opposed to one column at a time). To make
things easier, we're going to wrap each row in a div with the class
tile-row so that we can access arbitrary rows.
To create a new DOM element (basically, an element in the page), we use
the jQuery function ($) with a string argument representing the HTML
that we should use to construct that object. We just need to create a
<div> with a class of tile-row, so the string we want is '<div class="tile"></div>'. We can then store this in a variable named
$curTileRow (note that $ is a perfectly valid character to use in a
JavaScript variable name). Finally, we use the jQuery append method to
append this tile row to the tile wrapper.
We'll be using a multidimensional array to maintain what color each
board piece is. JavaScript arrays aren't fixed in length (because
they're actually just special objects), so we'll be dynamically adding a
row every time we need a new one. What that means for us is that within
the outer loop, where code will be executed once per row, we should add
a row to our multidimentional array. This is done with the code
boardColors[curRow] = [];, which sets the contents of the curRow'th
row to an empty array.
Now that we've done the required setup for each row, we can work on adding the individual tiles that will ultimately constitute the columns. We'll add a for loop inside our outer for loop that ranges over the entire number of columns.
The first thing this inner loop should do is create the DOM element for
the tile residing in that column. This is done with the jQuery append
method that we used before to add a row to the outer, wrapper div. Next,
since all but the top left tiles should start out blue, we'll set the
color of this tile to blue (and worry about the one green one later).
I'll use a helper function called setColor to set the color of a tile.
It'll take a row, column, and a string representing a color. We'll write
this function after the next step. For now, just know that it takes care of
setting the color for us.
Finally, we'll have to set the click event on the div to handle what should happen when the user "makes a move," or attempts to change the color of a tile. This part is actually very sophisticated! We're going to be using a technique called closures, which is a very powerful concept that is used quite frequently in JavaScript.
The basic paradigm stems from the fact that JavaScript maintains a context of all the variables and their values for every function that is created. That means that we can create special contexts that bind additional variables that a function would find useful to use.
First you'll note the helper function getTile. We'll write this helper
function in after next step as well, for but for now just now that it's
basically a wrapper around the $ function that we've been using this
whole time. What that means is that it returns to us the same jQuery
DOM object that we would normally get back, so we can set the .click
attribute of that result to bind a function to the click event of that
particular DOM element.
Next, we have an anonymous function wrapped in parentheses,
(function(r, c) {...}), followed by (curRow, curCol). When you see
this, a function definition wrapped in parentheses immediately followed
by parentheses, this will create an anonymous function and immediately
call it with the arguments specified in the second set of parentheses.
This is called a self executing function, and you can read more about it
here.
Remember that the argument to the .click function needs to be a
function which takes one variable: the event object e that triggered
the click event. We're actually going to use this self-executing
function to return a different function. What's special about this
returned function is that it will have access to the parameters of the
outer function, namely the current row and current column. We can then
reference those inside our returned function; in our case, we're going
to pass them on to move, a third helper function that will take care
of the brunt of the game logic for us. We'll start writing that function
in three steps.
Since the function we pass to click must take an event object as an
argument, we'll make the function we return take just that argument, and
we'll pass it on to move just in case we need it.
That's it for the processing we need to do within the loop! If you remember from before, I said that we'd take care of changing the upper left tile to green later, so let's go ahead and do that now. Remember that we use a helper function for this before, so we'll use the same function again now.
One last thing to take care of before we start going nuts with our game is to set up number of greens, blues, and total tiles, and then display these values.
We'll initialize a few global variables called reds, greens, and
blues to 0, set greens to 1 (for the upper left tile), and set
blues to the remaining number of tiles. Obviously, the total number of
tiles is rows * columns, and this will be important because we will
know the user has won if the number of reds is equal to this value.
Finally, we'll use a couple more jQuery calls to display these values.
The .html function on a jQuery object will set the inner HTML of that
attribute to the text specified (if you pass something that's not a
string, it will try and cast it to a string). That means we can use this
method, passing it blues and greens respectively, to set these
colors to the right values. Remember that the HTML elements with id's
blues and greens house the data we want to display. Note that we
don't have to worry about setting the number of reds because it should
already be set to 0 in the HTML (you can verify this).
As I mentioned two steps ago, we're going to now define the helper function we used to both set a tile's color given its indices, and to get the DOM node of an element given its indices.
The first of these, getTile, is a one-liner, though it is a bit
complicated. We'll be using the jQuery/CSS :nth() pseudo-selector,
which basically allows us to access the nth element of a particular
class. We'll also be using the > selector, which selects the children
of a particular node. Remember that we wrapped every row in a div with
the class tile-row, so to get the nth row we can use `.tile-row:nth('
- row + ')
. Then we want to access an individual tile (an element with the classtile), which is a child of atile-row. That means the next selector needs to be a>. Finally, we want the nth tile, so in a similar fashion to before, we want to use.tile:nth(' + col + '). If you hadn't already figured it out, the:nth()` pseudo-selector takes as an argument an integer which represents the index of which element it should select.
Next up is setColor. We have two things to do: update our internal
representation of the colors of the tiles, and then actually change the
color of the element. Remember that we've been storing our internal
representation of the board's colors in a multidimensional array called
boardColors. We want to access the element at [row][col] and set
it's contents to color. We'll use this stored value later in
processing what to do when the user clicks on a tile.
The second thing to do is change the actual color of the tile. In the
CSS stylesheet (styles.css), there are a bunch of handy classes that
will turn our element a particular color depending on which class is
applied. First, we want to get the DOM element represented by the row
and column we are dealing with, which is simple enough because we have a
helper function to deal with that! We'll next use the .removeClass
jQuery method to remove any previous color class that had previously
been attached to this element, and then the .addClass method to add
the class corresponding to the color we want to make this tile.
There are a couple of things to note here. The first is that if
removeClass doesn't find the class you specified to remove, it doesn't
do anything. The second is that you can chain jQuery calls. Note that
there are no semicolons after each line, only the last line. Every time
we call a jQuery method that's operating on a DOM element like this, we
can chain as many calls as we want on top of each other to keep
modifying the current jQuery DOM element.
We're approaching the home stretch! move is the last function we'll
need to implement.
Recall that move will be called every time someone clicks a tile
element, and it will have access to the current row and column because
of the closure we used when initializing the click event. We'll use this
method to handle processing all the rules of the game that we outlined
in the Overview.
In the rules, we specified that the only tiles we can actually click on are the green ones, so let's check if the current tile is green.
Remember that we have access to the row and column of the clicked tile
from the parameters to the move function, and we have a
multidimensional array which will give us the color of a tile given a
row and a column. Once we have this stored in a variable, we can check
to see whether that color is 'green'.
If it is green, we need to change it's color to red, increment the number of reds, and decrement the number of greens.
Once we've toggled the current tile to red, we need to process the current tile's neighbors. If we know what the current row and column are, we can easily get the row and column of the neighbors by adding or subtracting one to the appropriate variable.
Note that some of these cells will actually not be valid; for example, if we are in the 0th row, trying to access the 0 - 1 = -1th row will be invalid. We can check whether a variable is valid by checking the appropriate condition... beware of off by one errors!
To make things easier, we're going to utilize the fact that JavaScript
variables are dynamically typed. Since we don't have to declare a
variable's type, we could assign either a jQuery DOM element or null
to a variable and let the JavaScript interpreter determine this at
runtime. Combining this with the JavaScript ternary operator,
<condition> ? <value if true> : <value if false>, we can very
conveniently check the edge conditions, get the neighbor if safe, and
evaluate to null if not.
Once we've done this, I'm going to stick all four variables into an array with their corresponding coordinates so that we can conveniently iterate over the array, and do the same processing for all four.
This time, when iterating over each potential neighbor, we're going to
use a different style of JavaScript for loop called Array.forEach.
This is a function present on all JavaScript arrays that allows you to
loop over all the elements of a list without using indices.
The way this works is you specify a function to the .forEach() method.
This function takes as parameters the current element of the list, the
index of the current element, and the a reference to the array the
function was called on (in our example this isn't necessary because we
have access to the original array variable, but there are times when
this isn't the case). Since this function is a callback, it will be
evaluated once for each element of the array, and the appropriate values
will be substituted in.
Within the forEach callback, we want to check to make sure that the
current neighbor we're processing exists, i.e. is not null. Since the
actual element we're iterating over is an object containing the
properties tile, row, and col, we need to check the
$neighbor.tile property to see if the element itself exists. Since
JavaScript values are all truthy, an object is only ever falsy if it's
null, we can simply use if($neighbor.tile) to check whether the
neighbor exists or not.
Now that we know whether the neighbor exists, we can do the appropriate
processing. Particularly, what we need to do depends on the color of the
current element. Let's get the color of the current neighbor using the
same method we did for the clicked tile (making care to draw the row and
column from the properties we set on the current $neighbor variable).
Now we can case on the color of the current element. If it's green, we'll need to toggle it's color to blue and adjust the number of blues and greens, and vice versa it the neighbor is blue.
The only thing left to do is update the scoreboard and check whether the player has won the game.
If you recall the code to update the scoreboard from early, we will use the same code now, but add an additional line to update reds (because reds will have definitely changed this time).
If the number of reds is equal to the total number of tiles, that means
that every tile has been flipped, so the user has won. If the number of
greens is 0, then the user lost, because there aren't any tiles that can
be flipped. There's a JavaScript function alert that lets you display
a message in a dialog box, so we can use this to report a win or a loss.
It turns out that some boards are winnable and some boards aren't (if you're interested you should try to prove it). Regardless of this, I thought it'd be funny when I made this game and distributed it to my fellow 251 classmates to tell them that a non-winnable board was in fact winnable... it had some interesting consequences. Feel free to change the winning and losing messages to something more "agreeable."
The final thing we'll take care of is reload the page for if the user
want to play again. We can do with with location.reload(), which will
take the current page's URL and reload that addresss.
That's it! You should be able to try everything out and have it work in the browser. If you're still stuck, or you've got errors, flag down a mentor or ask someone for help and we'd love to give you some hints to fix everything up.
Thanks!
Written by Jake Zimmerman, July 2014