Main content
Course: Computer programming - JavaScript and the web > Unit 4
Lesson 5: Making a memory gameMemory game: Flipping tiles
Okay, so we now have a grid of tiles that we can display face up or face down. But we have no way of actually playing the game. To remind you, here's how the game works:
- When the game starts, all tiles are turned face down.
- The player then flips over two cards, selecting them by clicking on them.
- If the two tiles have the same image, they remain face up. If not, they should be flipped face down again after a short delay.
Click-flipping tiles
Right now, we have a program that draws a grid of tiles and then never draws anything else. Going forward, we want our program to draw different things over time - it'll start off drawing face down tiles, but then it'll display clicked tiles, and if all goes well for the player (fingers crossed!), it'll display a win screen.
So let's move all our drawing code into the ProcessingJS
draw
function. The computer will keep calling draw()
while the program is running, so the tiles will keep getting drawn according to whether they're face up or face down:draw = function() {
background(255, 255, 255);
for (var i = 0; i < tiles.length; i++) {
tiles[i].draw();
}
};
Let's get some of those tiles face up now! To flip a tile, the player must click on it. To respond to clicking in ProcessingJS programs, we can define a
mouseClicked
function, and the computer will execute that code every time the mouse is clicked.mouseClicked = function() {
// process click somehow
};
When our program sees that the player has clicked somewhere, we want to check if they've clicked on a tile, using
mouseX
and mouseY
. Let's start by adding an isUnderMouse
method to Tile
that returns true
if a given x and y is within a tile's area.With the way we've drawn the tiles, the x and y of the tile correspond to the upper left corner of the tile, so we should return true only if the given x is between
this.x
and this.x + this.size
, and if the given y is between this.y
and this.y + this.size
:Tile.prototype.isUnderMouse = function(x, y) {
return x >= this.x && x <= this.x + this.size &&
y >= this.y && y <= this.y + this.size;
};
Now that we have that method, we can use a
for
loop in mouseClicked
to check if each tile is under the mouseX
and mouseY
. If so, we set the tile's isFaceUp
property to true
:mouseClicked = function() {
for (var i = 0; i < tiles.length; i++) {
if (tiles[i].isUnderMouse(mouseX, mouseY)) {
tiles[i].isFaceUp = true;
}
}
};
Here's what that looks like. Click a bunch of tiles and see what happens:
Restricting tile flips
Notice something? We implemented one aspect of game play, that the player is able to flip over the tiles, but we're missing an important restriction: they shouldn't be able to flip more than two tiles at once.
We will need to keep track of the number of flipped tiles somehow. One simple way would be a global
numFlipped
variable that we increment each time the player turns a card face up. We only flip a tile over if numFlipped
is less than 2 and the tile isn't already face up:var numFlipped = 0;
mouseClicked = function() {
for (var i = 0; i < tiles.length; i++) {
var tile = tiles[i];
if (tiles.isUnderMouse(mouseX, mouseY)) {
if (numFlipped < 2 && !tile.isFaceUp) {
tile.isFaceUp = true;
numFlipped++;
}
}
}
};
Delay-flipping tiles
Okay, our two-tile-flipping logic is complete. What's next? Let's recap the game rules again:
If the two tiles have the same image, they remain face up. Otherwise, the tiles flip back over after some period of time.
We'll first implement the second part, which automatically flips the tiles back over, because it will be hard to test the first part if we can't easily look for new matches.
We know how to flip tiles back over, by setting
isFaceUp
to false
, but how do we do that after some period of time? Every language and environment has a different approach to delaying execution of code, and we need to figure out how to do it in ProcessingJS. We need some way of keeping track of time - whether the delay period has passed - and a way of calling code after the period of time has passed. Here's what I'd suggest:- We create a global variable called
delayStartFC
, initially null. - In the
mouseClicked
function, right after we've flipped over a second tile, we store the current value offrameCount
indelayStartFC
. That variable tells us how many frames have passed since the program started running, and is one way of telling time in our programs. - In the
draw
function, we check if the new value offrameCount
is significantly higher than the old one, and if so, we flip all of the tiles over and setnumFlipped
to 0. We also resetdelayStartFC
tonull
.
It's actually a nice solution that doesn't require too much code to implement. As a performance optimization, we can use the
loop
and noLoop
functions to make sure that the draw
code is only being called when there's a delay happening. Here it all is:var numFlipped = 0;
var delayStartFC = null;
mouseClicked = function() {
for (var i = 0; i < tiles.length; i++) {
var tile = tiles[i];
if (tile.isUnderMouse(mouseX, mouseY)) {
if (numFlipped < 2 && !tile.isFaceUp) {
tile.isFaceUp = true;
numFlipped++;
if (numFlipped === 2) {
delayStartFC = frameCount;
}
loop();
}
}
}
};
draw = function() {
if (delayStartFC &&
(frameCount - delayStartFC) > 30) {
for (var i = 0; i < tiles.length; i++) {
tiles[i].isFaceUp = false;
}
numFlipped = 0;
delayStartFC = null;
noLoop();
}
background(255, 255, 255);
for (var i = 0; i < tiles.length; i++) {
tiles[i].draw();
}
};
Flip some tiles below - it's pretty cool how the tiles automatically flip back over, aye? To help you understand it more, try changing how long the delay waits and how many tiles have to be flipped over before the delay starts.
Checking matches
If you managed to match any tiles above, you were probably sad when they flipped back over, because, hey, you made a match! So now it's time to implement this rule of the game:
If two tiles match, then they should stay face up.
That means that we should check for matching tiles whenever there are 2 flipped over, and before we set up the delay. In pseudo-code, that'd be:
if there are two tiles flipped over:
if first tile has same face as second tile:
keep the tiles face up
We already have a check for whether there are two tiles flipped over (
numFlipped === 2
), so how do we check if the tiles have the same face? First, we need some way of accessing the two flipped over tiles. How do we find them?We could iterate through our array each time, find all the tiles with
isFaceUp
set to true, and then store those into an array.Here's a shortcut: let's just always store our flipped tiles in an array, for easy access. That way, we don't have to iterate through our whole
tiles
array every time the player flips a tile.As a first step, we can replace
numFlipped
with an array, and then use flippedTiles.length
everywhere we previously used numFlipped
. Our mouseClicked
function looks like this:var flippedTiles = [];
var delayStartFC = null;
mouseClicked = function() {
for (var i = 0; i < tiles.length; i++) {
var tile = tiles[i];
if (tile.isUnderMouse(mouseX, mouseY)) {
if (flippedTiles.length < 2 && !tile.isFaceUp) {
tile.isFaceUp = true;
flippedTiles.push(tile);
if (flippedTiles.length === 2) {
delayStartFC = frameCount;
loop();
}
}
}
}
};
Now, we need to figure out if the two tiles in the
flippedTiles
array do indeed have the same face. Well, what is the face
property? It's an object - and actually, the face of matching tiles should be exactly the same object, as in, the variable is pointing at the same place in computer memory for both. That's because we only created each image object once (like with getImage("avatars/old-spice-man")
) and then we pushed the same image object onto the faces array twice:var face = possibleFaces[randomInd];
selected.push(face);
selected.push(face);
In JavaScript at least, the equality operator will return true if it's used on two variables that point to objects, and both of those variables refer to the same object in memory. That means that our check can be simple - just use the equality operator on the
face
property of each tile:if (flippedTiles[0].face === flippedTiles[1].face) {
...
}
Now that we know the tiles match, we need to keep them up. Currently, they'd all get turned over after a delay. We could just not set up the animation in this case, but remember, there will be an animation in later turns - so we can't rely on that.
Instead, we need a way of knowing "hey, when we turn all of them back over, we shouldn't turn these particular ones over." Sounds like a good use for a boolean property! Let's add an
isMatch
property to the Tile
constructor, and then set isMatch
to true
only inside that if
block:if (flippedTiles[0].face === flippedTiles[1].face) {
flippedTiles[0].isMatch = true;
flippedTiles[1].isMatch = true;
}
Now we can use that property to decide whether to turn the tiles over after the delay.
for (var i = 0; i < tiles.length; i++) {
var tile = tiles[i];
if (!tile.isMatch) {
tile.isFaceUp = false;
}
}
Play with it below! When you find two matching tiles below, they should stay up after the delay (and after future turns):
Want to join the conversation?
- What does this mean?
(delayStartFC && (frameCount - delayStartFC) > 30)(30 votes)- The first part before the && checks if
delayStartFC is true or false.
If delayStartFC still has the value: null,
then a second tile hasn't been opened yet,
and delayStartFC evaluates to false,
the second part never gets checked.
That's because of the logical "and" operator: &&,
both sides of the && operator must be true
for the whole statement to be true.
delayStartFC is true if it has a value
other than: null, 0, false, undefined or NaN.
In this game that means that delayStartFC has
been assigned the current frameCount when we
clicked on the second tile:
if(numFlipped === 2){
delayStartFC = frameCount;
loop();
}
The second part checks if the difference
between the current framecount and
delayStartFC is greater than 30.
For example:
If the frameCount was 20 when we opened
a second tile, then delayStartFC would
get the value: 20
the if statement would look like this:
if(20 && (frameCount - 20) > 30)
the first part: if(20 ..., evaluates to true
(it's not null, 0, false, undefine or NaN)
and the second part: (frameCount - 20) > 30
is checked aswell.
Every time the draw loop runs, the
frameCount increases by 1.
The first time through the draw loop,
frameCount is 20:
(20 - 20) > 30, 0 > 30, that's false,
the tiles will stay turned face up.
The second time through the draw loop,
frameCount is 21:
(21 - 20) > 30, 1 > 30, false again.
It will evaluate to false until the
draw loop has gone through 31 times
and the frameCount is 51,
(51 - 20) > 30, 31 > 30, that's true,
and the tiles now turn face down,
numFlipped and delayStartFC gets reset
and the draw loop stops, waiting for
another two tiles to be opened.(91 votes)
- Why don't we use
frameRate(1);
and only wait 1 frame to turn the cards down?(8 votes)- The problem with doing that is that it's a global change, so effects the entire program, not just the delay for flipping. In the simple case of this game, it will work, but if you later want to add animation, or make a timer show how long the player has been playing, then you'll run into problems. In general it's best to avoid changing the frameRate or making changes that have large repercussions unless you're absolutely sure that's how you want your game to run.
It's all about writing code to be as flexible as possible so it's easy for you, or someone else, to make modifications later.
Having said that, this code does useloop
andnoLoop
which make similarly global changes to the draw loop, so they would also have to be changed if you wanted to modify the program to include animations later.(25 votes)
- How
Math.random()
is different fromrandom()
?(3 votes)Math.random()
can have no parameters, and only returns a random number between 0 and 1. If you callrandom()
without any parameters, it will generate a random number between 0 and 1, just likeMath.random()
. If you use it with one parameter, it will generate a random number between 0 and the number you specified. (i.erandom(50)
will generate a random number between 0 and 50) If you specify two parameters, it will generate a random number between num1 and num2. (i.erandom(10, 15)
will generate a random number between 10 and 15)(24 votes)
- Is it just me or have the JS lessons on Khan academy suddenly gone from super easy to understand straight to crazy difficult to do? I have had trouble with almost all the challenges and projects so far in the advanced JS lessons.(14 votes)
- Well, that was the beginners course and you are doing the advanced course, so I guess it is to be expected?(6 votes)
- What's the difference between
null
andundefined
?(8 votes)- in JavaScript
undefined
means a variable has been declared but has not yet been assigned a value.null
is an assignment value.You can assign the value null to a variable to represent that it has no value . JSYK:undefined
andnull
are two distinct types: undefined is atype(undefined)
whilenull
is anobject
(17 votes)
- I noticed that on this "Playing the game" page, what was previously called the 'faces' array (in the previous "Grid of tiles" page) is recreated here as an array called possibleFaces, via the line:
var possibleFaces = faces.slice(0);
Besides providing a good prompt to learn a new method that can be applied to arrays (ie. slice() - I thought it was a typo for splice at first! :) - was there any particular reason to recreate the array here, or is it likely that Pamela just had a change of heart regarding what she wanted the array to be called?
With thanks,
Ben(9 votes)- Each time a random element is selected, it is removed from the array. If she removed it from the original array
faces
then upon Restart that array would be empty. So she clones thefaces
array and empties the clone.(13 votes)
- When you initiate "var possibleFaces" why did you define it as "faces.slice(0)"?
In fact, why was a possibleFaces variable needed at all? Was it used as a check to make sure we didn't have more Faces than tiles?
Also, did I use the terms "initiate" and "define" correctly.(7 votes)- I was confused as well. I had to look up "slice".
faces.slice(0) returns the entire faces array.
This cleared it up some for me.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
".slice(0)" tells it to copy all the elements starting with [0]. Using a copy maintains the original array for reuse (for example, once you add a 'restart' button).(10 votes)
- What do
loop
andnoLoop
functions do here? Pamela said she used them as a performance optimization. Could someone explain more?(7 votes)- Well, from a distance, it's unnecessary to call the draw function when it's just waiting for a user input. It's akin to playing a video with a minutes of a black screen at the end vs one that goes directly to the recommended list. One increases the networking costs for no reason but does the same thing as the other. Also, please take down your duplicate question.(6 votes)
- Hi! 1st question: The function isUnderMouse is a processing js function or we created it? How can I tell which functions are in the processing js? I cannot tell, both looks the same:
Tile.prototype.isUnderMouse= function() {...}
mouseClicked = function() {...};
2nd question: flippedTiles[0].isMatch = true;
How can we create a property isMatch of flippedTiles inside a function and use it outside of it? I've did some programming before and I always thought that any new variable has to be typed and initialized before the first use. Same with tiles[i].isMatch. Where is it defined? How do we know the value of tiles[i].isMatch? Also I was tought that any object can only be accessed within it's parent where it was created, otherwise it would just result an error or an empty object. Maybe t is different with properties.(5 votes)- If a function or method is not in your program, then it must come from the environment - Javascript, Processing.js or the web page.
isMouseUnder
is declared on line 24 whilemouseClciked
has no visible declaration.
Unlike many languages, Javascript do not require that all properties of an object be statically declared when the object is declared. The program may assign a new property to any object at any time (or delete a property from an object at any time). For examplevar v = { x: 150, y: 212 };
v.z = 347;
v.isMatch = false;(10 votes)
- In "Project: Memory++" I'm attempting to have the tiles change color when I move my mouse over them, but I'm stuck. I see that there's a mouseOver function, but I'm not sure how to implement it. Please help :'((8 votes)
- mouseOver checks if the mouse is over the canvas, not the tile. You can just make that into a if statement, just make it where if your mouseX is less than the tile x + width and its greater than the x, same thing with the y but with height(2 votes)