Building Memory in Vue.js

memory game (for nerds!)

You probably played this game as a child. Memory is a game where you need to match pairs of tiles. Playing is very simple — you turn over one tile and then try to find a matching tile. The tiles are placed facedown on the table so you cannot see what they are. As the game progresses you gradually see more of the tiles and guessing becomes easier if you remember the tiles positions. This game, like many lend themselves to various web frameworks. Especially frameworks that enable holding state in the browser. We will use Vue.js in building this out. Building games is a very good way to learn a programming language or framework. They present various challenges such as flow, data structures, and logic. This game is no exception and hopefully we can learn some things about javascript, Vue.js and more importantly become better developers in the process.

We can think about building this game in a series of steps. In order to help us focus more on the game building aspects and not the npm build tools which require many dependencies, we will use a repository that includes all the tools we need without any of the dependencies which may not work with your OS. You can also find the code here. I did the best I could to break all the steps into their appropriate branches to make it easier to follow along.

Step 1

Vue.js concepts covered in the step.
https://vuejs.org/v2/guide/list.html
https://vuejs.org/v2/guide/instance.html
https://vuejs.org/v2/api/#Options-Lifecycle-Hooks

I think the first step in making this game should be to seed the board. Many times, especially for visual learners once there is something on the screen the steps that follow present themselves. So lets get something on the page and see what it looks like. To do this we can simply declare an array in our data namespace called tiles, and populate the array when our component mounts using the mounted lifecycle hook.

((() => {
const html = `
<div class="wrapper">
<div v-for="(tile, index) in tiles" v-bind:key="tile.id" class="box">{{tile.id}}</div>
</div>
`
Vue.component("app", {
template: html,
data() {
return {
tiles: [],
}
},
mounted() {
this.populateBoard()
},
methods: {
populateBoard() {
for(let i = 0; i <= 23; i++) {
this.tiles.push({id: i})
}
}
}
})
}))()

Notice in the populateBoard function we are pushing an object into the array with the key of id. So we now have an array of objects. Although we could have chosen to not use an object, hopefully it will become obvious as we see the output. Once we populate the board we can render the list using v-for and binding the key to the id. We should see something like this when we open the page in the browser. See branch commit here.

The populated board. Displaying the array index/id of the object

Great! Now that we have something to look at we can see what the next steps need to be. One of the most obvious tasks we need to complete is place the tiles on the board face down, and implement a click handler to show the tile.

Step 2

Vue.js and CSS concepts covered in this step:
https://vuejs.org/v2/guide/events.html
https://vuejs.org/v2/guide/class-and-style.html
https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/rotate

The first thing we can do is modify our tile object to have a key that specifies whether to show the face of the tile. That should be pretty easy. Since we are already using a tile object, we can add a key with a boolean as the value. We want to originally display the tiles face down so the value should be false. With that in place we can place a click handler on the tile and once that tile is clicked we can bind another CSS class that shows the tiles face. Lets look at the code that implements that. Specifically, notice the v-bind that attaches a CSS class when the tile has a specific value. In our handleClick method we are modifying the object that binds to that object.

((() => {
const html = `
<div class="wrapper">
<div v-for="(tile, index) in tiles"
v-bind:key="tile.id"
v-bind:class="{black_active: !tile.showFace }"
class="box"
@click="handleClick(tile)">
{{tile.id}}
</div>
</div>
`
Vue.component("app", {
template: html,
data() {
return {
tiles: [],
}
},
mounted() {
this.populateBoard()
},
methods: {
populateBoard() {
for(let i = 0; i <= 23; i++) {
this.tiles.push({id: i, showFace: false})
}
},
      handleClick(tile) {
tile.showFace = true

setTimeout(function() {
tile.showFace = false
}, 2000)
}
}
})
}))()
///CSS changes
.box {
background-color: #444;
color: #fff;
border-radius: 5px;
padding: 20px;
font-size: 150%;
order: 1;
cursor: pointer;
}
.wrapper {
width: 600px;
grid-template-columns: repeat(6, 100px);
grid-gap: 10px;
margin-left: 10%;
display: grid;
}
.black_active {
color: black;
background: black;
transform: rotateY(180deg);
}

Once we have that in place we should see this when we click on a tile. The branch can be found here.

onClick handler shows the tiles face

Step 3

Vue.js concepts covered in the step.
https://vuejs.org/v2/guide/components.html

OK. Great. We are starting to see the game come together as we have laid the foundation in the previous steps. One of the steps that’s becoming more apparent is that we should extract the concept of a tile into it’s own component. Components are at the heart of more modern javascript frameworks and VueJS is no exception. The idea behind this concept is to literally compose the webpage with many small isolated pieces. One important concept in VueJS is data down / events up. This helps guide us in placing the components on the page, where our data needs to live, and where it needs to be modified. So let’s take advantage of this idea and move some code into a new tile component.

//updated app.js file
((() => {
const html = `
<div>
<tile :tiles="tiles"></tile>
</div>
`
Vue.component("app", {
template: html,
data() {
return {
tiles: [],
}
},
})
}))()

As you can see we moved all the code away from the app component. Now it’s only rendering a new tiles component. We are still declaring the tiles array and passing it into the tile component.

//tile.js
((() => {
const html = `
<div class="wrapper">
<div v-for="(tile, index) in tiles"
v-bind:key="tile.id"
v-bind:class="{black_active: !tile.showFace }"
class="box"
@click="handleClick(tile)">
{{tile.id}}
</div>
</div>
`
Vue.component("tile", {
template: html,
data() {
return {

}
},
mounted() {
this.populateBoard()
},
props: {
tiles: {
type: Array,
required: true
}
},
methods: {
populateBoard() {
for(let i = 0; i <= 23; i++) {
this.tiles.push({id: i, showFace: false})
}
},
      handleClick(tile) {
tile.showFace = true

setTimeout(function() {
tile.showFace = false
}, 2000)
}
}
})
}))()
//register the new component in index.html
<script src="src/components/tile.js"></script>

Our functionality remains intact except now we have two components. Let’s continue building out tile.js keeping in mind that app.js is the parent of tile.js. So any changes in tile.js should be emitted as an event to app.js. We also need to introduce the concept of pairs to begin to implement the game logic. I’ve decided to add another data piece to app.js called matchingOptions

///app.js
((() => {
const html = `
<div>
<tile :tiles="tiles" :matchingOptions="matchingOptions"></tile>
</div>
`
Vue.component("app", {
template: html,
data() {
return {
tiles: [],
matchingOptions: [
{name: "Rails", pairs: 2},
{name: "PHP", pairs: 2},
{name: "Node", pairs: 2},
{name: "React", pairs: 2},
{name: "GoLang", pairs: 2},
{name: "Lisp", pairs: 2},
{name: "Perl", pairs: 2},
{name: "Java", pairs: 2},
{name: "C++", pairs: 2},
{name: "C", pairs: 2},
{name: "Ruby", pairs: 2},
{name: "Python", pairs: 2},
],
}
},
})
}))()
//tile.js
((() => {
const html = `
<div class="wrapper">
<div v-for="(tile, index) in tiles"
v-bind:key="tile.id"
v-bind:class="{black_active: !tile.showFace }"
class="box"
@click="handleClick(tile)">
{{tile.id}}
</div>
</div>
`
Vue.component("tile", {
template: html,
data() {
return {

}
},
mounted() {
this.populateBoard()
},
props: {
tiles: {
type: Array,
required: true
},
matchingOptions: {
type: Array,
required: true
},
},
methods: {
populateBoard() {
      for(let i = 0; i <= ((this.matchingOptions.length*2)-1); i++)   {
this.tiles.push({id: i, showFace: false, matched: false })
}
        return this.tiles
},
      handleClick(tile) {
tile.showFace = true

setTimeout(function() {
tile.showFace = false
}, 2000)
}
}
})
}))()

This step adds the idea of matching a tile. As you can see in our tile object we added the property matched, originally set to false, and we are also looping through the length of the matchingOptions array twice. This will let us add each matchingOption to a single tile twice. So for every individual tile there will be a match. The commit for this step can be found here.

Step 4

Now that we are adding tiles to the board let’s assign each tile a matchingOption. To do this we can add another property to our tile object and have that value be a call to a function that will grab a random matchingOption and decrement that objects pair property by 1. The pseudocode steps might be this. 
* Find all the elements whose pair value is greater than or equal to 1
* Shuffle the array returned from step one
* Select a random element from the array in step 2
* Decrement that objects pair value by 1

//tile.js
populateBoard() {
  for(let i = 0; i <= ((this.matchingOptions.length*2)-1); i++) {
this.tiles.push({id: i, showFace: false, matched: false, face: this.getRandomElement() })
}
   return this.tiles
},
getRandomElement() {
let pairs = this.matchingOptions.filter(object => object.pairs >= 1)
pairs = this.shuffleArray(pairs)
let item = pairs[Math.floor(Math.random() * pairs.length)]
this.itemToDecrement(item)
     return item
},
shuffleArray(array) {
return _.shuffle(array)
},
itemToDecrement(item) {
let decrementItem = this.matchingOptions.find(object => object.name === item.name)
  return this.matchingOptions[decrementItem.pairs-=1]
},
//updated html to render the name of the tile
<div class="wrapper">
<div v-for="(tile, index) in tiles"
v-bind:key="tile.id"
v-bind:class="{black_active: !tile.showFace }"
class="box"
@click="handleClick(tile)">
{{tile.face.name}}
</div>
</div>
//updated css to keep the new text in the box
.box {
background-color: #444;
color: #fff;
border-radius: 5px;
padding: 20px;
font-size: 150%;
order: 1;
cursor: pointer;
font-size: 125%;
}

Now that this method is complete we can start to see our tiles being seeded with matchingOptions . The branch can be found here.

our seeded board

Step 5

Vue.js concepts covered in this
https://vuejs.org/v2/guide/components.html#Custom-Events

All there is to do now is implement the game logic. The basis of our logic is keeping track of a click count, and which guess it corresponds to. The first file we need to modify is tile.js

//props
guesses: {
type: Array,
required: true
},
...
handleClick(tile) {
if(tile.matched === true || !self.verifyNonDuplicateGuess(tile)) {return}
this.$emit("compare-matches", event)
},
verifyNonDuplicateGuess(tile) {
if(this.guesses.length === 1 && this.guesses[0].id === tile.id ){ return false }
return true
},

In our handleClick function we take in a tile object and first check to see if that tile has already been matched. We also check to see if the first guess is the same as the second guess. If either of these things are true we return from the function without emitting an action. However if they are not true we can emit an event to app.js. As you can see we are emitting the event compare-matches this event will be published to all components on the same level as tile.js as well as the component that renders tile.js. In order to catch this event in app.js we need to subscribe to it. Let’s look at the code that can do that.

//app.js
const html = `
<div>
<tile :tiles="tiles" :matchingOptions="matchingOptions" @compare-matches="handleClickEventFromChild"></tile>
</div>
`
....
data() {
return {
guesses: [],
clickCount: 0,
....
}
}

Now app.js is subscribing to the event and calling it’s own function handleClickEventFromChild. With this code in place the two components are communicating. App.js is passing data down to tile.js and tile.js is emitting events up to app.js. Let’s implement the handleClickEventFromChild function which will be the game logic. There’s a lot going on here but we will walk through it.

handleClickEventFromChild(tile) {
  tile.showFace = !tile.showFace
++this.clickCount
this.guesses.push(tile)
  if(this.guesses.length > 1) {
this.compareGuesses()
}
},
compareGuesses() {
const self = this
  if(this.guesses[0].face.name === this.guesses[1].face.name) {
this.guesses[1].matched = true
this.guesses[0].matched = true
this.resetRound()
} else {
setTimeout(function() {
self..resetRound()
}, 500)
}
},
resetRound() {
this.clickCount = 0
this.guesses = []
  for(let i = 0; i < this.tiles.length; i++) {
let tile = this.tiles[i]
    if (tile.showFace === true && tile.matched === false){
this.tiles[i].showFace = false
}
}
},

The first thing we do is turn the tile face up. Then increment the clickCount by 1. After that we want to push the clicked on tile into the guesses array. Then we need to decide if we need to check if we should compare guesses. If the guesses array is greater than one, we know that we need to compare the guesses and call the function compareGuesses. The compareGuesses function compares the faces in the guesses array. Since we only have at most 2 elements in the guesses array, we can simply compare the first against the second. If the names match we set their matched properties to true and call the resetRound function. If they are not a match, we call resetRound. In the resetRound function we reset the clickCount and guesses back to their original states, and loop through the tiles reset each tile that does not have a match back to facedown. And that’s it! 97% percent of the work is done. The only thing we need to do now is implement the logic to win the game. The code for step 5 can be found here.

Step 6

Vue.js concepts in this step.
https://vuejs.org/v2/guide/computed.html
https://vuejs.org/v2/guide/conditional.html#v-show

In this step we will implement the logic for winning the game when there are no matches left. To do that we can create a computed property to check if there are any matches left.

computed: {
matchesLeft() {
let matchesLeft = 0
let tilesLength = this.tiles.length
    for(let i = 0; i < tilesLength; i++) {
if (self.tiles[i].matched == true) {
++matchesLeft
}
}
return ((tilesLength-matchesLeft)/2)
}
},

We can also add some html/css and a v-if to display a modal.

<div>
<div id="myModal" class="modal" v-if="matchesLeft == 0">
<!-- Modal content -->
<div class="modal-content">
<span>You win!</span>
<input type="button" class="close" value="Play Again!" v-on:click="resetGame()"">
</div>
</div>
<h1 style="margin-right:25%;">ROUND {{ level }}</h1>
<h1 style="margin-right:25%;">Matches to go {{ matchesLeft }}</h1>
<tile :tiles="tiles"
:guesses="guesses"
:matchingOptions="matchingOptions"
@compare-matches="handleClickEventFromChild">
</tile>
</div>
///style.css changes

/* The Modal (background) */
.modal {
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
/* Modal Content/Box */
.modal-content {
background-color: #fefefe;
margin: 15% auto; /* 15% from the top and centered */
padding: 20px;
border: 1px solid #888;
width: 15%; /* Could be more or less, depending on screen size */
display: block;
height: 40%;
}
/* The Close Button */
.close {
background: #3498db;
background-image: -webkit-linear-gradient(top, #3498db, #2980b9);
background-image: -moz-linear-gradient(top, #3498db, #2980b9);
background-image: -ms-linear-gradient(top, #3498db, #2980b9);
background-image: -o-linear-gradient(top, #3498db, #2980b9);
background-image: linear-gradient(to bottom, #3498db, #2980b9);
-webkit-border-radius: 28;
-moz-border-radius: 28;
border-radius: 28px;
font-family: Arial;
color: #ffffff;
font-size: 20px;
padding: 10px 20px 10px 20px;
text-decoration: none;
}
.close:hover {
background: #3cb0fd;
background-image: -webkit-linear-gradient(top, #3cb0fd, #3498db);
background-image: -moz-linear-gradient(top, #3cb0fd, #3498db);
background-image: -ms-linear-gradient(top, #3cb0fd, #3498db);
background-image: -o-linear-gradient(top, #3cb0fd, #3498db);
background-image: linear-gradient(to bottom, #3cb0fd, #3498db);
text-decoration: none;
}

With this in place we should see something like this, and as we play it’ll keep track of how many rounds it takes to win the game as well as now many matches there are remaining. The code for this commit can be found here.