HTML5 JavaScript Gem Game With Saved Scoreboard

Everyone is talking about HTML5 games and how they are the future of mobile gaming, but where do you start? What is it you need to do to create a game? In short the answer is actually “not much”, everything that I will be using in todays tutorial I’ve already covered in various different guises in my previous tutorials. We’ll be using only pure JavaScript and CSS in this tutorial and the images I’ve used are from the PlanetCute image pack from Daniel Cook

PlanetCute

Apologies in advance, but this is a fairly length tutorial. If you just want to play the game you can click here. This version is slightly different because of some issues with the CDN

Getting Started

The first thing we need to do is define what the game will do and what features it has. To keep things simple, the game will randomly generate gems of different value and place them on the game area. It will do this for a fixed amount of time, before the game is over. To highlight this, we’re going to display how many seconds are left and the current score. When the game is over we’re going to display the top 10 scores and if needed update them.

Now that we know what basic features we have, we can easily define the HTML for the game. We’ll need a play area, and somewhere to display the score and time left. We should also allow the user to start or restart the game and display the top 10 scores somewhere.

<section id="game">
    <span class="score">0</span>
    <span class="time">0</span>
    <span class="start">START!
    <ol class="high-scores">
    </ol>
    </span>            
</section>

We’ll then need to style the HTML elements using CSS. I’m going to absolutely position the score to the left and the time left to the right. I’ll also position the start button and high scores in the center.

ss1

section#game .score{
    display: block;
    position: absolute;
    top: 10px;
    left: 10px;
}

section#game .time{
    display: block;
    position: absolute;
    top: 10px;
    right: 10px;
}

section#game .start{
    display: block;
    padding-top: 50%;
    margin: 0 auto 0 auto;
    text-align: center;
    width: 50%;
    cursor: pointer;
}

section#game .start .high-scores{
    text-align: left;
}

Next up is the game play area

The Game Background

We’ve previously looked at CSS3s multiple background images, today we’re going to use the same trick. First we’re going to specify a width and height for our game play area, and then add the images in sequence, also applying the position and repeat properties in the same sequence.

section#game {
    width: 480px;
    height: 800px;
    max-width: 100%;
    max-height: 100%;
    overflow: hidden;
    position: relative;
    background-image: url('Tree Short.png'), url('Tree Tall.png'), url('Grass Block.png'), url('Sky.png');
    background-position: 80% 90%, 10% 90%, left bottom, left top;
    background-repeat: no-repeat, no-repeat, repeat-x, repeat-x;    
    position: relative;
    color: #ffffff;
    font-size: 30px;
    font-family: "arial,sans-serif";
}

ss2

The final pieces of CSS will be for the actual gems. They’ll also need to be positioned absolutely and we’ll need to specify a different gem image for each color

section#game .gem{
    display: block;
    position: absolute;
    width: 40px;
    height: 44px;
    cursor: pointer;
}

section#game .gem.green{
    background: url('Gem Green.png') no-repeat top left;
}

section#game .gem.blue{
    background: url('Gem Blue.png') no-repeat top left;
}

section#game .gem.orange{
    background: url('Gem Orange.png') no-repeat top left;
}

The JavaScript

For the actual game JavaScript we’re going to encapsulate all of our functions in a parent function. This will allow us to clearly define and break out the game logic.

The first thing we need to do is define our parent function and create references to our html elements

function Game(){
    var game = document.querySelector("section#game");
    var score = game.querySelector("section#game span.score");
    var high_scores = game.querySelector("section#game ol.high-scores");
    var time = game.querySelector("section#game span.time");
    var start = game.querySelector("section#game span.start");
)

Here you can see I’m using the document.querySelector API to get the game area, and then limiting the following queries to the game object e.g.

game.querySelector("section#game span.score");

This means that only the children of the game node will be queried, which will improve performance in larger games or HTML documents

The Gems

The next step is to define our gems. As I previously mentioned I’ve gone with 3 colored ones, which I will assign different values to. I’m also going to define the maximum time they will live for (TTL). To do this I’m going to create a function and define the 3 values as properties of it. I am then going to create 3 new instances of the function and assign them to an array for easy retrieval later.

function Gem(Class, Value, MaxTTL) {
    this.Class = Class;
    this.Value = Value;
    this.MaxTTL = MaxTTL;
};            

var gems = new Array();
gems[0] = new Gem('green', 10, 1.2);
gems[1] = new Gem('blue', 20, 1);
gems[2] = new Gem('orange', 50, 0.75);

Adding Gems

To actually make our gems appear we’re going to define a Spawn function that will randomly pick on the of the gem types, calculate a random position and then add it to the game area.

To pick a gem, we need to create a random number between 0 and 2, the indexes of the array. The Math.random function generates a random float value between 0 and 1, so to make the value be between 0 and 3 we need to multiple the random result by 3 and then use the Math.floor function to round it.

We’re going to use similar methods to the above to generate a random id value for our gem, the time to live (TTL) value and the X/Y co-ordinates. Once we have all of these values, we are then going to create a new span element and add it to the game area. The actual value of the gem will be stored using the data-* attributes

function Spawn() {
    var index = Math.floor( ( Math.random() * 3 ) );
    var gem = gems[index];
    
    var id = Math.floor( ( Math.random() * 1000 ) + 1 ); 
    var ttl = Math.floor( ( Math.random() * parseInt(gem.MaxTTL) * 1000 ) + 1000 );                
    var x = Math.floor( ( Math.random() * ( game.offsetWidth - 40 ) ) );
    var y = Math.floor( ( Math.random() * ( game.offsetHeight -  44 ) ) );
    
    var fragment = document.createElement('span');
    fragment.id = "gem-" + id;
    fragment.setAttribute('class', "gem " + gem.Class);
    fragment.setAttribute('data-value', gem.Value);        
    
    game.appendChild(fragment);

    fragment.style.left = x + "px";
    fragment.style.top = y + "px";

    setTimeout( function(){
        Remove(fragment.id);
    }, ttl)
}

The final step for this function is to setup up a timer event to remove the gem if it has not been clicked on. To do this we will use the setTimeout function and call our Remove function which we will create next.

ss3

Removing Gems

There are two ways of removing a gem from the game area, the first detailed above is the Remove method that gets called when the gem timeout expires. The second is to click on it, if you do succeed on clicking on a gem it’s value should be added to your total score.

function Remove(id) {
    var gem = game.querySelector("#" + id);
    
    if(typeof(gem) != 'undefined')
        gem.parentNode.removeChild(gem);
}

For the Click event, we first need to disable any other events from triggering. To prevent default event actions you can call the event.preventDefault, to prevent an event from bubbling up the event chain, you can call event.stopPropagation

Once that is done we then need to get the event target and make sure it was a gem. If a gem was clicked on, we can then pull out the value and add it to the current score and then remove the gem.

function Click(event) {
    if(event.preventDefault) event.preventDefault();
    if (event.stopPropagation) event.stopPropagation();
    else event.cancelBubble = true;
    
    var target = event.target || event.srcElement;
    
    if(target.className.indexOf('gem') > -1){
        var value = parseInt(target.getAttribute('data-value'));
        var current = parseInt( score.innerHTML );
        score.innerHTML = current + value;
        target.parentNode.removeChild(target);
    }
    
    return false;
}

ss4

Starting The Game

Now that we can add and remove gems as needed, we can start adding in the main game logic. To do this, we’re going to create a new function, but this name we’re going to make it public or externally available. We do this by using the following format:

this.Start = function(){
};

The Start function should reset the score and hide the Start link and top scores. It will also manage the timers used to display the time left and spawn new gems.

Previously we have used the setTimeout function to call a delayed once off function, this time we want to call a function repeatedly after a given period of time. To do this we use the setInterval function.

The first function we will schedule is the Spawn function we created earlier, for this one we’re going to specify an interval of 750ms. We’re also going to store the interval reference returned by the setInterval function. This will also us to cancel it at a later date.

this.Start = function() {
    score.innerHTML = "0";
    start.style.display = "none";
    var interval = setInterval(Spawn, 750);    
};

The next timer will be the countdown timer, for this to work we need to know how long a game should last. I’ve chosen 10 seconds to make games short and hopefully a little frantic. The timer function will check to see if the time left is greater than or equal to zero. If it is, it will update the time left element, if the game is over we will need to remove the left over gems and cancel the intervals. Once that is done we can update and display the top scores.

function Stop(interval) {
    clearInterval(interval);
}

this.Start = function() {
    score.innerHTML = "0";
    start.style.display = "none";
    var interval = setInterval(Spawn, 750);
    
    var count = 10;
    var counter = null;
    
    function timer()
    {
        count = count-1;
        if (count <= 0)
        {
            var left = document.querySelectorAll("section#game .gem");
            
            for (var i = 0; i < left.length; i++) {
                if(left[i] && left[i].parentNode) {
                    left[i].parentNode.removeChild(left[i]);
                }
            }
            Stop(interval);
            Stop(counter);
            time.innerHTML = "Game Over!";
            start.style.display = "block";
            
            UpdateScore();
            
            return;
        } else {
            time.innerHTML = count + "s left";
        }
    }
    
    counter = setInterval(timer, 1000); 
    
    setTimeout( function(){
        Stop(interval);
    }, count * 1000)
};

The Scores

To manage the scores we’re going to need to be able to display and update them. To allow us to store the scores between sessions we’re going to use the localStorage object. This will allow us to store a JSON object that we can turn into an array to keep track of scores.

Displaying The Scores

To display the scores, we will need to first verify we have access to local storage and then confirm that the high scores have been saved. If they haven’t we will just hide the high scores element for now until the game has been played.

function HighScores() {
    if(typeof(Storage)!=="undefined"){
        var scores = false;
        if(localStorage["high-scores"]) {
            high_scores.style.display = "block"; 
            high_scores.innerHTML = '';           
        }
    } else {
        high_scores.style.display = "none";
    }
}

We’re also going to have to make sure that the high scores are shown in order, to do this we use the array.sort function and pass in a custom sort function to ensure that we sort from highest to lowest score. Then all we need to do is loop through the scores and add a new list item to the high scores element.

function HighScores() {
    if(typeof(Storage)!=="undefined"){
        var scores = false;
        if(localStorage["high-scores"]) {
            high_scores.style.display = "block";
            high_scores.innerHTML = '';
            scores = JSON.parse(localStorage["high-scores"]);
            scores = scores.sort(function(a,b){return parseInt(b)-parseInt(a)});

            for(var i = 0; i < 10; i++){
                var s = scores[i];                        
                var fragment = document.createElement('li');
                fragment.innerHTML = (typeof(s) != "undefined" ? s : "" );
                high_scores.appendChild(fragment);
            }
        }
    } else {
        high_scores.style.display = "none";
    }
}

ss5

Updating The Scores

The UpdateScore function is very similar to the HighScores function, it however will parse out the current score and compare the value against the existing top scores. If it finds that it is a higher value than an existing score, it will use the array.splice function to insert it in to the correct position. Then it will force the array length to be 10 and use the JSON.stringify method to convert the array into a JSON string

function UpdateScore() {
    if(typeof(Storage)!=="undefined"){
        var current = parseInt(score.innerHTML);
        var scores = false;
        if(localStorage["high-scores"]) {

            scores = JSON.parse(localStorage["high-scores"]);
            scores = scores.sort(function(a,b){return parseInt(b)-parseInt(a)});
            
            for(var i = 0; i < 10; i++){
                var s = parseInt(scores[i]);
                
                var val = (!isNaN(s) ? s : 0 );
                if(current > val)
                {
                    val = current;
                    scores.splice(i, 0, parseInt(current));
                    break;
                }
            }
            
            scores.length = 10;                                
            localStorage["high-scores"] = JSON.stringify(scores);

        } else {                        
            var scores = new Array();
            scores[0] = current;
            localStorage["high-scores"] = JSON.stringify(scores);
        }
        
        HighScores();
    } 
}

Adding it all together

The final step is to actually bind the functions to the click events and to call the HighScores function when the game is initialized. In a previous tutorial on HTML5 drag and drop we used a function to make adding events easier. Today we’re going to use it again to add the events.

addEvent(game, 'click', Click);
addEvent(start, 'click', this.Start);
HighScores();
function addEvent(element, event, delegate ) {
    if (typeof(window.event) != 'undefined' && element.attachEvent)
        element.attachEvent('on' + event, delegate);
    else 
        element.addEventListener(event, delegate, false);
}

We’re also going to use the addEvent function to add a document ready event to initialize our game object only after the page has finished loading.

addEvent(document, 'readystatechange', function() {
    if ( document.readyState !== "complete" ) 
        return true;
        
    var game = new Game();        
});

The final JavaScript should look something like this. You can play the game and download the full source and all of the images below.

function addEvent(element, event, delegate ) {
    if (typeof (window.event) != 'undefined' &&∓∓ element.attachEvent)
        element.attachEvent('on' + event, delegate);
    else 
        element.addEventListener(event, delegate, false);
}        
    
function Game(){
    var game = document.querySelector("section#game");
    var score = game.querySelector("section#game span.score");
    var high_scores = game.querySelector("section#game ol.high-scores");
    var time = game.querySelector("section#game span.time");
    var start = game.querySelector("section#game span.start");
    
    function Gem(Class, Value, MaxTTL) {
        this.Class = Class;
        this.Value = Value;
        this.MaxTTL = MaxTTL;
    };            
    
    var gems = new Array();
    gems[0] = new Gem('green', 10, 1.2);
    gems[1] = new Gem('blue', 20, 1);
    gems[2] = new Gem('orange', 50, 0.75);
    
    function Click(event)
    {
        if(event.preventDefault) event.preventDefault();
        if (event.stopPropagation) event.stopPropagation();
        else event.cancelBubble = true;
        
        var target = event.target || event.srcElement;
        
        if(target.className.indexOf('gem') > -1){
            var value = parseInt(target.getAttribute('data-value'));
            var current = parseInt( score.innerHTML );
            score.innerHTML = current + value;
            target.parentNode.removeChild(target);
        }
        
        return false;
    }
    
    function Remove(id) {
        var gem = game.querySelector("#" + id);
        
        if(typeof(gem) != 'undefined')
            gem.parentNode.removeChild(gem);
    }
    
    function Spawn() {
        var index = Math.floor( ( Math.random() * 3 ) );
        var gem = gems[index];
        
        var id = Math.floor( ( Math.random() * 1000 ) + 1 ); 
        var ttl = Math.floor( ( Math.random() * parseInt(gem.MaxTTL) * 1000 ) + 1000 ); //between 1s and MaxTTL                
        var x = Math.floor( ( Math.random() * ( game.offsetWidth - 40 ) ) );
        var y = Math.floor( ( Math.random() * ( game.offsetHeight -  44 ) ) );
        
        var fragment = document.createElement('span');
        fragment.id = "gem-" + id;
        fragment.setAttribute('class', "gem " + gem.Class);
        fragment.setAttribute('data-value', gem.Value);        
        
        game.appendChild(fragment);

        fragment.style.left = x + "px";
        fragment.style.top = y + "px";

        setTimeout( function(){
            Remove(fragment.id);
        }, ttl)
    }
    
    function HighScores() {
        if(typeof(Storage)!=="undefined"){
            var scores = false;
            if(localStorage["high-scores"]) {
                high_scores.style.display = "block";
                high_scores.innerHTML = '';
                scores = JSON.parse(localStorage["high-scores"]);
                scores = scores.sort(function(a,b){return parseInt(b)-parseInt(a)});

                for(var i = 0; i < 10; i++){
                    var s = scores[i];                        
                    var fragment = document.createElement('li');
                    fragment.innerHTML = (typeof(s) != "undefined" ? s : "" );
                    high_scores.appendChild(fragment);
                }
            }
        } else {
            high_scores.style.display = "none";
        }
    }
    
    function UpdateScore() {
        if(typeof(Storage)!=="undefined"){
            var current = parseInt(score.innerHTML);
            var scores = false;
            if(localStorage["high-scores"]) {
    
                scores = JSON.parse(localStorage["high-scores"]);
                scores = scores.sort(function(a,b){return parseInt(b)-parseInt(a)});
                
                for(var i = 0; i < 10; i++){
                    var s = parseInt(scores[i]);
                    
                    var val = (!isNaN(s) ? s : 0 );
                    if(current > val)
                    {
                        val = current;
                        scores.splice(i, 0, parseInt(current));
                        break;
                    }
                }
                
                scores.length = 10;                                
                localStorage["high-scores"] = JSON.stringify(scores);

            } else {                        
                var scores = new Array();
                scores[0] = current;
                localStorage["high-scores"] = JSON.stringify(scores);
            }
            
            HighScores();
        } 
    }

    function Stop(interval) {
        clearInterval(interval);
    }
    
    this.Start = function() {
        score.innerHTML = "0";
        start.style.display = "none";
        var interval = setInterval(Spawn, 750);
        
        var count = 10;
        var counter = null;
        
        function timer()
        {
            count = count-1;
            if (count <= 0)
            {
                var left = document.querySelectorAll("section#game .gem");
                
                for (var i = 0; i < left.length; i++) {
                    if(left[i] && left[i].parentNode) {
                        left[i].parentNode.removeChild(left[i]);
                    }
                }
                Stop(interval);
                Stop(counter);
                time.innerHTML = "Game Over!";
                start.style.display = "block";
                
                UpdateScore();
                
                return;
            } else {
                time.innerHTML = count + "s left";
            }
        }
        
        counter = setInterval(timer, 1000); 
        
        setTimeout( function(){
            Stop(interval);
        }, count * 1000)
    };
    
    addEvent(game, 'click', Click);
    addEvent(start, 'click', this.Start);
    HighScores();
}

addEvent(document, 'readystatechange', function() {
    if ( document.readyState !== "complete" ) 
        return true;
        
    var game = new Game();        
});

I hope you enjoy the game and I look forward to seeing what you come up with!


This version is slightly different because of some issues with the CDN

SHARE THIS POST
  • Map

    The article offers information really interesting. Thic animals I feel about this. Really thank you.

  • Jonny Schnittger

    That’s great Tero, thank you very much. I’m glad you found the tutorial useful :)