Mahjong Game – The Board

In the previous posts we created most of the graphics needed for the mahjong game, and built the layout files that we will use. Now it’s time to put those layout files to some use.

The Layout Class

Our first step now will be to create a Layout class that will read our layout files. Let’s say we have a mahjong directory in which we’ve created a layout directory, which in turn contains our layout files.
The constructor for our Layout class can look something like this:

function __construct($layout_name) {
    $this->file = "layouts".DIRECTORY_SEPARATOR.
                         $layout_name.".inc.php";
    $this->loaded = $this->load_layout_from_file();
}

Every time the class is loaded, it will go and fetch a layout file, which can then be processed like so:

private function load_layout_from_file(){
    if ( file_exists($this->file) ){
        include $this->file;
        $this->map = $layout;
        $this->rows = $rows;
        $this->cols = $cols;
        return is_array($this->map);
    } else {
        return FALSE;
    }
}

Since our layer file already contains the variables ($layout, $rows, $cols), the process of loading and processing takes little time. By including the file we gain access to those variables, and can save them in our class. The layout variable should always be an array, since the layout itself is basically an array of positions.

At this point we can read the layout file and store it in our class, but we still need to build a game board out of it. To do that, we’re going to need two more classes: one to handle the board and one to handle a block on the board (AKA tile). But before we rush off to build these classes, let’s take another look at our layout file, because there’s quite a bit more information we can get out of it.

Another responsibility of the Layout class is processing the location of each block in each layer of the layout, and providing relevant information about it, such as its position or whether or not it is covered by a higher layer.
Here’s some code to clarify things:
Numbers of layers in the layout:

public function num_of_layers(){
    return count($this->map);
}

The position of a block:

public function get_block_coords($index){
    ++$index; // Easier to calc
    $b_width = constant("BLOCK_WIDTH");
    $b_height = constant("BLOCK_HEIGHT");
       
    // Find the index column
    $col = ($index % $this->cols) ? $index % $this->cols : $this->cols;
       
    // Find the index row
    $row = ceil($index / $this->cols);
       
    // Pieces on the board can be 0.5 apart
    $top = (($row - 1) * (constant('BLOCK_HEIGHT') / 2))-(($row-1)*2);
    $left = (($col - 1) * (constant('BLOCK_WIDTH') / 2))-(($col-1)*2);
       
    return array("pos"=>array("top"=>$top, "left"=>$left));
}

In the above method you can see that we’re placing the blocks according to their index in the array, but since our array has in fact double the actual number of columns and rows, we need to halve the result in order to get the right position. The end result of this method is the left and top positions of the block’s html element.

In addition to providing the positions of the separate blocks, the Layout class also needs to know how to provide the coordinates of the entire layer. We can do this by adding the following loop:

public function get_active_coords_by_layer($layer){
    $active = $this->get_active_indexes_by_layer($layer);
    $coords = array();
    for($i=0,$total=count($active);$i < $total;++$i){
        $coords[$i] = $this->get_block_coords($active[$i]);
        // Change the location to give a bit of a 3d look
        $coords[$i]["pos"]["top"] = $coords[$i]["pos"]["top"] - ($layer * 6);
        $coords[$i]["pos"]["left"] = $coords[$i]["pos"]["left"] - ($layer * 6);
        $coords[$i]["layer"] = $layer;
        $coords[$i]["index"] = $active[$i];
        $coords[$i]["zIndex"] = $this->map[$layer][$active[$i]];
        // Save some of the browser work since we are faster and see if this block is covered.
        // Indexes that might get in the way in upper layers are the ones that are
        // directly above, slightly to the left and slightly to the right of the block
        $covered = false;
        if ($this->is_block_spot($layer+1, $active[$i]) ||
            $this->is_block_spot($layer+1, $active[$i]-1) ||
            $this->is_block_spot($layer+1, $active[$i]+1) ||
            $this->is_block_spot($layer+1, $active[$i]+$this->cols) ||
            $this->is_block_spot($layer+1, $active[$i]-$this->cols) ||
            $this->is_block_spot($layer+1, $active[$i]-1+$this->cols) ||
            $this->is_block_spot($layer+1, $active[$i]+1+$this->cols) ||
            $this->is_block_spot($layer+1, $active[$i]-1-$this->cols) ||
            $this->is_block_spot($layer+1, $active[$i]+1-$this->cols)){
            $covered = true;
        }
        $coords[$i]["covered"] = ($covered) ? 'true' : 'false';
    }      
    return $coords;
}

What we did here was take the array holding the current layer, filter out the indexes which don’t contain blocks, so that we’re left only with cells that actually represent blocks. For each one of these cells, we checked the position value for the block, and used that to see if it is covered by a block in a higher layer. If you’re wondering how we can filter the active cells, it can be done like this:

private function check_has_block($var){
    return($var>0);
}
private function get_active_indexes_by_layer($layer){
    return array_keys(array_filter($this->map[$layer], array($this, "check_has_block")));
}

First we filter the array to get just the cells whose value is higher than zero, and then we request the keys of those cells.

Now our Layout class can really supply us with what we need in order to build the board, but as you can see it doesn’t build anything; it just returns an array of coordinates, which will be used to actually build the board (like in the Memory game). The one that will actually build the board will be the Board class, which will use the Block and Layout classes to generate that bodacious board-building code. And since we’ve already completed our Layout class, let’s get started on the Block class.

The Block Class

The Block class is pretty simple. Upon its creation it will get its index in the graphic map file we created earlier (i.e., which block it represents), and will then position the background image in the map file. It will do this by checking which row and column it’s located in; combined with the (already known) block dimensions, this provides the needed information. The block’s index in the graphics file will also tell the Block class if it’s a normal numbered block, or a group of special blocks that can exist only once and match each other.

function __construct($index) {
    $this->index = ++$index;
       
    // The structure of the image file is known
    $cols = constant("COLS_IN_IMAGE");
    $last_row_index = constant("LAST_ROW_MARKER");
    $special_group_size = constant("SPECIAL_GROUP_SIZE");
    if ( $index <= $last_row_index ){
        $this->row = ceil($index / $cols);
        $this->col = ($index % $cols) ? $index % $cols : $cols;
    } else {
        // We are at the last row - special groups
        $this->row = constant("ROWS_IN_IMAGE");;
        $this->col = $index - $last_row_index;
        $group_index = ceil($this->col / $special_group_size);
        $this->css_extra_class = "special_group_".$group_index;
        $this->single = TRUE;
    }
    $this->top = 0 - (($this->row - 1) * constant('BLOCK_HEIGHT'));
    $this->left = 0 - (($this->col - 1) * constant('BLOCK_WIDTH'));
    $this->css_class = $this->prefix.$index;
}

After setting all of these values, the block knows how to generate its css and html markup:

public function get_css_block(){
    return '.'.$this->get_name().'{background-position: '.
        $this->get_left().'px '.$this->get_top().'px;}';
}

public function get_positioned_html_block($params){
    $covered = $params["covered"];
    $pos = $params["pos"];
    $layer = $params["layer"];
    $index = $params["index"];
    $zIndex = $params["zIndex"];
       
    $style = ' style="top:'.$pos["top"].'px;left:'.$pos["left"].
         'px;z-index:'.$zIndex.';" ';
    $classes = 'block '.$this->get_name().' '.
           $this->css_extra_class.' index_'.$index.
           ' layer_'.$layer;
    return "r".
            '<div class="'.$classes.'"'.$style.'>'.
                '<script type="data">'.
                    '{index: '.$index.', layer: '.$layer.
                    ', block_class: ''.$this->css_class.'''.
                    ', group_class: ''.$this->css_extra_class.
                    '
', single: '.
                    ($this->single ? "true" : "false").
                    ', covered: '.$covered.'}'.
                '</script>'.
                '<img src="images/hovered_tile.png" alt=""/>'.
            '</div>';
}

As you can see in the above html method, the block will get its layout information as a parameter just to enable it to generate the markup; as an entity it only knows its type and which graphic to display.

Finally, the Board Class

As we’ve said before, the one that will bring all of the above together and use it all to create the actual board will be the Board class.
When the board is initialized it will gather all the required details:

function __construct($layout) {
    $blocks = array();
    $s_blocks = array(); // Some blocks are special
       
    // The css for a generic board needs to be the first so that it can be overriden easily
    $this->css[] = ".block{".
        "width: ".(constant('BLOCK_WIDTH'))."px;".
        "height:".(constant('BLOCK_HEIGHT'))."px;".
        "background: url(".constant('TILES_IMAGE').") no-repeat scroll;".
        "background-position: 0px 0px;".
        "position:absolute;".
        "float:none;".
    "}";
    // We run through all the blocks and get the block object and css for each of them
    // In addition to that, we need to separate the singles from the normal blocks so they will not be duplicated
    for ( $i = 0; $i < constant('BASE_CARDS'); ++$i ) {
        $block = new Block($i);
        if ( $block->can_duplicate() ){
            $blocks[] = $block;
        } else {
            $s_blocks[] = $block;
        }
       
        $this->css[] = $block->get_css_block();
    }
    // We need to have 2 pairs of each block, for a total of 4
    $this->blocks = array_merge($blocks, $blocks, $blocks, $blocks);
           
    // The special blocks are not duplicated, but we still need them
    $this->blocks = array_merge($this->blocks, $s_blocks);
           
    // Shuffle the blocks to create the order on the board
    shuffle($this->blocks);
           
    // Get the layout
    $this->layout = new Layout($layout);
}

After the Board class is initialized, we can easily get the html markup and the needed css:

function get_css(){
    return implode("n",$this->css);
}
       
function get_html(){
    $cols = $this->layout->get_visible_cols();
    $rows = $this->layout->get_visible_rows();
    $board_width =  $cols * constant("BLOCK_WIDTH");
    $board_height = $rows * constant("BLOCK_HEIGHT");
    $board_html = '<div id="game_board" style="position:relative;width: '
                .$board_width.'px;height:'.$board_height.'px;">';
    $board_html .= '<script type="data">{rows: '.$this->layout->get_rows().', '.
                'cols:'.$this->layout->get_cols().
                ', layers: '.$this->layout->num_of_layers().'}</script>';
    $layer = 0; // The first layer
    $blocks_counter = 0;
    for ( $layer=0, $num_of_layers=$this->layout->num_of_layers(); $layer < $num_of_layers ; ++$layer){    
        $active_blocks = $this->layout->get_active_coords_by_layer($layer);    
        // For each block in this layer
        for ($i=0,$total=count($active_blocks);$i<$total;++$i,++$blocks_counter){
            $block = $this->get_block($blocks_counter);
            $params = array("pos"=>$active_blocks[$i]["pos"],
                        "index"=>$active_blocks[$i]["index"],
                        "layer"=>$layer+1,
                        "zIndex"=>$active_blocks[$i]["zIndex"],
                        "covered"=>$active_blocks[$i]["covered"]);
            $board_html .= $block->get_positioned_html_block($params);
        }
    }
    $board_html .= '</div>';       
    print $board_html;
}

Thats about it. A simple file with a call to the following code can get us a nice looking mahjong game board. But we are still only halfway there: we still need to add the game logic, and we will do just that in our next post.

<?php  
    require "block.php";
    require "layout.php";
    require "board.php";
   
    define("BLOCK_WIDTH", 54);
    define("BLOCK_HEIGHT", 72);
    define("BASE_CARDS", 39);
    define("TILES_IMAGE", "images/tiles-2.png");
    define("COLS_IN_IMAGE", 9);
    define("ROWS_IN_IMAGE", 5);
    define("LAST_ROW_MARKER", 35);
    define("SPECIAL_GROUP_SIZE", 4);
       
    $layout_name = "classic";
    if ( $_REQUEST["layout"] ) $layout_name = $_REQUEST["layout"];
    else {
        $layouts = array("classic", "cat");
        $layout_name = $layouts[array_rand($layouts, 1)];
    }
    $board = new Board($layout_name);
?>
<style type="text/css">
<?php
    print $board->get_css();
?>
</style>
...
<div id="game_board_container">
<?php
    print $board->get_html();
?>
</div>

Until then, have fun!

Adi Gabai

Leave a Reply

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

Categories