Main content
Course: Computer programming - JavaScript and the web > Unit 4
Lesson 5: Making a memory gameMemory game: Drawing a grid of tiles
The first step of playing the "Memory" game is to randomly shuffle all the tiles, and then lay them out in a rectangular grid, face down so that we can't see which image is on the other side of each tile.
Face-down tiles
To start off in programming the game, let's just worry about creating face-down tiles, and figure out how to do the different images later.
The "tile" is an important enough object in the game of "Memory" that we will use object-oriented principles to define a
Tile
object and then create multiple instances of it. Then we'll be able to associate both properties (like location and image) as well as behavior (like drawing them) with each of the Tile
s.To start off, we'll define the
Tile
constructor function. Since we're not dealing with the images yet, we'll just pass x
and y
arguments to it. We'll also remember the tile size (a constant) in a property on the object.var Tile = function(x, y) {
this.x = x;
this.y = y;
this.size = 50;
};
Now that we've defined the constructor, we can use that in a loop to create tiles at appropriate x and y positions. In fact, we'll use two
for
loops - a nested for
loop - as that makes it conceptually easy to generate coordinates for a grid.First we need to declare an empty
tiles
array, to store all those tiles:var tiles = [];
Our outer loop iterates for as many columns as we want, our inner loop iterates for each of the rows, and each new
Tile
is initialized with an x and y that corresponds to that row and column.var NUM_COLS = 5;
var NUM_ROWS = 4;
for (var i = 0; i < NUM_COLS; i++) {
for (var j = 0; j < NUM_ROWS; j++) {
var tileX = i * 54 + 5;
var tileY = j * 54 + 40;
tiles.push(new Tile(tileX, tileY));
}
}
But, uh, it's hard to know if the tiles will look good, because we don't have any code to draw them yet! In fact, maybe we should have done that first. Sometimes, in programming, it's hard to know what to do first, aye? Let's now add a method to the
Tile
object that draws a tile face-down on the canvas. We'll draw a rounded rectangle with a cute Khan leaf on top, at the assigned location.Tile.prototype.draw = function() {
fill(214, 247, 202);
strokeWeight(2);
rect(this.x, this.y, this.size, this.size, 10);
image(getImage("avatars/leaf-green"),
this.x, this.y, this.size, this.size);
};
We're so close to being able to check how our tiles look! Let's add a new
for
loop that iterates through all the tiles and calls the drawing method on them:for (var i = 0; i < tiles.length; i++) {
tiles[i].draw();
}
Here's what our program looks like, with all that code. Try tweaking the different numbers in the nested for loop to see how it changes the grid or changing how they're drawn (different logo, perhaps?)
Face-up tiles
Now that we've got a grid of face-down tiles, let's tackle a bit of a trickier problem: assigning each of them an image, such that there's 2 of every image in the array, randomly distributed throughout. There are likely many ways we could accomplish this, but here's what I'd suggest:
- We create an array of the possible images, using the
getImage
function to pick ones from our library. - We'll only need 10 images for the faces of our 20 tiles, so then we create a new array that holds 2 copies of 10 randomly selected images from that first array.
- We shuffle the selected images array, so that the pairs of images are no longer next to each other in an array.
- In the nested for loop where we create the tiles, we'll assign an image from that array to each tile.
Those steps may not make sense yet - let's do them each and see what they look like.
Step 1: We create an array of the possible images, using the
getImage
function to pick ones from our library:var faces = [
getImage("avatars/leafers-seed"),
getImage("avatars/leafers-seedling"),
getImage("avatars/leafers-sapling"),
getImage("avatars/leafers-tree"),
getImage("avatars/leafers-ultimate"),
getImage("avatars/marcimus"),
getImage("avatars/mr-pants"),
getImage("avatars/mr-pink"),
getImage("avatars/old-spice-man"),
getImage("avatars/robot_female_1"),
getImage("avatars/piceratops-tree"),
getImage("avatars/orange-juice-squid")
];
I picked a bunch of avatars, but you could change it to pick whatever your favorite images are. The important thing is to make sure that this array has at least 10 images in it, so that we don't run out of images for our 20 tiles. We can add lots more than 10 images though, to give our game more variety each time its played, because we'll narrow down the list in the next step.
Step 2: We'll only need 10 images for the faces of our 20 tiles, so then we create a new array that holds 2 copies of 10 randomly selected images from that first array.
To do that, we create a for loop that iterates 10 times. In each iteration, we randomly pick an index from the
faces
array, push that twice onto the selected
array, and then use the splice method to remove it from the faces
array, so that we don't select it twice. That last step is very important!var selected = [];
for (var i = 0; i < 10; i++) {
// Randomly pick one from the array of faces
var randomInd = floor(random(faces.length));
var face = faces[randomInd];
// Push 2 copies onto array
selected.push(face);
selected.push(face);
// Remove from faces array so we don't re-pick
faces.splice(randomInd, 1);
}
Step 3: We shuffle the selected images array, so that the pairs of images are no longer next to each other in an array.
You've probably shuffled a deck of cards in your life, but have you ever shuffled an array in JavaScript? The most popular technique for shuffling in any programming language is called the Fisher-Yates Shuffle, and that's what we'll use here.
The Fisher-Yates Shuffle starts by first selecting a random element anywhere in the array, and swapping it with the last element in the array. In the next step, it selects a random element from anywhere in the array besides the last element, and swaps it with the second to last element. It keeps going until it has swapped every element.
You can click through this visualization to see what I mean:
To implement that in JavaScript, let's make a
shuffleArray
function that takes in an array and shuffles its elements, changing the original array:var shuffleArray = function(array) {
var counter = array.length;
// While there are elements in the array
while (counter > 0) {
// Pick a random index
var ind = Math.floor(Math.random() * counter);
// Decrease counter by 1
counter--;
// And swap the last element with it
var temp = array[counter];
array[counter] = array[ind];
array[ind] = temp;
}
};
If the algorithm doesn't quite make sense yet after stepping through the visualization and reading the code, you could try it with an actual deck of cards in the real world or watch how Adam Khoury does it in his Youtube video.
After defining that function, we need to actually call it:
shuffleArray(selected);
And now we have an array of 10 pairs of images, randomly shuffled!
Step 4: In the nested for loop where we create the tiles, we'll assign an image from that array to each tile.
We have 20 images in our
selected
array, and we're iterating 20 times to instantiate new tiles at locations in the grid. To select a random image for each tile, we can call the pop
method on the array. That method removes the last element from the array and returns it, and is the easiest way to make sure we assign all the images but don't double assign them. for (var i = 0; i < NUM_COLS; i++) {
for (var j = 0; j < NUM_ROWS; j++) {
var tileX = i * 54 + 5;
var tileY = j * 54 + 40;
var tileFace = selected.pop();
var tile = new Tile(tileX, tileY, tileFace);
tiles.push(tile);
}
}
Notice how that code passes
tileFace
as the third parameter to the Tile
constructor? Our constructor originally only had 2 parameters, x
and y
, but now we modify it so that we can also remember the image of each tile face, plus whether it's face up:var Tile = function(x, y, face) {
this.x = x;
this.y = y;
this.size = 70;
this.face = face;
this.isFaceUp = false;
};
So we now theoretically have images assigned to each tile, but we're not displaying them yet! Let's modify the
Tile.draw
method so that it can draw tiles that are face up:Tile.prototype.draw = function() {
fill(214, 247, 202);
strokeWeight(2);
rect(this.x, this.y, this.size, this.size, 10);
if (this.isFaceUp) {
image(this.face, this.x, this.y,
this.size, this.size);
} else {
image(getImage("avatars/leaf-green"),
this.x, this.y, this.size, this.size);
}
};
Finally, to test it all works, we can change our
for
loop to set each tile's isFaceUp
property to true
before drawing it:for (var i = 0; i < tiles.length; i++) {
tiles[i].isFaceUp = true;
tiles[i].draw();
}
Here it is, all together. Try restarting it to see how the tiles change each time.
Want to join the conversation?
- how are the rectangles rounded out?(59 votes)
- The setup of a
rect()
is this:rect(cornerX, cornerY, width, height, radius*)
You do not have to specify the radius, but if you want the rectangle rounded, then you should.(30 votes)
- Some days ago I was wondering if there was something like
.push
, but opposite (instead of adding something to the array, deleting something from the array), and now I see it's.splice
.
How are those things called? the ".push" and the ".splice" thing? I mean, what do I have to look for on google to find more like them?
Also, in this part:
// Remove from faces array so we don't re-pick
faces.splice(randomInd, 1);
Why did you write a "1" right before the randomInd var? What does that 1 do?(30 votes)- The 1 in
splice
tells it to remove one item, so you can change this to remove multiple items. In addition tosplice
, which allows you to remove a variable number of items from any position in the array (and also add items at the same time), there are also:array.pop()
- removes the last item in the array (opposite ofpush
).array.shift()
- removes the first item in the array (opposite ofunshift
).
All of these methods return the value of the item they remove. e.gvar a = [1, 2, 3];
var x = a.pop();
This will result ina
equalling[1, 2]
andx
equalling3
.(56 votes)
- Why is floor( ); being used instead of round( ); ?(8 votes)
floor( )
always rounds down, whileround( )
rounds to the nearest integer.(23 votes)
- I don't understand how you use the splice function. If someone could explain this more it would be great! Thank you!(6 votes)
- .splice removes a value from an array. For example:
var favoriteFruits = ["apple", "banana", "pear"];
and if you don't like bananas anymore, you could write:favoriteFruits.splice(1, 1);
to get rid of the 2nd item and to get rid of only the 2nd item.(2 votes)
- Can anyone explain how
selected.sort(function() {
return 0.5 - random();
});
can randomize the array?(9 votes)- What a horrible way to introduce sorting! Sorting is used to bring order to data. This example uses sorting to accomplish the opposite goal.
All sorting depends on the ability to compare two pieces of data,a
, andb
and tell ifa>b
,a===b
, ora<b
. If you read about the Javascript arraysort
method, it specifies how to communicate the results of each comparison. For example, if I were sorting an array of numbersnums
, I might write a number compare function that looks like
and then sort the array of numbers by invoking/*
* Compare a to b, and return a positive number
* if a>b, a negative number if a<b, and zero
* when they are equal.
*/
var compareNums = function(a, b) {
if (a > b) {
return 42;
} else if (a < b) {
return -3.14;
} else {
return 0;
}
};nums.sort(compareNums);
So, this program "sorts" an array of faces and provides a comparison function that ignores its arguments (it doesn't even declare parameters) and randomly returns numbers between -0.5 and +0.5 (becauserandom
invoked with no arguments returns a number between zero and one, which has 0.5 subtracted from it.)
The results of using this unreliable, random comparator is an array of elements that is anything but sorted.(2 votes)
- What is the function this? Also, could it get any jpg image from the web?(4 votes)
- what is the difference between math.floor and floor(6 votes)
- Since they are equal, there cannot be any difference.
println(Math.floor === floor);
(6 votes)
- I get the Oh Noes guy and he is saying this
Cannot read property 'width' of undefined
can someone please help?(7 votes)- Yes i think it was because you didn't add the this.face=face; to the tile object. This document doesn't make that clear.(2 votes)
- Which sort method does Chrome use?
How about Edge?
According to BobLyon, browsers don't use bubble sort.
https://www.khanacademy.org/profile/BobLyon/discussion
I know randomizing via sort is computationally inefficient; typing-wise though, does it use the least number of characters to get the job done?(5 votes)- Here's some discussion about it, if not an exact answer: http://stackoverflow.com/questions/234683/javascript-array-sort-implementation
For Webkit-based browsers, which includes Chrome, it seems to depend on the data type.
Not sure about Edge, and it might be harder to find out, being closed-source.
The answer to this is subject to change in new releases, too.(5 votes)
- for those like me stuck at the randomInd variable in step 2 it took me a hwile to figure this out; for those like me, hope this helps:
var randomInd = floor(random(faces.length));
we need a random image from our array of faces..so we use a code to get a random index(randomInd); first to assign random value we assign a minimum and maximum to it..here it is the number of elements in our array (faces.length) and then since random can return a number in decimals we need to round it down (not up as we want to access the lower number so all indices are covered in case rounding up is greater than the number of elements in that array)
so assign faces.length as minimum and maximum values to random and use floor to round it down
this automatically gives us that if we want any one image from our selected ones we make the variable as follows:
var face(any new image) = faces[randomInd];(randomly chosen from our selected images array by using the random index code)
if am incorrect please those who do understand correct me..TIA(4 votes)- It sounds like you understand what is happening in the code.
Just one note. The syntax (or rules) of therandom()
function is
If only one argument is passed torandom(min, max);
random()
, that argument becomes themax
and the function assigns 0 as themin
argument.
will return a random value between 0.000 and the value of the length of therandom(faces.length)
faces
array. You are correct about why we round down.(5 votes)