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:
- We will consider as only valid horizontal and vertical lines.
- 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.
- 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.
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>
<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>
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>
<head>
<!--
<script src="https://code.jquery.com/jquery-3.1.0.min.js"
integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s="
crossorigin="anonymous">
</script>
<script>
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.
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>
<head>
<!--
<script src="https://code.jquery.com/jquery-3.1.0.min.js"
integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s="
crossorigin="anonymous">
</script>
<script>
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>
<head>
<!--
<script src="https://code.jquery.com/jquery-3.1.0.min.js"
integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s="
crossorigin="anonymous">
</script>
<script>
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 -->
}
}
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";
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.
<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.
<script>
var width = $('body').width();
var height = $('body').height();
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.
<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.
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.
<script>
function _ondragstart(a)
{
a.dataTransfer.setData("text/plain", a.target.id);
}
function _onDragOverEnabled(e)
{
e.preventDefault();
console.log("drag over " + e.target.id);
}
function _onDrop(e)
{
var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
if (isFirefox) {
console.log("firefox compatibility");
e.preventDefault();
}
var src = e.dataTransfer.getData("text");
var sr = src.split("_")[1];
var sc = src.split("_")[2];
var dst = e.target.id;
var dr = dst.split("_")[1];
var dc = dst.split("_")[2];
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;
}
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);
_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!).
<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++)
{
if (grid[r][c].locked || grid[r][c].isInCombo)
{
figureStart = null;
figureStop = null;
prevCell = null;
figureLen = 1;
continue;
}
if (prevCell==null)
{
prevCell = grid[r][c].src;
figureStart = c;
figureLen = 1;
figureStop = null;
continue;
}
else
{
var curCell = grid[r][c].src;
if (!(prevCell==curCell))
{
prevCell = grid[r][c].src;
figureStart = c;
figureStop=null;
figureLen = 1;
continue;
}
else
{
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;
}
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>
function _executeDestroy()
{
for (var r=0;r<rows-1;r++)
for (var c=0;c<cols-1;c++)
if (grid[r][c].isInCombo)
{
grid[r][c].o.animate({
opacity:0
},500);
}
$(":animated").promise().done(function() {
_executeDestroyMemory();
});
}
function _executeDestroyMemory() {
for (var r=0;r<rows-1;r++)
{
for (var c=0;c<cols-1;c++)
{
if (grid[r][c].isInCombo)
{
grid[r][c].o.attr("src","")
grid[r][c].isInCombo=false;
for (var sr=r;sr>=0;sr--)
{
if (sr==0) break;
if (grid[sr-1][c].locked)
break;
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");
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 (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;
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)");
}
}
}
console.log("jewels resetted and rewpawned");
_checkAndDestroy();
}
</script>
Play the Game!
You can play this prototype here.
Points of Interest
- The real game must ensure that each level can be won by the player!
- A real jewel game must allow locking the cell in order to change the shape of the level.
- 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