Mahjong Game – The End

Over the course of the previous posts, we’ve built and set up the entire infrastructure for our mahjong game. All that is left now is to create the actual game logic. Let’s start by going over the required functionality. What do we have in this game?

  1. The blocks are arranged in layers, and there can be multiple layers on a given board
  2. In each turn, the player must select two free, matching blocks to remove them from the board
  3. The game is won when there are no more blocks on the board
  4. The game is lost if there are still blocks on the board, but no moves are available to the player

In the previous post, we created a script tag for each block on the board, to help us to transfer the block metadata to the game client. In order to be able to read this metadata, we are going to use one of my favorite jquery plugins: metadata. The metadata plugin can read data about each element from attributes or script tags—in our case we used script tags—so using this method to fetch data is as simple as adding the following line somewhere at the start of the script:

$.metadata.setType("elem", "script");

Selecting Blocks

As stated above, each turn consists of the player selecting two blocks; if the blocks match each other and are not blocked by other blocks, they are removed from the board. Blocks are considered free if they can be moved either left or right without disturbing another block (i.e., there is no block immediately to their left or right, and there is no block above them). Let’s take a look at how we can check whether a block is free:

function freeToMove(el){
    var isFree = false;
    // If the element was marked free it can't be changed
    if ( el.is(".free") ){
        return true;
    }
        // Get some information with the metadata plugin
    var index = el.metadata()["index"];
    var layer = el.metadata()["layer"];
   
    // Check the same layer
    var $parent = el.parent("div#game_board");
    var cols = $parent.metadata()["cols"];
    var current_col = (index+1)%cols;
    var same_layer_class = ".layer_"+layer;
    var left_side_class = false;
    var right_side_class = false;
    if ( current_col != 1 ){
        left_side_class = "div.index_"+(index-2)+same_layer_class;
        left_side_class += ",div.index_"+(index-2-cols)+same_layer_class;
        left_side_class += ",div.index_"+(index-2+cols)+same_layer_class;
    }
    // Make sure there is room for a full block or dont check it
    if ( current_col < (cols - 1) ){
        right_side_class = "div.index_"+(index+2)+same_layer_class;
        right_side_class += ",div.index_"+(index+2-cols)+same_layer_class;
        right_side_class += ",div.index_"+(index+2+cols)+same_layer_class;
    }
    if (( $parent.find(left_side_class).length == 0 ) ||
        ( $parent.find(right_side_class).length == 0 )){
            /*
             * Nothing found on the right side or on the left side,
             * so the element is free from the sides...
             * But is it free from above?  
             * The server saved us some work by telling us if the block is
             * covered or not.
             */

            if ( el.metadata()["covered"] ){
                var upper_layer_class = ".layer_"+(layer+1);
                // The blocks above can be in 0.5 distance apart so we need to check a few classes
                // half before, the same, half after and the rows...
           
                var upper_classes = "div.index_"+(index-1)+upper_layer_class; // a bit to the left
                upper_classes += ",div.index_"+(index)+upper_layer_class; // directory above
                upper_classes += ",div.index_"+(index+1)+upper_layer_class; // a bit to the right
                upper_classes += ",div.index_"+(index-cols)+upper_layer_class; // a bit above
                upper_classes += ",div.index_"+(index+cols)+upper_layer_class; // a bit below
                upper_classes += ",div.index_"+(index-1-cols)+upper_layer_class; // a bit above to the left
                upper_classes += ",div.index_"+(index-1+cols)+upper_layer_class; // a bit below to the left
                upper_classes += ",div.index_"+(index+1-cols)+upper_layer_class; // a bit above to the right
                upper_classes += ",div.index_"+(index+1+cols)+upper_layer_class; // a bit below to the right
                // Even one of those blocks will prevent us from taking the block out
                if ( $parent.find(upper_classes).length == 0 ){
                    isFree = true;
                }
            } else {
                // Not covered we are ok
                isFree = true;
            }
    }
    $parent = null;
    return isFree;
}

Whew, that was a bit long. But at least it wasn’t complicated: What we did here was check whether the block has already been marked as free (since once a block is marked as free, it can’t ever be blocked again). If it’s not already marked as free, we then determine whether there are blocks to the left and right of the block. If there aren’t, we check the layer above the block, to see whether it’s covered by a block from that layer.
Now, let’s see what happens when the user selects a block. To do that, we will use the toggleBlock function:

function toggleBlock(event){
    var $block = $(this);
    // Check if block is free
    var index = $block.metadata()["index"];
    var layer = $block.metadata()["layer"];
    $block.toggleClass("selected");
    // Did we just marked this block?
    if ( $block.is(".selected") ) {
        $(document).trigger("flipped_block");
        // Make sure the block is free for marking
        if ( freeToMove($block) ){
            $block.find("img").attr("src", "images/selected_tile.png");
            // Check how many blocks selected if pair - act
            var $selected = $block.parent("div").find(".selected");
            if ($selected.length == 2){
                // Two blocks are free to move and selected, check if we
                // have a match
                var block_class = $block.metadata()["single"] ?
                                $block.metadata()["group_class"] :
                                $block.metadata()["block_class"];
                                           
                var $matched_blocks = $selected.filter("."+block_class);
                if ( $matched_blocks.length == 2 ){
                    $(document).trigger("found_match",
                                    {elements: $matched_blocks});
                } else {
                    $(document).trigger("not_a_match",
                                    {elements: $selected});
                }
                var $matched_cards = null;
            }          
            $selected = null;
        } else {
            $block.toggleClass("selected");
            $(document).trigger("blockNotFree", {element: $block});
        }
    } else {
        $block.find("img").attr("src", "images/hovered_tile.png");
    }
   
    $block = null;
};

The toggleBlock function is basically our “onClick”-type function: it will be called every time a block is clicked and select or unselect the block, assuming it’s not blocked by another. As can be expected, it uses the freeToMove function we just defined to determine this. In addition, if two free blocks are selected, the toggleBlock function will check for a match, and raise the appropriate event. This event will in turn call a function that will handle the blocks’ removal from the board, and check for a win/lose condition.

Removing Blocks

Removing blocks from the board and checking if the player won is fairly simple:

function foundMatchingBlocks(event, params){
    var $elements = params.elements;
    $elements.remove();

    // Check if we have blocks left
    if ( $("#game_board div.block").length == 0 ){
        // Game won
        $(document).trigger("game_won", {});
    }
    // Don't halt the progress to make this change
    setTimeout(function(){
        $(document).trigger("changed_num_blocks");
    }, 1);
    $elements = null;
};

One thing worth take a closer look at is the setTimeout function right in the middle: in my experience, when you start operations that can take a while it’s best to isolate them and separate them from our current call, which requires a more immediate response. You can see that within setTimeout’s scope we’re only triggering a custom event. We’re merely leaving a hook for future features – specifically, showing how many free blocks remain. This event can take a while to run, since it needs to scan the board for free elements (something that will take some time on IE), so putting these events in a different, unrelated scope can prevent our game from looking unresponsive. Please note that if the event was to trigger something which had to end before we could continue, we would have had to find a different method.

Now that we can handle two matching blocks, we also need to handle cases where the blocks do not match. This can be done using a simple function like this:

function clearMarkedBlocks(event, params){
    params.elements.removeClass("selected").find("img").attr("src", "images/hovered_tile.png").hide();
};

Someone Won?

Up until now we’ve been happily toggling blocks and removing matched pairs, but what do we do if someone won? We’ve seen that our foundMatchingBlocks function checks whether there are no more blocks left (our winning condition) and triggers the proper event, but what happens then? And also, what if there are no more moves?

function gameWon(){
    var $game_board = $("#game_board_container");
    var $player_won = $("#player_won");
    $game_board.hide();
    $player_won.show();
    $("#start_again").show();
    $game_board = $player_won = null;
};

function gameOver(){
    var $game_board = $("#game_board_container");
    var $player_lost = $("#player_lost");
    $game_board.hide();
    $player_lost.show();
    $("#start_again").show();
   
    $game_board = $player_lost = null;
};

Above are two really simple functions that simply handle the display of winning and losing. But… wait a minute… when and how do we check for the losing condition (no matching blocks left)? Well… remember the setTimeout-triggered event we talked about? Every time we remove blocks from the board we also trigger the changed_num_blocks event, which fires the following function:

function updateFreeBlocks(){
    $("#availableMoves").text("...");
    setTimeout(function(){
        var free = findFreeBlocks();
        $("#availableMoves").text(free);
        if ( !free ){
            $(document).trigger("game_over");
        }
    }, 1); 
};

The updateFreeBlocks function checks how many matching pairs are left on the board. If there are no matching pairs left, updateFreeBlocks will trigger the game over event; otherwise, it will just update the display with the new number of remaining matches. Because we already know how to tell whether a block is free, checking if we have any matches left is pretty simple. We can do it by mapping the free blocks, and counting for possible pairs, like so:

function findFreeBlocks(){
    var $blocks = $("#game_board .block");
    var block_classes = {};
    var pairs = 0;
    $.each($blocks, function(index, el){
        var $block = $(el);
        // Check if the block is free
        if ( freeToMove($block) || $block.is(".free") ){
            $block.addClass("free");
            var class_name = ($block.metadata()["single"] ?
                                 $block.metadata()["group_class"]                                  
                                : $block.metadata()["block_class"]);
            // If free set the counter
            block_classes[class_name] = ++block_classes[class_name] || 1;
            if ( block_classes[class_name] == 2 ){
                ++pairs;
                block_classes[class_name] = null;
                delete block_classes[class_name];
            }
        }
        $block = null;
    });

    $blocks = null;
    return pairs;
}

That’s Not All

Some more code is required to get the finishing touches done on the game, but I do believe we’ve got the core of the game covered. As always, you can find the rest of the javascript in the game demo itself:

Zip file for this tutorial

Demo for this tutorial

So until next time, have fun!

Adi Gabai

4 Responses to Mahjong Game – The End

  • This is the best script I have ever stumbled across! This brings back memories of Mahjong on Win 95 😛

    May we use this script on our websites? Can we modify the scripts? What are the terms and conditions on your scripts?

    Thanks for the great examples!
    Jack

  • Hi Jack,

    I’m glad you enjoyed it, there are more coming soon…

    You may use any script on this site without any terms, though a link back here would be most appreciated 😛

    Best Regards,
    Adi

  • Hi,
    nice tutorial, but i think that your implementation of Mahjong is to complicated. Why you decided to use php and javascript? I think that pure javascript could be much simpler, isn’t it? And what about arrays that describe layers. Maybe it could be simpler? How this implementation could be oversimplified?

    • Hi Joe,

      You are right that it can be done with just JavaScript – it is true for a lot of things. However, with this project my goal was also to demonstrate some server-side functionality (e.g. enabling the player to access the same board he started on one computer, from another computer), and for that I needed PHP.

      Regarding the layers arrays I’ll try to think of something simpler as soon as I have some spare time. Back when this post was written I wanted something that would make it simple to create new types of layouts in various shapes and sizes, and on which the various checks could be implemented with ease. The solution you read about is what came to mind :).

      If you have any ideas for improvements I’ll be happy to hear them.

      Thanks,
      Adi

Leave a Reply

Your email address will not be published. Required fields are marked *

Categories