Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Implement a Jewel Game like Candy Crush - Desktop Browser Version

4.78/5 (20 votes)
8 May 2017CPOL8 min read 53.4K   892  
In this article, we will investigate how to build the kernel of the jewel game (similar to candy crush and others)

Introduction

I have returned to write on CodeProject after two years of absence from when I wrote the last article. I used this time to improve my knowledge about Javascript and PHP.

Today, I would like to investigate the jewel game with you, as you will recognize with the name Candy Crush or 'AnyOtherWord' Crush in the mobile app store. All these games share the same behaviors: At first, you must move one jewel on a grid, in order to make lines, squares or other geometrical figures; With the second step, you must reach one or more goals in order to win a level!

When the player has made one or more geometrical figures which are considered valid, the game removes the figures and respawns new objects in the previous position. The respawn action can follow many rules.

In this article, we will just add some limitations in order to maintain the code and keep it clear and simple:

  1. We will consider as only valid horizontal and vertical lines.
  2. For the respawn, we just apply gravity to the objects. The object will then fall down when it has empty cells on the bottom border.
  3. As a goal, we just use a combination of the valid figures and time. We will ask the user to make N valid figures in T seconds in order to win the level.

We must think about the level goal. Each jewel games assure that each level can be won by the player, but we don't have time to draw the levels. So, we just follow a randomized approach when we are setting up a level: We will spawn random jewels for each grid cell.

So, after the game has started, we hope that the randomized placement of the jewels will allow the winning of the level.

Image 1

You can see in bold letters the keywords of a jewel game. We have a grid on which a set of objects lies and on which we can move the objects. The Grid is the most important word in this article. We will always interact with a grid! Grid can have different dimensions (cardinality) but we can choose to always use the square matrix and lock cells in order to draw the game grid that we want!

I am sure that all people who read this article know how to play with a jewel game, so I will not spend any more words on jewel gameplay.

At the end of this article, we would like to have a working prototype of the jewel game, developed for the web, so we will use just one framework: jQuery! Yes, I am a purist, I hate using too many layers in my code and so I just uses a few technologies when I work on a job.

The name of this prototype will be Solar System Jewel Saga! (Just for a joke.)

Background

In order to implement a jewel game, we need to make available just one capability: The player must be able to change the position of the jewels on the game matrix. Obviously, the changing of the position must follow a set of rules, but for this article, the jewels can only be moved at distance 1 from the original position (on the x and y axis).

Then, in order to implement the object movement in a web app, we need to know the following:

What is jQuery?

As you can read on their official website: "jQuery is a fast, small, and feature-rich JavaScript library. It makes things like HTML document traversal and manipulation, event handling, animation [...]"

So we expect that it is important to manipulate DOM objects in this project.

What is Drag and Drop?

Drag and drop is the ability to take an object from position A, move it and release it in another position B. You drag 'n' drop the object everytime in your daily life.

What is a Matrix?

You can see a matrix as a set of rows and columns. This is sufficient for the purpose of this article. We do not require any mathematical skills.

Using the Code

Starting from scratch, the first thing that we must do is the activation of jQuery. This action can be performed by opening a new HTML file and writing a single line:

HTML
<html>
  <head>
    <!-- Insert jQuery in our web page! -->
    <script src="https://code.jquery.com/jquery-3.1.0.min.js" 
      integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" 
      crossorigin="anonymous">
    </script>
  </head>
  <body>
  </body>
</html>

At this point, we should be pleased! We have completed at least 15% of the game! As we discussed before, a jewels game needs a grid in order to work and we must implement it in our code.

We can use JavaScript Array for this purpose. I think the code explains itself so I will not make too many comments.

HTML
<html>
  <head>
    <!-- Insert jQuery in our web page! -->
    <script src="https://code.jquery.com/jquery-3.1.0.min.js" 
      integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" 
      crossorigin="anonymous">
     </script>
     <script>
       // A 10x10 grid implemented with JavaScript Array 
       var rows=10; 
       var cols = 10; 
       var grid = []; 
      </script>
  </head>
  <body>
  </body>
</html>

We now need to populate the grid! In the jewel game, we have many kinds of jewels. For example, we can think of a solar system jewel game so our jewels will be solar system objects. I like the following icons but you can choose any other images. I hope that the following images are free of charge and open source but if this is not true, please write to me and I will remove them.

Image 2 Image 3 Image 4 Image 5 Image 6 Image 7 Image 8

We now need to implement this image as jewels in JavaScript! We can define a Jewels object to this! A JavaScript object is simply an object with methods and properties!

HTML
<html>
  <head>
    <!-- Insert jQuery in our web page! -->
    <script src="https://code.jquery.com/jquery-3.1.0.min.js" 
      integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" 
      crossorigin="anonymous">
    </script>
    <script>
      // A 10x10 grid implemented with Javascript Array
      var rows=10;
      var cols = 10;
      var grid = [];
      //
      function jewel(r,c,obj,src)
      {
        return {
          r: r,  <!-- current row of the object -->
          c: c,  <!-- current columns of the object -->
          src:src, <!-- the image showed in cells (r,c) A Planet image!! -->
          locked:false, <!-- This property indicate if the cell (r,c) is locked -->
          isInCombo:false, <!-- This property indicate if the cell (r,c) is currently in valid figure-->
          o:obj <!-- this is a pointer to a jQuery object -->
       }
     }
  </script>
  </head>
  <body>
  </body>
</html>

We now have a representation of a jewel in memory, so we need to insert the pictures for the jewels in our code. Again, we can use JavaScript Array in order to maintain a set of jewels in memory.

HTML
<html>
  <head>
  <!-- Insert jQuery in our web page! -->
  <script src="https://code.jquery.com/jquery-3.1.0.min.js" 
    integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" 
    crossorigin="anonymous">
  </script>
  <script>
    // A 10x10 grid implemented with JavaScript Array
    var rows=10;
    var cols = 10;
    var grid = [];

    function jewel(r,c,obj,src)
    {
      return {
      r: r,  <!-- current row of the object -->
      c: c,  <!-- current columns of the object -->
      src:src, <!-- the image showed in cells (r,c) A Planet image!! -->
      locked:false, <!-- This property indicate if the cell (r,c) is locked -->
      isInCombo:false, <!-- This property indicate if the cell (r,c) is currently in valid figure-->
      o:obj <!-- this is a pointer to a jQuery object -->
     }
    }

    // Jewels used in Solar System JSaga
    var jewelsType=[];
    jewelsType[0]="http://findicons.com/files/icons/1007/crystal_like/128/globe.png";
    jewelsType[1]="http://findicons.com/files/icons/2009/volcanoland/128/mars02.png";
    jewelsType[2]="http://wfarm1.dataknet.com/static/resources/icons/set121/676c25c0.png"; 
    jewelsType[3]=
    "https://www.hscripts.com/freeimages/icons/symbols/planets/pluto-planet/128/pluto-planet-clipart3.gif";
    jewelsType[4]=
    "https://www.hscripts.com/freeimages/icons/symbols/planets/jupiter-planet/128/jupiter-planet-clipart12.gif";
    jewelsType[5]="https://cdn2.iconfinder.com/data/icons/solar_system_png/128/Moon.png";
    jewelsType[6]="http://www.seaicons.com/wp-content/uploads/2015/10/Neptune-icon-150x150.png";
            
    // this function returns a random jewel.
    function pickRandomJewel()
    {
      var pickInt = Math.floor((Math.random()*7));
      return jewelsType[pickInt];
     }
  </script>
  </head>
  <body>
  </body>
</html>

At this point, we have all the objects which will be used in the game, implemented as JavaScript code. We will just investigate the following points.

How to Build a Level?

As I have written previously, we need to spawn random jewels for each grid cell. This can be done by simple randomized function such as PickRandomJewel() which you can see in the previous code. We have 7 jewels, so for each cell, we take one of them from the jewel array, and put it in a cell.

The most simple way to do this is to iterate on each grid cell and call the function.

HTML
<script>
 // prepare grid - Simple and fun!
 for (var r = 0; r < rows; r++)
   {
      grid[r]=[];
      for (var c =0; c< cols; c++) {
         grid[r][c]=new jewel(r,c,null,pickRandomJewel());
     }
  }</script>    

Ok, we have a game with random level in memory! We need to draw on the screen all of these objects!

How to Draw the Memory Representation of the Game Status?

When we talk about drawing, we must consider the technology we wish to use. We can choose between canvas (my preferred choice), DOM element or WebGL. For this article, I have chosen to use the DOM element in order to represent the game on the screen.

We have a set of jewels, so we can think of these as a set of images on the screen. HTML has the <img> tag that can help us.

Keep in mind one thing: We need to draw a grid on the DOM document, so we need to have four coordinates between those we are going to draw: up/down left/right corners! We just use the vectorial products of (0, 0) x (pageWidth, pageHeight).

The simplest way to perform this action is to calculate body width and height and use it in order to calculate the jewels <img> tag size.

HTML
<script>
  // initial coordinates
  var width = $('body').width();
  var height = $('body').height(); // for firefox use $(document) instead of $(body)
  var cellWidth = width / (cols+1);
  var cellHeight = height / (rows+1);
  var marginWidth = cellWidth/cols;
  var marginHeight = cellHeight/rows;
</script>    

Now, we have a grid, a level in memory, the drawing coordinates and the size of jewels! We just need to create the <img> tag in our document in order to draw the level! For the moment, ignore the ondragstart, ondrop and all the other event handlers. Just look at the results of these functions.

JavaScript
<script>
  for (var r = 0; r < rows; r++)
  {
    for (var c =0; c< cols; c++) {
      var cell = $("<img class='jewel' id='jewel_"+
      r+"_"+c+"' r='"+r+"' c='"+c+"' 
                 ondrop='_onDrop(event)' ondragover='_onDragOverEnabled(event)'  
                 src='"+grid[r][c].src+"' style='padding-right:20px;width:"+
                 (cellWidth-20)+"px;height:"+cellHeight+"px;position:absolute;top:"+
                 r*cellHeight+"px;left:"+(c*cellWidth+marginWidth)+"px'/>");
      cell.attr("ondragstart","_ondragstart(event)");
      $("body").append(cell);
      grid[r][c].o = cell;
    }
   }
</script>    

Et voila! We have our level on the screen. The following pictures show what could be the final results of loading our HTML page.

Image 9

How to Handle User Action?

The next step will be the handling of the user action. We will use the drag and drop browser ability in order to perform this step. Please keep in mind that the mobile browser does not implement drag and drop (October 2016) so I will investigate how to extend this prototype for the mobile phone in a future article!

We must handle three events: drag start, drag over and drag drop. Again, the simplest way is to use three simple functions.

HTML
<script>
  // executed when user clicks on a jewel
  function _ondragstart(a)
  {
    a.dataTransfer.setData("text/plain", a.target.id);
   }
   
   // executes when user moves the jewel over another jewel 
   // without releasing it
   function _onDragOverEnabled(e)
   {
     e.preventDefault();
     console.log("drag over " + e.target.id);
    }
           
    // executes when user releases jewel on other jewel
    function _onDrop(e)
    {
      // only for firefox! Firefox open new tab with dropped element as default
      // behavior. We hate it!
      var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
      if (isFirefox) {
        console.log("firefox compatibility");
        e.preventDefault();
      }
 
     // gets source jewel
     var src = e.dataTransfer.getData("text");
     var sr = src.split("_")[1];
     var sc = src.split("_")[2];

     // get destination jewel
     var dst = e.target.id;
     var dr = dst.split("_")[1];
     var dc = dst.split("_")[2];

     // check distance between jewel and avoid jump with distance > 1 ;)
     var ddx = Math.abs(parseInt(sr)-parseInt(dr));
     var ddy = Math.abs(parseInt(sc)-parseInt(dc));
     if (ddx > 1 || ddy > 1)
     {
       console.log("invalid! distance > 1");
       return;
     }
  
    // executes jewels swap
    var tmp = grid[sr][sc].src;
    grid[sr][sc].src = grid[dr][dc].src;
    grid[sr][sc].o.attr("src",grid[sr][sc].src);
    grid[dr][dc].src = tmp;
    grid[dr][dc].o.attr("src",grid[dr][dc].src);

    // searches for valid figures
    _checkAndDestroy();   
}

</script>    

Check for Valid Figures and Respawn

The last thing that we can handle is the checking of valid figures in THE game matrix and the RESPAWNING of destroyed objects. We need to implement two functions, one for SEARCHING for valid figures and one for DESTROYING and RESPAWNING.

The first function is named _checkAndDestroy. The following code SEARCHING for THE horizontal valid figure. I avoid PASTING THE entire function body because it is too long (this function is naive, there are many METHODS to implement this search in AN efficient way!).

HTML
<script>
        function _checkAndDestroy()
        {
              for (var r = 0; r < rows; r++)
              {
                    var prevCell = null;
                    var figureLen = 0;
                    var figureStart = null;
                    var figureStop = null;
                    for (var c=0; c< cols; c++)
                    {
                         // Bypass jewels that is in valid figures.
                         if (grid[r][c].locked || grid[r][c].isInCombo)
                         {
                             figureStart = null;
                             figureStop = null;
                             prevCell = null;
                             figureLen = 1;
                             continue;
                          }
                          // first cell of combo!
                          if (prevCell==null)
                          {
                            //console.log("FirstCell: " + r + "," + c);
                            prevCell = grid[r][c].src;
                            figureStart = c;
                            figureLen = 1;
                            figureStop = null;
                            continue;
                           }
                           else
                           {
                              //second or more cell of combo.
                              var curCell = grid[r][c].src;
                              // if current cell is not equal to prev cell 
                              // then current cell becomes new first cell!
                              if (!(prevCell==curCell))
                              {
                               //console.log("New FirstCell: " + r + "," + c);
                               prevCell = grid[r][c].src;
                               figureStart = c;
                               figureStop=null;
                               figureLen = 1;
                               continue;
                               }
                               else
                               {
                               // if current cell is equal to prevcell 
                               // then combo length is increased
                               // Due to combo, current combo 
                               // will be destroyed at the end of this procedure.
                               // Then, the next cell will become new first cell
                               figureLen+=1;
                               if (figureLen==3)
                               {
                                validFigures+=1;
                                figureStop = c;
                                console.log("Combo from " + figureStart + 
                                " to " + figureStop + "!");
                                for (var ci=figureStart;ci<=figureStop;ci++)
                                {
                                  grid[r][ci].isInCombo=true;
                                  grid[r][ci].src=null;
                                  //grid[r][ci].o.attr("src","");
                                 }
                                 prevCell=null;
                                 figureStart = null;
                                 figureStop = null;
                                 figureLen = 1;
                                 continue;
                             }
                    }
                  }
                }
              }
</script>    

After identification of all valid FIGURES, we need to destroy it and respawn empty cells. Then, before RETURNING the control to play, we need to re-check if after respawn, there are valid FIGURES. We need to check and destroy UNTIL there are no other valid FIGURES on THE game matrix.

Keep in mind: We MUST call CheckAndDestroy before GIVING the control to the player at startup time because the randomized approach can draw valid figures when preparing the level.

This is really simple (I added a fading animation in the code, ignore it if you don't want this animation.)

JavaScript
<script>    
   // execute the destroy fo cell
             function _executeDestroy()
             {             
                  for (var r=0;r<rows-1;r++)          
                      for (var c=0;c<cols-1;c++)
                          if (grid[r][c].isInCombo)  // this is an empty cell
                          {                              
                              grid[r][c].o.animate({
                                  opacity:0
                              },500);                          
                          }
             
                 $(":animated").promise().done(function() {
                      _executeDestroyMemory();
                });                              
             }             
             
             function _executeDestroyMemory() {
                   // move empty cells to top 
                  for (var r=0;r<rows-1;r++)
                  {                       
                      for (var c=0;c<cols-1;c++)
                      {                          
                          if (grid[r][c].isInCombo)  // this is an empty cell
                          {                                   
                              grid[r][c].o.attr("src","")
                               
                              // disable cell from combo 
                              // (The cell at the end of this routine will be on the top)
                            
                              grid[r][c].isInCombo=false;
                             
                              for (var sr=r;sr>=0;sr--)
                              {
                                  if (sr==0) break; // cannot shift. this is the first rows
                                  if (grid[sr-1][c].locked) 
                                      break; // cannot shift. my top is locked
                                  
                                      // shift cell
                                      var tmp = grid[sr][c].src;
                                        grid[sr][c].src=grid[sr-1][c].src;
                                    grid[sr-1][c].src=tmp;                                
                              }             
                          }                      
                      }                           
                 }                                       
                     
                     console.log("End of movement");
                                                    
                       //redrawing the grid
                       // and setup respaw                                 
                                                      
                       //Reset all cell
                    for (var r=0;r<rows-1;r++)
                     {    for (var c = 0;c<cols-1;c++)
                         {
                             grid[r][c].o.attr("src",grid[r][c].src);
                             grid[r][c].o.css("opacity","1");
                             grid[r][c].isInCombo=false;
                             if (grid[r][c].src==null) 
                                 grid[r][c].respawn=true;
                              // if respawn is needed
                              if (grid[r][c].respawn==true)
                             {                             
                                 grid[r][c].o.off("ondragover");
                                 grid[r][c].o.off("ondrop");
                                 grid[r][c].o.off("ondragstart");
                                 
                                 grid[r][c].respawn=false; // respawned!
                                 console.log("Respawning " + r+ "," + c);
                                 grid[r][c].src=pickRandomJewel();
                                 grid[r][c].locked=false;
                                 grid[r][c].o.attr("src",grid[r][c].src);
                                 grid[r][c].o.attr("ondragstart","_ondragstart(event)");
                                 grid[r][c].o.attr("ondrop","_onDrop(event)");
                                 grid[r][c].o.attr("ondragover","_onDragOverEnabled(event)");
                                 //grid[r][c].o.css("opacity","0.3");
                                 //grid[r][c].o.css("background-color","red");
                             }
                         }
                     }                      
                             
                     console.log("jewels resetted and rewpawned");
                     
                     // check for other valid figures
                     _checkAndDestroy();                            
             } 
  </script>

Play the Game!

You can play this prototype here.

Points of Interest

  1. The real game must ensure that each level can be won by the player!
  2. A real jewel game must allow locking the cell in order to change the shape of the level.
  3. The CheckAndDestroy method must be implemented in an efficient way!

History

  • Improving English - thanks to Natascia Spada @ 27/10/2016 - 14.42
  • Removed formatting issues (bold and italic in source code) and adding file for download @ 25/10/2016 - 12:00
  • Fixed a bug in example web page @ 25/10/2016 - 11:52
  • First publication @ 22/10/2016 - 12.40

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)