Stopbyte

How To Make Simple Jewel Game Using Javascript & HTML?

Hi There! :grinning:

This is our first post on stopbyte.com, then We would like to introduce our self before starting the article. We are a group of Italian developers that love to work in the Game Developing environments. We love to write about the challenges that day by day we face!

In this article, We want to study in which manner the jewel game works. Also, We want to show how to build a simple jewel game (like Candy Crush) for mobile phone browser. For the goal of this post, We choose to use jQuery in order to write code that can be executed in a wide range of devices.

A simple jewel game :v:

A little overview

All the jewel games share the same behaviors:

  1. The Player must move one jewel at time on a grid, in order to make lines, squares or other kind of geometrical figures;

  2. The Player must reach one or more goals in order to win the levels!

  3. When a geometrical figures is considered valid, the kernel of the game removes the figures and respawns new objects in the previous position. The respawn action can follow many rules.

At the end of this article, We will have a working game for mobile browser, so we just need to start with the game requirements. The name of this prototype will be STOPBYTE Saga!

Game requirements

We need a framework that allow us to write code executable on at least all mobile platform! We choose jQuery because it is supported in all the major mobile web browser like Chrome, Mobile Safari, Firefox and so forth. We don’t know if Internet Explorer and Edge support jQuery because we want that all people on the earth avoid to use these browsers :joy:!

For the purpose of this article, We will add some limitations in order to keep code simple:

  1. Only horizontal and vertical lines will be considered valid figures.

  2. Respawn will use only gravity. Then, the new object will then fall down when it has empty cells on the bottom border.

  3. To win a level, Player must make N valid figures in T seconds.

  4. We know that Each jewel games MUST assure that each level can be won by the player, but we don’t have time to draw the levels. So, our level will be created following randomized approach! We will spawn random jewels for each grid cell.

  5. We need to make available just one action: The player must be able to change the position of the jewels on the game matrix. In our game, the jewels can only be moved at distance 1 from the original position (on the x and y axis).

Some definitions

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! Other words to know is jQuery, Javascript Object and Drag n Drop, described below:

jQuery is a fast, small, and feature-rich JavaScript library. It makes things like HTML document traversal and manipulation, event handling, animation

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.

A JavaScript object is simply an object with methods and properties!

Shake our hands

For first, we need to define what image we want to use as jewel! Due to the name of our game (STOPBYTE Crush) we will use image that is related with bytes and bit! We will use following image as jewels.

The first goal to reach is the embedding of jQuery in our game. This action can be completed pasting jQuery reference from a CDN in an empty HTML page.

<html>
  <head>
    <script src="https://code.jquery.com/jquery-3.1.0.min.js"  integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" 
      crossorigin="anonymous">
    </script>
  </head>
  <body>
  </body>
</html>

The second goal to reach is the implementation of a grid and of the jewel in the memory of our browser.
We can define Grid and Jewels using Javascript object.

<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>

Now, 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. We omitted the arraylist in the code in order to mantain the post clear. You can see the complete code here.

PLEASE NOTE that we defined a function that return a random jewel! We will use this function when we create the level!

<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) -->
      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 the game
    var jewelsType=[];

            
    // 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.

Create our first 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.

<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>

At this point we have all the game object in memory. The next step is the drawing of the object on the screen!

How to Draw game object on the screen?

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, and for all articles of this series, I have chosen to use the DOM element in order to represent the game 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.

Now, we have a grid, a level in memory, the drawing coordinates and the size of jewels! We just need to create the 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.

<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>    

Good work! The game is now on the screen!

How to Handle the touch screen?

The next step will be the handling of the user action. We will use the touchscreen ability in order to perform this step.

Handling the touch screen is a little bit complex! We need to interact with some event like touchstart (when the user press the screen), touchend (when the user remove finger from the screen) and touchmove (again, when the user move the finger on the screen).

We need to take into account the initial position and the final position. Also, when the drag end, we need to check what jewel must be moved and if the new configuration of the grid have a valid figures. We will use a shadow element that will display the same jewel on which the event touchstart occured, and with this shadow icon we will track the finger movement.

When the player remove the finger from the screen, we will check the final position, the objects to swap and finally we will check entire grid in order to understand if a valid figures appear.

<script>
// touch capability
 			var startPos=[];
 			var toDrag = null;
 			var dragStart = false;
 			
 			$(document).on("touchstart",function(e) {
					
				 					
					e.preventDefault();
					
					var xPos = e.originalEvent.touches[0].pageX;
					var yPos = e.originalEvent.touches[0].pageY;
				
					startPos[0]=xPos;
					startPos[1]=yPos;
					
					$(".jewel").each(function(index)
					{
					
						var jx = parseFloat($(this).offset().left);
						var jy = parseFloat($(this).offset().top);
						var jw = parseFloat($(this).width());
						var jh = parseFloat($(this).height());
						
						
						if (jx <= xPos && jy <= yPos && xPos <= (jx+jw) && yPos <= (jy+jh))
						{
						
							var r = $(this).attr("id").split("_")[1];
							var c = $(this).attr("id").split("_")[2];
						
							if (grid[r][c].locked)
							{
								console.log("Collision on locked cell");
							}
							else {
								// collision detected
								console.log("collision on " + $(this).attr("id"));
								toDrag = grid[r][c];
								dragStart=true;
								
								
								// setting up shadow
							var shadow = document.getElementById("shadow");
							if (shadow==null)
							{
								console.log("Setting up shadow for dragging");
								shadow = document.createElement("img");
								document.getElementById("game").appendChild(shadow);
								shadow.setAttribute("src","");
								shadow.setAttribute("id","shadow");
								//shadow.style.visibility="hidden"; 
								shadow.style.position="absolute";
								shadow.style.zIndex="9999999";
							}
							
														
							$("#shadow").css("visibility","visible");
							
							var shadowImg = toDrag.src;
							
							console.log("Shadow image will be " + shadowImg);
							
							$("#shadow").attr("src",shadowImg);
							$("#shadow").css("left",xPos+"px");
							$("#shadow").css("top",yPos+"px");
							$("#shadow").css("width",jw+"px");
							$("#shadow").css("height",jh+"px");

								
								
								return;
							}
						}
						
					
					});
					
			});
			
			$(document).on("touchmove",function(e) {
				e.preventDefault();
				
				if (dragStart && toDrag!=null)
				{
				
						// move the shadow
						var xPos = e.originalEvent.touches[0].pageX;
						var yPos = e.originalEvent.touches[0].pageY;
						
						var sw = parseFloat($("#shadow").width());
						var sh = parseFloat($("#shadow").height());
						
						$("#shadow").css("left",(xPos-sw/2)+"px");
						$("#shadow").css("top",(yPos-sh/2)+"px");
				}
				
		});
		
		 
		
		
		$(document).on("touchend",function(e) {
				e.preventDefault();
				
				if (dragStart && toDrag!=null)
				{
				
						// hide the shadow and check if the drop is on target and on which
						$("#shadow").css("visibility","hidden");
						dragStart=false;
						 						
						var xPos = e.changedTouches[0].pageX;
						var yPos = e.changedTouches[0].pageY;
						
						console.log("Drop on " + xPos +"," + yPos);
						
						var target = null; 
												
						// find the destionation
						$(".jewel").each(function(index) {

							var tx = parseFloat($(this).offset().left);
							var ty = parseFloat($(this).offset().top);
							var tw = parseFloat($(this).width());
							var th = parseFloat($(this).height());
							
							
							if (tx <= xPos && ty <= yPos && xPos <= (tx+tw) && yPos <= (ty+th))
							{
								target = $(this).attr("id");
							}
					
 
						}).promise().done(function() 
						{
							if (target!=null)
								{
								
									console.log("jewel released on " + target);
									
									
			 
 										var src = toDrag.o.attr("id"); 
 										var sr = src.split("_")[1];
 										var sc = src.split("_")[2];
 				
 										var dst = target;
 				
 										var dr = dst.split("_")[1];
 										var dc = dst.split("_")[2];
 										
 										
 										
 										// check distance (max 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;
 										}
 										
 										
 										
 										
 										if (grid[dr][dc].locked)
 										{
 											console.log("you drop on locked cell!!!");
 											return;
 										}
 										
 										
 										
 										
 				
 				
 									console.log("swap " + sr + "," + sc+ " to " + dr + "," + dc);
 				
 										// execute 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);
 				
 				
 				
 									// search for combo
 									_checkAndDestroy(); 
 				
 							  	 
										
								}
							else
								console.log("drop out of targets");
								
								
							toDrag=false;
						});
						
						
						
				}
				
		});
 			
 		

</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!).

<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.)

<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! (Only desktop browser)

You can play this prototype here or on jsfiddle here.

2 Likes