Building a Matching Pairs Memory Game Using Knockout JS

Introduction

 
All of us want to improve mind memory and concentration. Certainly, we know "match pairs memory game." It is an amazing game that increase memory power. It would be great if you try building this game by yourself, using your favourite images and controlling the scalability. What do you think about this game from programility perspective? Let's begin.
 
The game will look like 4x4 blocks, with 8 different pairs of images:
 
Building Match Pairs Memory Game Using Knockout js
 
The below chart describes the general flow of the game. The game starts by showing 8 different pairs images (16 images randomly spread over 16 block) for a set number of seconds. During these seconds, the user tries to concentrate and focus on each image. Afterwards, all images will be flipped and the user tries to match images depending on his memory. For each matching pair, the user's score will be increased by 5, elsewise the score will be decreased by 5. Therefore the score could be a negative value. Once a user matches all images, the game will be finished.
 
Building Match Pairs Memory Game Using Knockout js
The user can refresh the game at anytime using the refresh icon, then all images will be spread again randomly over the blocks.
 
Building Match Pairs Memory Game Using Knockout js
 
Before any code explanation, please note that you can find the source code and all related files in the attachment part corresponding to the table below:
 
Building Match Pairs Memory Game Using Knockout js
Now let's get started.
 
Step 1
 
Create a GameViewModel that will contain all needed properties and functions.
 
1.1 You notice that the game is 4x4 blocks and each block line is a row. So, create 4 observable arrays named as imgsRows, suffixed by 1,2,3,4 numbers respectively and all of these 4 previous arrays will be pushed to a main observable array named imgsrows.
  1. GameViewModel = function() {  
  2. var self = this;  
  3. self.imgsRows = ko.observableArray();    
  4. self.imgsRow1 = ko.observableArray();    
  5. self.imgsRow2 = ko.observableArray();    
  6. self.imgsRow3 = ko.observableArray();    
  7. self.imgsRow4 = ko.observableArray();    
  8. }   
1.2 As the game is 4x4, 16 images will be used with suffix for each image's name. Suffixes are 1,2,3,4,5,6,7, 8, and assume each image has a name "Img" + suffix +".png" .
In this step, you need to distinguish between two things image suffIx and image index, suffix is from [1-8] combined with image name, however, index is related to image block in the 4x4 blocks and could be a value from [0-15], for example, image could has a name "Img8.png" with index = 15.
 
1.3 Generate a random integer from 0-15 then using it as an index for suffix array to get the value of suffix which will be a part of an image name. After that, remove the suffix from the suffix array.
  1. self.getRandomImages = function() {     
  2. var imgSuffixArray = [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8];    
  3.   var randomInt = Math.floor(Math.random() * imgSuffixArray.length);    
  4.   var  imgUrl = "url(Images/Images100x100/Img" + imgSuffixArray[randomInt] + ".png)";    
  5.   var  imgName = "Img" + imgSuffixArray[randomInt] + ".png";    
  6.   imgSuffixArray.splice(randomInt, 1);   
The above snippet code should be repeated 16 times, so use a For-Loop and assume that i variable is an index for an image.
  1. for (var i = 0; i < 16; i++)    
As mentioned above, four observable arrays were created to carry the properties of the 16 images. Let's agree that the properties for images with indices [0-3] will be pushed into imgsRow1 array, [4-7] will be pushed into imgsRow2 array, [8-11] will be pushed into imgsRow3 array and finally, images with indices [12-15] will be pushed into  imgsRow4 array.
  1. var imgProps ={    
  2.                  'imgName': ko.observable(imgName),    
  3.                  'imgIndex': ko.observable(i),    
  4.                  'imgUrl': ko.observable(imgUrl),    
  5.                  'imgPointerEvent': ko.observable('none'),    
  6.                  'currentShownImgUrl': ko.observable(imgUrl)    
  7.              }    
  8.     
  9.          if (i < 4) {    
  10.              koGame.imgsRow1.push(imgProps);    
  11.          } else if (i < 8) {    
  12.              koGame.imgsRow2.push(imgProps);    
  13.          } else if (i < 12) {    
  14.              koGame.imgsRow3.push(imgProps);    
  15.          } else if (i < 16) {    
  16.             koGame.imgsRow4.push(imgProps);    
  17.          }    
1.4 After finishing the For-Loop, push the four observable arrays into the main observable array imgsRows  
  1. koGame.imgsRows.push(self.imgsRow1);    
  2. koGame.imgsRows.push(self.imgsRow2);    
  3. koGame.imgsRows.push(self.imgsRow3);    
  4. koGame.imgsRows.push(self.imgsRow4);  
1.5 Define an observable property  
  1. self.emptyImgUrl = ko.observable("url(Images/Images100x100/EmptyImg.png)");   
1.6 Replace the real background image of each block with an empty image by changing  the observable image property "currentShowImgImage"
  1. setTimeout(function() {    
  2.     for (var i = 0; i < 4; i++) {    
  3.         for (var j = 0; j < 4; j++) {    
  4.             (koGame.imgsRows()[i])()[j].currentShownImgUrl(koGame.emptyImgUrl());    
  5.         }    
  6.     }     
  7. }, 2500);   
All the snippts code of points 1.3 ,1.4,1.5 and 1.6 should be written inside "getRandomImages" function inside the GameViewModel.
 
Step 2
 
Create a user interface view using HTML tags that should be written in a way to receive a knockout binding object , so create "divGame" as a container div and create a nested foreach loop for binding imgsRows array.
  1.  <div class="container-fluid content" id="divGame">    
  2.                   <classclass="row">    
  3.                 <div class="col-md-12">    
  4.                     <div class=" row" data-bind="style:{'display' : displayImgsRows},foreach: imgsRows">    
  5.                         <div class="col-md-12" data-bind="foreach: $data">    
  6.                             <div class="img" data-bind="click:$root.onClickImage  , style: {  'pointer-events' : imgPointerEvent , 'background-image': currentShownImgUrl}"></div>    
  7.                         </div>    
  8.                     </div>    
  9.                 </div>    
  10.             </div>    
  11. </div>    
Step 3
 
Bind GameViewModel to  "divGame" container div and then call "getRandomImages"
  1. koGame = new GameViewModel();    
  2. ko.applyBindings(koGame, document.getElementById("divGame"));    
  3. koGame.getRandomImages();  
Step 4
 
Inside GameViewModel,Create an "onClickImage" function to check if the two clicked images are the same or not. Before that, create these obserervable properties with these initialization inside GameViewModel.
  1. self.clickedImgName = ko.observable("");    
  2. self.clickedImgPositionX = ko.observable(-1);    
  3. self.clickedImgPositionY = ko.observable(-1);    
  4. self.clickedImgIndex = ko.observable(-1);    
  5. self.clickedImgUrl = ko.observable("");    
  6. self.clickedShownImgUrl = ko.observable("");    
  7. self.firstClickedImgIndex = ko.observable(-1);    
  8. self.firstClickedImgPositionX = ko.observable(-1);    
  9. self.firstClickedImgPositionY = ko.observable(-1);    
  10. self.firstCurrentShownImgUrl = ko.observable("");    
  11. self.firstClickedImgName = ko.observable("");    
  12. self.secondClickedImgIndex = ko.observable(-1);    
  13. self.secondClickedImgPositionX = ko.observable(-1);    
  14. self.secondClickedImgPositionY = ko.observable(-1);    
  15. self.secondCurrentShownImgUrl = ko.observable("");    
  16. self.secondClickedImgName = ko.observable("");   
Notice that the "onClickImage" function will be fired each time the user clicks on an image.
 
 4.1 Fill all clicked image properties inside the "onClickImage" function.
  1. self.onClickImage = function(data, event) {  
  2. koGame.clickedImgPositionX(-1);    
  3. koGame.clickedImgPositionY(-1);    
  4. koGame.clickedImgIndex(data.imgIndex());    
  5. koGame.clickedImgName(data.imgName());    
  6. koGame.clickedImgUrl(data.imgUrl());    
  7. koGame.clickedShownImgUrl(data.currentShownImgUrl());    
  8.    
  9. for (var i = 0; i < 4; i++) {    
  10.        for (var j = 0; j < 4; j++) {    
  11.                 if ((koGame.imgsRows()[i])()[j].imgIndex() == koGame.clickedImgIndex()) {    
  12.                     (koGame.imgsRows()[i])()[j].currentShownImgUrl(koGame.clickedImgUrl());    
  13.                     koGame.clickedImgPositionX(j);    
  14.                     koGame.clickedImgPositionY(i);    
  15.                     break;    
  16.                 }    
  17.           }    
  18.     }    
  19. }  
4.2  Inside the "onClickImage" function, two important properties should be used, "koGame.firstClickedImgIndex()", and "koGame.secondClickedImgIndex() ".
 
The first time that the user clicks on an image, all properties related to "firstClickedImage"  should be filled.
  1. if (koGame.firstClickedImgIndex() == -1) {    
  2.     koGame.firstClickedImgIndex(koGame.clickedImgIndex());    
  3.     koGame.firstClickedImgPositionX(koGame.clickedImgPositionX());    
  4.     koGame.firstClickedImgPositionY(koGame.clickedImgPositionY());    
  5.     koGame.firstCurrentShownImgUrl(koGame.clickedImgUrl());    
  6.     koGame.firstClickedImgName(koGame.clickedImgName());    
  7.     (koGame.imgsRows()[koGame.firstClickedImgPositionY()])()[koGame.firstClickedImgPositionX()].imgPointerEvent('none');    
  8. }    
Also, when user click on another image, all properties for the "secondClickedImage" should to be filled too.
  1. if (koGame.secondClickedImgIndex() == -1) {  
  2. koGame.secondClickedImgIndex(koGame.clickedImgIndex());    
  3.            koGame.secondClickedImgPositionX(koGame.clickedImgPositionX());    
  4.            koGame.secondClickedImgPositionY(koGame.clickedImgPositionY());    
  5.            koGame.secondCurrentShownImgUrl(koGame.clickedImgUrl());    
  6.            (koGame.imgsRows()[koGame.secondClickedImgPositionY()])()[koGame.secondClickedImgPositionX()].imgPointerEvent('none');   
  7. }   
4.3 Check if the two clicked images are the same or not  by comapring the imgUrl property for the first and second image. If they are equal, hide the two images and increase the score by 5. Otherwise, change the shown image URL to an empty image URL and decrease the score by 5.
  1. if ((koGame.imgsRows()[koGame.firstClickedImgPositionY()])()[koGame.firstClickedImgPositionX()].imgUrl() !=  
  2.                   (koGame.imgsRows()[koGame.secondClickedImgPositionY()])()[koGame.secondClickedImgPositionX()].imgUrl()) {  
  3.                   (koGame.imgsRows()[koGame.firstClickedImgPositionY()])()[koGame.firstClickedImgPositionX()].currentShownImgUrl(koGame.emptyImgUrl());  
  4.                   (koGame.imgsRows()[koGame.secondClickedImgPositionY()])()[koGame.secondClickedImgPositionX()].currentShownImgUrl(koGame.emptyImgUrl());  
  5.                   koGame.score(koGame.score() - 5);  
  6.                   (koGame.imgsRows()[koGame.firstClickedImgPositionY()])()[koGame.firstClickedImgPositionX()].imgPointerEvent('auto');  
  7.                   (koGame.imgsRows()[koGame.secondClickedImgPositionY()])()[koGame.secondClickedImgPositionX()].imgPointerEvent('auto');  
  8.   
  9.               } else {  
  10.                   koGame.score(koGame.score() + 5);  
  11.                   (koGame.imgsRows()[koGame.firstClickedImgPositionY()])()[koGame.firstClickedImgPositionX()].imgPointerEvent('none');  
  12.                   (koGame.imgsRows()[koGame.secondClickedImgPositionY()])()[koGame.secondClickedImgPositionX()].imgPointerEvent('none');  
  13.                   (koGame.imgsRows()[koGame.firstClickedImgPositionY()])()[koGame.firstClickedImgPositionX()].currentShownImgUrl("");  
  14.                   (koGame.imgsRows()[koGame.secondClickedImgPositionY()])()[koGame.secondClickedImgPositionX()].currentShownImgUrl("");  
  15.   
  16.            }  
Step 5
 
Now let's move to result part:
 
Building Match Pairs Memory Game Using Knockout js
 
5.1 Inside GameViewModel,Create observable properties for result part:
  1. self.disapearedImgsRowsFirstPart = ko.observableArray();    
  2. self.disapearedImgsRowsSecondPart = ko.observableArray();    
  3. self.allImgesCount = ko.observable(16);    
  4. self.allDisapearedImgesCount = ko.observable(0);    
  5. self.cardsFlipped = ko.observable(0);    
All the thses properties are filled once the two clicked images are matched. So, the following code should be written with an else condition in point 4.3 "koGame.allDisapearedImgesCount" property increased by 2, count of first and second images.
 
Two observable arrays are needed to show the images on left and right side of the result table.
  1. koGame.allDisapearedImgesCount(koGame.allDisapearedImgesCount() + 2);  
  2.   
  3.     if (koGame.allDisapearedImgesCount() < 9) {  
  4.                       koGame.disapearedImgsRowsFirstPart.push({  
  5.                           'imgUrl': ko.observable('url(Images/Images20x20/' + koGame.firstClickedImgName() + ')')  
  6.                       });  
  7.                   }  
  8.   
  9.      if (koGame.allDisapearedImgesCount() > 9) {  
  10.                       koGame.disapearedImgsRowsSecondPart.push({  
  11.                           'imgUrl': ko.observable('url(Images/Images20x20/' + koGame.firstClickedImgName() + ')')  
  12.                       });  
  13.                   }  
  14.   
  15.   
  16.     if (koGame.allImgesCount() == koGame.allDisapearedImgesCount()) {  
  17.                       koGame.displayImgsRows('none');  
  18.                       koGame.displaySuccessMsg('block');  
  19.                   }  
 5.2 Create HTML tags for the result part:
  1. <div class="row result">  
  2.     <div class="col-md-2">  
  3.         <div class="row" data-bind="foreach: disapearedImgsRowsFirstPart">  
  4.             <div class="disapeared-img" data-bind="style: { 'background-image' : imgUrl }"></div>  
  5.         </div>  
  6.     </div>  
  7.   
  8.     <div class="col-md-8">  
  9.         <table class="table table-bordered  result-table">  
  10.             <thead>  
  11.                 <tr>  
  12.                     <th>Cards Flipped</th>  
  13.                     <th>Score</th>  
  14.                 </tr>  
  15.             </thead>  
  16.             <tbody>  
  17.                 <tr>  
  18.                     <td data-bind="text:cardsFlipped"></td>  
  19.                     <td data-bind="text:score"></td>  
  20.                 </tr>  
  21.             </tbody>  
  22.         </table>  
  23.     </div>  
  24.     <div class="col-md-2">  
  25.         <div class="row" data-bind="foreach: disapearedImgsRowsSecondPart">  
  26.             <div class="disapeared-img" data-bind="style: { 'background-image' : imgUrl }"></div>  
  27.         </div>  
  28.     </div>  
  29. </div>  
Step 6
 
Inside GameViewmodel create a "refresh" function to initialize all properties and clear all arrays within the viewmodel, then calling the "getRandomImages" function to restart the game.
  1. self.refresh = function(data, event) {    
  2.     
  3.        koGame.disableClickOnImage(true);    
  4.        koGame.clickedImgPositionX(-1);    
  5.        koGame.clickedImgPositionY(-1);    
  6.        koGame.clickedImgIndex(-1);    
  7.        koGame.clickedImgUrl("");    
  8.        koGame.clickedShownImgUrl("");    
  9.        koGame.firstClickedImgIndex(-1);    
  10.        koGame.firstClickedImgPositionX(-1);    
  11.        koGame.firstClickedImgPositionY(-1);    
  12.        koGame.firstCurrentShownImgUrl("");    
  13.        koGame.secondClickedImgIndex(-1);    
  14.        koGame.secondClickedImgPositionX(-1);    
  15.        koGame.secondClickedImgPositionY(-1);    
  16.        koGame.secondCurrentShownImgUrl("");    
  17.        koGame.cardsFlipped(0);    
  18.        koGame.score(0);    
  19.        koGame.imgsRow1.removeAll();    
  20.        koGame.imgsRow2.removeAll();    
  21.        koGame.imgsRow3.removeAll();    
  22.        koGame.imgsRow4.removeAll();    
  23.        koGame.imgsRows.removeAll();    
  24.        koGame.disapearedImgsRowsFirstPart.removeAll();    
  25.        koGame.disapearedImgsRowsSecondPart.removeAll();    
  26.        koGame.displaySuccessMsg('none');    
  27.        koGame.getRandomImages();    
  28.     
  29.    }    
That's all , Now you can create your own version and use your own images by replacing them with the ones which are found in the images folder. Just take care about the images dimension, 100x100 px and 20x20 px.
 
If you have any question or any feedback please leave a comment .
 
Have a nice day!