This is a documentation for Board Game Arena: play board games online !

Tutorial gomoku: Difference between revisions

From Board Game Arena
Jump to navigation Jump to search
 
(44 intermediate revisions by 13 users not shown)
Line 1: Line 1:
This tutorial will guide you through the basics of creating a simple game on BGA Studio, through the example of [http://en.wikipedia.org/wiki/Gomoku '''Gomoku'''] (also known as Gobang or Five in a Row).
This tutorial will guide you through the basics of creating a simple game on BGA Studio, through the example of [http://en.wikipedia.org/wiki/Gomoku '''Gomoku'''] (also known as Gobang or Five in a Row).


== You will start from our 'emtpy game' template ==
== You will start from our 'empty game' template ==


Here is how your games looks by default when it has just been created :
Here is how your games looks by default when it has just been created:


[[File:Gomoku tuto1.png]]
[[File:Gomoku tuto1.png]]


== Setup the board ==
== Set up the board ==


Gather useful images for the game and edit them as needed. Upload them in the 'img' folder of your SFTP access.
Gather useful images for the game and edit them as needed. Upload them in the 'img' folder of your SFTP access.


Edit .tpl to add some divs for the board in the HTML. For example:
Edit <code>gomoku.js</code> (replace gomoku by your game name) to add the <code>divs</code> for your board at the beginning of the setup function:


<pre>
<pre>
document.getElementById('game_play_area').insertAdjacentHTML('beforeend', `
<div id="gmk_game_area">
<div id="gmk_game_area">
<div id="gmk_background">
<div id="gmk_background">
Line 20: Line 22:
</div>
</div>
</div>
</div>
`);
</pre>
</pre>


Line 38: Line 41:


#gmk_goban {
#gmk_goban {
background-image: url( '../../img/gomoku/goban.jpg');
background-image: url( 'img/Goban.jpg');
width: 620px;
width: 620px;
height: 620px;
height: 620px;
Line 44: Line 47:
}
}
</pre>
</pre>
You can find the goban board image here : https://en.doc.boardgamearena.com/File:Goban.jpg


[[File:Gomoku tuto2.png]]
[[File:Gomoku tuto2.png]]


== Setup the backbone of your game ==
== Set up the backbone of your game ==


Edit dbmodel.sql to create a table for intersections. We need coordinates for each intersection and a field to store the color of the stone on this intersection (if any).
Edit dbmodel.sql to create a table for intersections. We need coordinates for each intersection and a field to store the color of the stone on this intersection (if any).
Line 61: Line 66:
</pre>
</pre>


Edit gomoku.game.php->setupNewGame() to insert the empty intersections (19x19) with coordinates into the database.
Edit Game.php->setupNewGame() (in the modules/php dir) to insert the empty intersections (19x19) with coordinates into the database.


<pre>
<pre>
Line 73: Line 78:
             }
             }
         }
         }
         $sql .= implode( $values, ',' );
         $sql .= implode( ',', $values );
         self::DbQuery( $sql );
         $this->DbQuery( $sql );
</pre>
</pre>


Edit gomoku.game.php->getAllDatas() to retrieve the state of the intersections from the database.
Edit Game.php->getAllDatas() to retrieve the state of the intersections from the database.


<pre>
<pre>
         // Intersections
         // Intersections
         $sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection ";
         $sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection ";
         $result['intersections'] = self::getCollectionFromDb( $sql );
         $result['intersections'] = $this->getCollectionFromDb( $sql );
</pre>
</pre>


Edit .tpl to create a template for intersections.
Edit .js to create the function to generate intersections.


<pre>
<pre>
var jstpl_intersection='<div class="gmk_intersection ${stone_type}" id="intersection_${x}_${y}"></div>';
addIntersection: function(x, y, stone_color) {
            const stone_type = stone_color == null ? 'empty' : stone_color;
            document.getElementById('gmk_background').insertAdjacentHTML('beforeend', `<div class="gmk_intersection" data-color="${stone_type}" id="intersection_${x}_${y}"></div>`);
        }, 
</pre>
</pre>


Line 101: Line 109:
</pre>
</pre>


Edit gomoku.js->setup() to setup the intersections layer that will be used to get click events and to display the stones. The data you returned in $result['intersections'] in gomoku.game.php->getAllDatas() is now available in your gomoku.js->setup() in gamedatas.intersections.
Edit .js->setup() to setup the intersections layer that will be used to get click events and to display the stones. The data you returned in $result['intersections'] in Game.php->getAllDatas() is now available in your .js->setup() in gamedatas.intersections.


<pre>
<pre>
Line 109: Line 117:
                 var intersection = gamedatas.intersections[id];
                 var intersection = gamedatas.intersections[id];


                 dojo.place( this.format_block('jstpl_intersection', {
                 this.addIntersection(intersection.coord_x, intersection.coord_y, intersection.stone_color);
                    x:intersection.coord_x,
                    y:intersection.coord_y,
                    stone_type:(intersection.stone_color == null ? "no_stone" : 'stone_' + intersection.stone_color)
                } ), $ ( 'gmk_background' ) );


                 var x_pix = this.getXPixelCoordinates(intersection.coord_x);
                 var x_pix = this.getXPixelCoordinates(intersection.coord_x);
Line 122: Line 126:
                 if (intersection.stone_color != null) {
                 if (intersection.stone_color != null) {
                     // This intersection is taken, it shouldn't appear as clickable anymore
                     // This intersection is taken, it shouldn't appear as clickable anymore
                     dojo.removeClass( 'intersection_' + intersection.coord_x + '_' + intersection.coord_y, 'clickable' );
                     document.getElementById('intersection_' + intersection.coord_x + '_' + intersection.coord_y).classList.remove('clickable');
                 }
                 }
             }  
             }  
Line 139: Line 143:
</pre>
</pre>


You can declare some constants in material.inc.php and pass them to your gomoku.js for easy repositioning (modify constant, refresh). This is especially useful if the same constants have to be used on the server and on the client.
You can declare some constants on top of your .js for easy repositioning
 
* Declare your constants in material.inc.php (this will be automatically included in your gomoku.game.php)


<pre>
<pre>
$this->gameConstants = array(
const INTERSECTION_WIDTH = 30;
"INTERSECTION_WIDTH" => 30,
const INTERSECTION_HEIGHT = 30;
"INTERSECTION_HEIGHT" => 30,
const INTERSECTION_X_SPACER = 3;
"INTERSECTION_X_SPACER" => 2.8, // Float
const INTERSECTION_Y_SPACER = 3;
"INTERSECTION_Y_SPACER" => 2.8, // Float
const X_ORIGIN = 10;
"X_ORIGIN" => 0,
const Y_ORIGIN = 10;
"Y_ORIGIN" => 0,
);
</pre>
</pre>
* In gomoku.game.php->getAllDatas(), add the constants to the result array
        // Constants
        $result['constants'] = $this->gameConstants;
* In gomoku.js constructor, define a class variable for constants
        // Game constants
      this.gameConstants = null;


* Then use it in your getXPixelCoordinates and getYPixelCoordinates functions
* Then use it in your getXPixelCoordinates and getYPixelCoordinates functions


        getXPixelCoordinates: function( intersection_x )
        getXPixelCoordinates: function( intersection_x )
        {
        {
        return this.gameConstants['X_ORIGIN'] + intersection_x * (this.gameConstants['INTERSECTION_WIDTH'] + this.gameConstants['INTERSECTION_X_SPACER']);  
        return X_ORIGIN + intersection_x * (INTERSECTION_WIDTH + INTERSECTION_X_SPACER);  
        },
        },
       
       
        getYPixelCoordinates: function( intersection_y )
        getYPixelCoordinates: function( intersection_y )
        {
        {
        return this.gameConstants['Y_ORIGIN'] + intersection_y * (this.gameConstants['INTERSECTION_HEIGHT'] + this.gameConstants['INTERSECTION_Y_SPACER']);  
        return Y_ORIGIN + intersection_y * (INTERSECTION_HEIGHT + INTERSECTION_Y_SPACER);  
        },
        },


Here is what you should get:
Here is what you should get:
Line 182: Line 172:
== Manage states and events ==
== Manage states and events ==


Define your game states in states.inc.php. For gomoku we will use 3 states. One to play, one to check the end game condition, one to give his turn to the other player if the game is not over.
Define your game states in states.inc.php. For gomoku we will use 3 states in addition of the predefined states 1 (gameSetup) and 99 (gameEnd). One to play, one to check the end game condition, one to give his turn to the other player if the game is not over.


The first state requires an action from the player, so its type is 'activeplayer'.
The first state requires an action from the player, so its type is 'activeplayer'.
Line 196: Line 186:
         "descriptionmyturn" => clienttranslate('${you} must play a stone'),
         "descriptionmyturn" => clienttranslate('${you} must play a stone'),
         "type" => "activeplayer",
         "type" => "activeplayer",
         "possibleactions" => array( "playStone" ),
         "possibleactions" => array( "actPlayStone" ),
         "transitions" => array( "stonePlayed" => 3, "zombiePass" => 3 )
         "transitions" => array( "stonePlayed" => 3, "zombiePass" => 3 )
     ),
     ),
Line 218: Line 208:
</pre>
</pre>


Implement the 'stNextPlayer()' function in gomoku.game.php to manage turn rotation. Except if there are special rules for the game turn depending on context, this is really easy:
Implement the 'stNextPlayer()' function in Game.php to manage turn rotation. Except if there are special rules for the game turn depending on context, this is really easy:


<pre>
<pre>
     function stNextPlayer()
     function stNextPlayer()
     {
     {
     self::trace( "stNextPlayer" );
     $this->trace( "stNextPlayer" );
      
      
     // Go to next player
     // Go to next player
     $active_player = self::activeNextPlayer();
     $active_player = $this->activeNextPlayer();
     self::giveExtraTime( $active_player );     
     $this->giveExtraTime( $active_player );     
      
      
     $this->gamestate->nextState();
     $this->gamestate->nextState();
Line 233: Line 223:
</pre>
</pre>


Add onclick events on intersections in gomoku.js->setup()
Implement the 'stCheckEndOfGame()' function in Game.php to manage the state 3. The real implementation will be exposed later, but for now just create it to be able to run the game:
 
<pre>
    function stCheckEndOfGame()
    {
        $this->trace('stCheckEndOfGame');
 
        $this->gamestate->nextState('notEndedYet');
    }
</pre>
 
Add onclick events on intersections in .js->setup()


             // Add events on active elements (the third parameter is the method that will be called when the event defined by the second parameter happens - this method must be declared beforehand)
             // Add events on active elements (the third parameter is the method that will be called when the event defined by the second parameter happens - this method must be declared beforehand)
             this.addEventToClass( "gmk_intersection", "onclick", "onClickIntersection");
             this.addEventToClass( "gmk_intersection", "onclick", "onClickIntersection");


Declare the corresponding gomoku.js->onClickIntersection() function, which calls an action function on the server with appropriate parameters
Declare the corresponding .js->onClickIntersection() function, which calls an action function on the server with appropriate parameters.


<pre>
<pre>
Line 246: Line 247:
             dojo.stopEvent( evt );
             dojo.stopEvent( evt );


             if( ! this.checkAction( 'playStone' ) )
             if( ! this.checkAction( 'actPlayStone' ) )
             { return; }
             { return; }


Line 256: Line 257:
              
              
             if ( this.isCurrentPlayerActive() ) {
             if ( this.isCurrentPlayerActive() ) {
                 this.ajaxcall( "/gomoku/gomoku/playStone.html", { lock: true, coord_x: coord_x, coord_y: coord_y }, this, function( result ) {}, function( is_error ) {} );
                 this.bgaPerformAction('actPlayStone', { coord_x: coord_x, coord_y: coord_y });
             }
             }
         },
         },
</pre>
</pre>


Add this action function in gomoku.action.php, retrieving parameters and calling the appropriate game action
Add game action in Game.php to update the database, send a notification to the client providing the event notified (‘stonePlayed’) and its parameters, and proceed to the next state.


<pre>
<pre>
     public function playStone()
     function actPlayStone(int $coord_x, int $coord_y)
    {
     {      
        self::setAjaxMode();   
         $player_id = $this->getActivePlayerId();
 
        // Retrieve arguments
        // Note: these arguments correspond to what has been sent through the javascript "ajaxcall" method
        $coord_x = self::getArg( "coord_x", AT_posint, true );
        $coord_y = self::getArg( "coord_y", AT_posint, true );
 
        // Then, call the appropriate method in your game logic, like "playCard" or "myAction"
        $this->game->playStone( $coord_x, $coord_y );
 
        self::ajaxResponse( );
    }
</pre>
 
Add game action in gomoku.game.php to update the database, send a notification to the client providing the event notified (‘stonePlayed’) and its parameters, and proceed to the next state.
 
<pre>
    function playStone( $coord_x, $coord_y )
     {
        // Check that this is player's turn and that it is a "possible action" at this game state (see states.inc.php)
        self::checkAction( 'playStone' );
       
         $player_id = self::getActivePlayerId();
          
          
         // Check that this intersection is free
         // Check that this intersection is free
Line 300: Line 279:
                     AND stone_color is null
                     AND stone_color is null
               ";
               ";
         $intersection = self::getObjectFromDb( $sql );
         $intersection = $this->getObjectFromDb( $sql );


         if ($intersection == null) {
         if ($intersection == null) {
             throw new BgaUserException( self::_("There is already a stone on this intersection, you can't play there") );
             throw new \BgaUserException( $this->_("There is already a stone on this intersection, you can't play there") );
         }
         }


Line 314: Line 293:
                     player_id = $player_id
                     player_id = $player_id
               ";
               ";
         $player = self::getNonEmptyObjectFromDb( $sql );
         $player = $this->getNonEmptyObjectFromDb( $sql );
         $color = ($player['player_color'] == 'ffffff' ? 'white' : 'black');
         $color = ($player['player_color'] == 'ffffff' ? 'white' : 'black');


Line 326: Line 305:
                     id = $intersection_id
                     id = $intersection_id
               ";
               ";
         self::DbQuery($sql);
         $this->DbQuery($sql);
          
          
         // Notify all players
         // Notify all players
         self::notifyAllPlayers( "stonePlayed", clienttranslate( '${player_name} dropped a stone ${coordinates}' ), array(
         $this->notifyAllPlayers( "stonePlayed", clienttranslate( '${player_name} dropped a stone on ${coord_x},${coord_y}' ), array(
             'player_id' => $player_id,
             'player_id' => $player_id,
             'player_name' => self::getActivePlayerName(),
             'player_name' => $this->getActivePlayerName(),
            'coordinates' => $this->getFormattedCoordinates($coord_x, $coord_y),
             'coord_x' => $coord_x,
             'coord_x' => $coord_x,
             'coord_y' => $coord_y,
             'coord_y' => $coord_y,
Line 343: Line 321:
</pre>
</pre>


Catch the notification in gomoku.js->setupNotifications() and link it to a javascript function to execute when the notification is received.
Catch the notification in .js->setupNotifications() and link it to a javascript function to execute when the notification is received.


<pre>
<pre>
Line 350: Line 328:
             console.log( 'notifications subscriptions setup' );
             console.log( 'notifications subscriptions setup' );
          
          
             dojo.subscribe( 'stonePlayed', this, "notif_stonePlayed" );
             const notifs = [
                ['stonePlayed', 1],
            ];
   
            notifs.forEach((notif) => {
                dojo.subscribe(notif[0], this, `notif_${notif[0]}`);
                this.notifqueue.setSynchronous(notif[0], notif[1]);
            });
         }
         }
</pre>
</pre>
Line 363: Line 348:


             // Create a stone
             // Create a stone
             dojo.place( this.format_block('jstpl_stone', {
             document.getElementById( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ).insertAdjacentHTML('beforeend', `<div class="gmk_stone" data-color="${notif.args.color}" id="stone_${notif.args.coord_x}_${notif.args.coord_y}"></div>`);
                    stone_type:'stone_' + notif.args.color,
                    x:notif.args.coord_x,
                    y:notif.args.coord_y
                } ), $( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ) );


             // Place it on the player panel
             // Place it on the player panel
Line 373: Line 354:


             // Animate a slide from the player panel to the intersection
             // Animate a slide from the player panel to the intersection
             dojo.style( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y, 'zIndex', 1000 );
             dojo.style( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y, 'zIndex', 1 );
             var slide = this.slideToObject( $( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ), 1000 );
             var slide = this.slideToObject( $( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ), 1000 );
             dojo.connect( slide, 'onEnd', this, dojo.hitch( this, function() {
             dojo.connect( slide, 'onEnd', this, dojo.hitch( this, function() {
        dojo.style( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y, 'zIndex', 'auto' );
                        // At the end of the slide, update the intersection
      }));
                        document.getElementById( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y).dataset.color = notif.args.color;
                        dojo.removeClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'clickable' );
       
                        // We can now destroy the stone since it is now visible through the change in style of the intersection
                        dojo.destroy( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y );
          }));
             slide.play();
             slide.play();
         
            // This intersection is taken, it shouldn't appear as clickable anymore
            dojo.removeClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'clickable' );
         },
         },
</pre>
</pre>


For this function to work properly, you also need:
For this function to work properly, you also need to define the css styles for the stones
 
* to declare a stone javascript template in your .tpl file.


<pre>
<pre>
var jstpl_stone='<div class="gmk_stone ${stone_type}" id="stone_${x}_${y}"></div>';
.gmk_intersection, .gmk_stone {
</pre>
 
* to define the css styles for the stones
 
<pre>
.gmk_stone {
width: 30px;
width: 30px;
height: 30px;
height: 30px;
    position: absolute;
     background-image: url( 'img/stones.png');
     background-image: url( '../../img/gomoku/stones.png');
background-position:-60px 0px;
}
}


.no_stone { background-position: -60px 0px; }
.gmk_intersection[data-color="black"], .gmk_stone[data-color="black"] {
background-position:0px 0px;
}


.stone_black {  background-position: 0px 0px; }
.gmk_intersection[data-color="white"], .gmk_stone[data-color="white"] {
.stone_white { background-position: -30px 0px; }
background-position:-30px 0px
}
</pre>
</pre>


Line 424: Line 402:
</pre>
</pre>


* in gomoku.js, when we enter the 'playerTurn' state, we add the 'clickable' style to the intersections where there is no stone
* in .js, when we enter the 'playerTurn' state, we add the 'clickable' style to the intersections where there is no stone


<pre>
<pre>
Line 437: Line 415:
                     if( this.isCurrentPlayerActive() )
                     if( this.isCurrentPlayerActive() )
                     {
                     {
                         var queueEntries = dojo.query( '.no_stone' );
                         document.querySelectorAll('.gmk_intersection[data-color="empty"]').forEach(elem => elem.classList.add('clickable'));
                    for(var i=0; i<queueEntries.length; i++) {              
                  dojo.addClass( queueEntries[i], 'clickable' );
                    }
                     }             
                     }             
             }
             }
         },
         },
</pre>
</pre>
Finally, make sure to modify the default colors for players to white and black
          $default_colors = array( "000000", "ffffff", );


The basic game turn is implemented: you can now drop some stones!
The basic game turn is implemented: you can now drop some stones!
Line 456: Line 435:
[[File:Gomoku tuto5.png]]
[[File:Gomoku tuto5.png]]


== Implement rules and end of game conditions ==
== Add some counters in the player panels ==
 
Edit .js->setup() to setup the player panels with this extra information
 
<pre>
            // Setting up player boards
            for( var player_id in gamedatas.players )
            {
                var player = gamedatas.players[player_id];
                       
                // Setting up players boards if needed
                document.getElementById('player_board_'+player_id).insertAdjacentHTML('beforeend', `
                <div class="cp_board">
                    <div id="stoneicon_p${player.player_id}" class="gmk_stoneicon" data-color="${player.color}"></div><span id="stonecount_p${player.player_id}">0</span>
                </div>
                `);
            }
</pre>
 
Add some styles in your .css
 
<pre>
.gmk_stoneicon {
width: 14px;
height: 14px;
display: inline-block;
    position: relative;
    background-repeat: no-repeat;
    background-image: url( 'img/stone_icons.png');
    background-position: -28px 0px;
    margin-top: 4px;
    margin-right: 3px;
}
 
.gmk_stoneicon[data-color="000000"] {
background-position:0px 0px
}
 
.gmk_stoneicon[data-color="ffffff"] {
background-position:-14px 0px
}
 
.cp_board {
clear: both;
}
</pre>
 
[[File:Stone_icons.png]]
 
In your Game.php, create a function to return your game counters
 
<pre>
    /*
        getGameCounters:
       
        Gather all relevant counters about current game situation (visible by the current player).
    */
    function getGameCounters($player_id) {
    $sql = "
    SELECT
    concat('stonecount_p', cast(p.player_id as char)) counter_name,
    case when p.player_color = 'white' then 180 - count(id) else 181 - count(id) end counter_value
    FROM (select player_id, case when player_color = 'ffffff' then 'white' else 'black' end player_color FROM player) p
    LEFT JOIN intersection i on i.stone_color = p.player_color
    GROUP BY p.player_color, p.player_id
    ";
    if ($player_id != null) {
    // Player private counters: concatenate extra SQL request with UNION using the $player_id parameter
    }
   
    return $this->getNonEmptyCollectionFromDB( $sql );
    }
</pre>
 
Return your game counters in your Game.php->getAllDatas()
 
<pre>
        // Counters
        $result['counters'] = $this->getGameCounters($current_player_id);
</pre>
 
And pass them in any notification that needs to update them e.g. Include 'counters' in the "stonePlayed" notification code
 
<pre>
        // Notify all players
        $this->notifyAllPlayers( "stonePlayed", clienttranslate( '${player_name} dropped a stone ${coordinates}' ), array(
            'player_id' => $player_id,
            'player_name' => $this->getActivePlayerName(),
            'coord_x' => $coord_x,
            'coord_y' => $coord_y,
            'color' => $color,
            'counters' => $this->getGameCounters($this->getCurrentPlayerId())
        ) );
</pre>
 
Finally, in your .js->setup() function call
 
<pre>
this.updateCounters(gamedatas.counters);
</pre>
 
and in notif_stonePlayed() notification handler function call
 
<pre>
this.updateCounters(notif.args.counters);
</pre>
 
And define the updateCounters function in your js file (above setup() to avoid error)
<pre>
updateCounters: function( counters )
{
    for( var counter_container_id in counters )
    {
        var counter_value = counters[counter_container_id]['counter_value'];
        document.getElementById(counters[counter_container_id]['counter_name']).innerText = counter_value;
    }
}
</pre>


Implement specific rules for the game. For example in Gomoku, black plays first. So in gomoku.game.php->setupNewGame():
You now have a working counter!


* modify the default colors for players to white and black
[[File:Gomoku tuto7.png]]


          $default_colors = array( "000000", "ffffff", );
== Implement rules and end of game conditions ==


* and at the end of the setup make the black player active
Implement specific rules for the game. For example in Gomoku, black plays first. So in Game.php->setupNewGame(), at the end of the setup make the black player active:


<pre>
<pre>
         // Black plays first
         // Black plays first
         $sql = "SELECT player_id, player_name FROM player WHERE player_color = '000000' ";
         $sql = "SELECT player_id, player_name FROM player WHERE player_color = '000000' ";
         $black_player = self::getNonEmptyObjectFromDb( $sql );
         $black_player = $this->getNonEmptyObjectFromDb( $sql );


         $this->gamestate->changeActivePlayer( $black_player['player_id'] );
         $this->gamestate->changeActivePlayer( $black_player['player_id'] );
</pre>
</pre>


Implement rule for computing game progression in gomoku.game.php->getGameProgression(). For Gomoku we will use the rate of occupied intersections over the total number of intersections. This will often be wildly inaccurate as the game can end pretty quickly, but it's about the best we can do (the game can drag to a stalemate with all intersections occupied and no winner).
Implement rule for computing game progression in Game.php->getGameProgression(). For Gomoku we will use the rate of occupied intersections over the total number of intersections. This will often be wildly inaccurate as the game can end pretty quickly, but it's about the best we can do (the game can drag to a stalemate with all intersections occupied and no winner).


<pre>
<pre>
Line 485: Line 581:
    SELECT round(100 * count(id) / (19*19) ) as value from intersection WHERE stone_color is not null
    SELECT round(100 * count(id) / (19*19) ) as value from intersection WHERE stone_color is not null
     ";
     ";
     $counter = self::getNonEmptyObjectFromDB( $sql );
     $counter = $this->getNonEmptyObjectFromDB( $sql );


         return $counter['value'];
         return $counter['value'];
Line 493: Line 589:
Implement end of game detection and update the score according to who is the winner. It is easier to check for a win directly after setting the stone, so:  
Implement end of game detection and update the score according to who is the winner. It is easier to check for a win directly after setting the stone, so:  


* declare a global 'end_of_game' variable in gomoku.game.php->Gomoku()
* declare a global 'end_of_game' variable in Game.php->__construct()


         self::initGameStateLabels( array(
         $this->initGameStateLabels( array(
                   "end_of_game" => 10,
                   "end_of_game" => 10,
         ) );
         ) );


* we init that global variable to 0 in gomoku.game.php->setupNewGame()
* init that global variable to 0 in Game.php->setupNewGame()


         self::setGameStateInitialValue( 'end_of_game', 0 );
         $this->setGameStateInitialValue( 'end_of_game', 0 );


* add the appropriate code in gomoku.game.php before proceeding to the next state, using a checkForWin() function implemented separately for clarity. If the game has been won, we set the score, notify it to update the score on the client side, and set the 'end_of_game' global variable to 1 as a flag signaling that the game has ended.
* add the appropriate code in Game.php before proceeding to the next state, using a checkForWin() function implemented separately for clarity. If the game has been won, we set the score, send a score update notification to the client side, and set the 'end_of_game' global variable to 1 as a flag signaling that the game has ended.


<pre>
<pre>
Line 511: Line 607:
             // Set active player score to 1 (he is the winner)
             // Set active player score to 1 (he is the winner)
             $sql = "UPDATE player SET player_score = 1 WHERE player_id = $player_id";
             $sql = "UPDATE player SET player_score = 1 WHERE player_id = $player_id";
             self::DbQuery($sql);
             $this->DbQuery($sql);


             // Notify final score
             // Notify final score
Line 517: Line 613:
     clienttranslate( '${player_name} wins the game!' ),
     clienttranslate( '${player_name} wins the game!' ),
     array(
     array(
     "player_name" => self::getActivePlayerName(),
     "player_name" => $this->getActivePlayerName(),
     "player_id" => $player_id,
     "player_id" => $player_id,
     "score_delta" => 1,
     "score_delta" => 1,
Line 524: Line 620:


             // Set global variable flag to pass on the information that the game has ended
             // Set global variable flag to pass on the information that the game has ended
             self::setGameStateValue('end_of_game', 1);
             $this->setGameStateValue('end_of_game', 1);


             // End of game message
             // End of game message
Line 536: Line 632:
</pre>
</pre>


Add the checkForWin() function into the Utility Functions area in your Game.php.
<pre>
    function checkForWin( $coord_x, $coord_y, $color )
    {
        // Get intersections in the same row
        $sql = "SELECT
                    id, coord_x, coord_y, stone_color
                FROM
                    intersection
                WHERE
                    coord_y = $coord_y
                ORDER BY
                    coord_x
              ";
        $intersections = $this->getCollectionFromDb( $sql );
        if ( $this->checkFiveInARow( $intersections, $coord_x, $coord_y, $color ) ) {
            return true;
        }
        // Get intersections in the same column
        $sql = "SELECT
                    id, coord_x, coord_y, stone_color
                FROM
                    intersection
                WHERE
                    coord_x = $coord_x
                ORDER BY
                    coord_y
              ";
        $intersections = $this->getCollectionFromDb( $sql );
        if ( $this->checkFiveInARow( $intersections, $coord_x, $coord_y, $color ) ) {
            return true;
        }
        // Get intersections in the same top to bottom diagonal
        $sql = "SELECT
                    id, coord_x, coord_y, stone_color
                FROM
                    intersection
                WHERE
                    ( cast(coord_x as signed ) - cast( coord_y as signed) ) = $coord_x - $coord_y
                ORDER BY
                    coord_x
              ";
        $intersections = $this->getCollectionFromDb( $sql );
        if ($this->checkFiveInARow( $intersections, $coord_x, $coord_y, $color)) return true;
        // Get intersections in the same bottom to top diagonal
        $sql = "SELECT
                    id, coord_x, coord_y, stone_color
                FROM
                    intersection
                WHERE
                    coord_x + coord_y = $coord_x + $coord_y
                ORDER BY
                    coord_x
              ";
        $intersections = $this->getCollectionFromDb( $sql );
        if ($this->checkFiveInARow( $intersections, $coord_x, $coord_y, $color)) return true;
        return false;
    }
</pre>Then add the checkFiveInARow() function into the Utility Functions area in your Game.php.
    function checkFiveInARow( $intersections, $coord_x, $coord_y, $color )
    {
        $j=0;
        foreach ($intersections as $intersection)
        {
            if ($intersection['stone_color'] == $color) {
                $j=$j+1;
            }
        }
        if ($j>=5) return true;
        return false;
    }
* Then in the gomoku->stCheckEndOfGame() function which is called when your state machine goes to the 'checkEndOfGame' state, check for this variable and for other possible 'end of game' conditions (draw).
* Then in the gomoku->stCheckEndOfGame() function which is called when your state machine goes to the 'checkEndOfGame' state, check for this variable and for other possible 'end of game' conditions (draw).


Line 541: Line 717:
     function stCheckEndOfGame()
     function stCheckEndOfGame()
     {
     {
         self::trace( "stCheckEndOfGame" );
         $this->trace( "stCheckEndOfGame" );


         $transition = "notEndedYet";
         $transition = "notEndedYet";
Line 547: Line 723:
         // If there is no more free intersections, the game ends
         // If there is no more free intersections, the game ends
         $sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection WHERE stone_color is null";
         $sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection WHERE stone_color is null";
         $free = self::getCollectionFromDb( $sql );
         $free = $this->getCollectionFromDb( $sql );


         if (count($free) == 0) {
         if (count($free) == 0) {
Line 554: Line 730:


         // If the 'end of game' flag has been set, end the game
         // If the 'end of game' flag has been set, end the game
         if (self::getGameStateValue('end_of_game') == 1) {
         if ($this->getGameStateValue('end_of_game') == 1) {
             $transition = "gameEnded";
             $transition = "gameEnded";
         }
         }
Line 562: Line 738:
</pre>
</pre>


* Catch the score notification on the client side in gomoku.js->setupNotifications(). It is advised to set up a small delay after that so that end of game popup doesn't show too quickly.
* Catch the score notification on the client side in .js->setupNotifications(). It is advised to set up a small delay after that so that end of game popup doesn't show too quickly.


<pre>
<pre>
Line 585: Line 761:


[[File:Gomoku tuto6.png]]
[[File:Gomoku tuto6.png]]
[[Category:Studio]]

Latest revision as of 09:51, 22 October 2024

This tutorial will guide you through the basics of creating a simple game on BGA Studio, through the example of Gomoku (also known as Gobang or Five in a Row).

You will start from our 'empty game' template

Here is how your games looks by default when it has just been created:

Gomoku tuto1.png

Set up the board

Gather useful images for the game and edit them as needed. Upload them in the 'img' folder of your SFTP access.

Edit gomoku.js (replace gomoku by your game name) to add the divs for your board at the beginning of the setup function:

document.getElementById('game_play_area').insertAdjacentHTML('beforeend', `
<div id="gmk_game_area">
	<div id="gmk_background">
		<div id="gmk_goban">
		</div>
	</div>	
</div>
`);

Edit .css to set the div sizes and positions and show the image of the board as background.

#gmk_game_area {
	text-align: center;
	position: relative;
}

#gmk_background {
	width: 620px;
	height: 620px;	
	position: relative;
	display: inline-block;
}

#gmk_goban {	
	background-image: url( 'img/Goban.jpg');
	width: 620px;
	height: 620px;
	position: absolute;	
}

You can find the goban board image here : https://en.doc.boardgamearena.com/File:Goban.jpg

Gomoku tuto2.png

Set up the backbone of your game

Edit dbmodel.sql to create a table for intersections. We need coordinates for each intersection and a field to store the color of the stone on this intersection (if any).

CREATE TABLE IF NOT EXISTS `intersection` (
   `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
   `coord_x` tinyint(2) unsigned NOT NULL,
   `coord_y` tinyint(2) unsigned NOT NULL,
   `stone_color` varchar(8) NULL,
   PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

Edit Game.php->setupNewGame() (in the modules/php dir) to insert the empty intersections (19x19) with coordinates into the database.

        // Insert (empty) intersections into database
        $sql = "INSERT INTO intersection (coord_x, coord_y) VALUES ";
        $values = array();
        for ($x = 0; $x < 19; $x++) {
            for ($y = 0; $y < 19; $y++) {
        	
            	$values[] = "($x, $y)";   	
            }
        }
        $sql .= implode( ',', $values );
        $this->DbQuery( $sql );

Edit Game.php->getAllDatas() to retrieve the state of the intersections from the database.

        // Intersections
        $sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection ";
        $result['intersections'] = $this->getCollectionFromDb( $sql );

Edit .js to create the function to generate intersections.

addIntersection: function(x, y, stone_color) {
            const stone_type = stone_color == null ? 'empty' : stone_color;
            document.getElementById('gmk_background').insertAdjacentHTML('beforeend', `<div class="gmk_intersection" data-color="${stone_type}" id="intersection_${x}_${y}"></div>`);
        },  

Define the styles for the intersection divs.

.gmk_intersection {
    width: 30px;
    height: 30px;
    position: relative;
}

Edit .js->setup() to setup the intersections layer that will be used to get click events and to display the stones. The data you returned in $result['intersections'] in Game.php->getAllDatas() is now available in your .js->setup() in gamedatas.intersections.

            // Setup intersections
            for( var id in gamedatas.intersections )
            {
                var intersection = gamedatas.intersections[id];

                this.addIntersection(intersection.coord_x, intersection.coord_y, intersection.stone_color);

                var x_pix = this.getXPixelCoordinates(intersection.coord_x);
                var y_pix = this.getYPixelCoordinates(intersection.coord_y);
                
                this.slideToObjectPos( $('intersection_'+intersection.coord_x+'_'+intersection.coord_y), $('gmk_background'), x_pix, y_pix, 10 ).play();

                if (intersection.stone_color != null) {
                    // This intersection is taken, it shouldn't appear as clickable anymore
                    document.getElementById('intersection_' + intersection.coord_x + '_' + intersection.coord_y).classList.remove('clickable');
                }
            } 

Use some temporary css border-color or background-color and opacity to see the divs and make sure you have them positioned right.

.gmk_intersection {
    width: 30px;
    height: 30px;
    position: relative;
    background-color: blue;
    opacity: 0.3;
}

You can declare some constants on top of your .js for easy repositioning

const INTERSECTION_WIDTH = 30;
const INTERSECTION_HEIGHT = 30;		
const INTERSECTION_X_SPACER = 3;
const INTERSECTION_Y_SPACER = 3;
const X_ORIGIN = 10;
const Y_ORIGIN = 10;
  • Then use it in your getXPixelCoordinates and getYPixelCoordinates functions
        getXPixelCoordinates: function( intersection_x )
        {
        	return X_ORIGIN + intersection_x * (INTERSECTION_WIDTH + INTERSECTION_X_SPACER); 
        },
        
        getYPixelCoordinates: function( intersection_y )
        {
        	return Y_ORIGIN + intersection_y * (INTERSECTION_HEIGHT + INTERSECTION_Y_SPACER); 
        },

Here is what you should get:

Gomoku tuto3.png

Manage states and events

Define your game states in states.inc.php. For gomoku we will use 3 states in addition of the predefined states 1 (gameSetup) and 99 (gameEnd). One to play, one to check the end game condition, one to give his turn to the other player if the game is not over.

The first state requires an action from the player, so its type is 'activeplayer'.

The two others are automatic actions for the game, so their type is 'game'.

We will update the progression while checking for the end of the game, so for this state we set the 'updateGameProgression' flag to true.

    2 => array(
        "name" => "playerTurn",
        "description" => clienttranslate('${actplayer} must play a stone'),
        "descriptionmyturn" => clienttranslate('${you} must play a stone'),
        "type" => "activeplayer",
        "possibleactions" => array( "actPlayStone" ),
        "transitions" => array( "stonePlayed" => 3, "zombiePass" => 3 )
    ),

    3 => array(
        "name" => "checkEndOfGame",
        "description" => '',
        "type" => "game",
        "action" => "stCheckEndOfGame",
        "updateGameProgression" => true,
        "transitions" => array( "gameEnded" => 99, "notEndedYet" => 4 )
    ),

    4 => array(
        "name" => "nextPlayer",
        "description" => '',
        "type" => "game",
        "action" => "stNextPlayer",
        "transitions" => array( "" => 2 )
    ),

Implement the 'stNextPlayer()' function in Game.php to manage turn rotation. Except if there are special rules for the game turn depending on context, this is really easy:

    function stNextPlayer()
    {
    	$this->trace( "stNextPlayer" );
    	 
    	// Go to next player
    	$active_player = $this->activeNextPlayer();
    	$this->giveExtraTime( $active_player );    
    	 
    	$this->gamestate->nextState();
    }

Implement the 'stCheckEndOfGame()' function in Game.php to manage the state 3. The real implementation will be exposed later, but for now just create it to be able to run the game:

    function stCheckEndOfGame()
    {
        $this->trace('stCheckEndOfGame');

        $this->gamestate->nextState('notEndedYet');
    }

Add onclick events on intersections in .js->setup()

           // Add events on active elements (the third parameter is the method that will be called when the event defined by the second parameter happens - this method must be declared beforehand)
           this.addEventToClass( "gmk_intersection", "onclick", "onClickIntersection");

Declare the corresponding .js->onClickIntersection() function, which calls an action function on the server with appropriate parameters.

        onClickIntersection: function( evt )
        {
            console.log( '$$$$ Event : onClickIntersection' );
            dojo.stopEvent( evt );

            if( ! this.checkAction( 'actPlayStone' ) )
            { return; }

            var node = evt.currentTarget.id;
            var coord_x = node.split('_')[1];
            var coord_y = node.split('_')[2];
            
            console.log( '$$$$ Selected intersection : (' + coord_x + ', ' + coord_y + ')' );
            
            if ( this.isCurrentPlayerActive() ) {
                this.bgaPerformAction('actPlayStone', { coord_x: coord_x, coord_y: coord_y });
            }
        },

Add game action in Game.php to update the database, send a notification to the client providing the event notified (‘stonePlayed’) and its parameters, and proceed to the next state.

    function actPlayStone(int $coord_x, int $coord_y)
    {        
        $player_id = $this->getActivePlayerId();
        
        // Check that this intersection is free
        $sql = "SELECT
                    id, coord_x, coord_y, stone_color
                FROM
                    intersection 
                WHERE 
                    coord_x = $coord_x 
                    AND coord_y = $coord_y
                    AND stone_color is null
               ";
        $intersection = $this->getObjectFromDb( $sql );

        if ($intersection == null) {
            throw new \BgaUserException( $this->_("There is already a stone on this intersection, you can't play there") );
        }

        // Get player color
        $sql = "SELECT
                    player_id, player_color
                FROM
                    player 
                WHERE 
                    player_id = $player_id
               ";
        $player = $this->getNonEmptyObjectFromDb( $sql );
        $color = ($player['player_color'] == 'ffffff' ? 'white' : 'black');

        // Update the intersection with a stone of the appropriate color
        $intersection_id = $intersection['id'];
        $sql = "UPDATE
                    intersection
                SET
                    stone_color = '$color'
                WHERE 
                    id = $intersection_id
               ";
        $this->DbQuery($sql);
        
        // Notify all players
        $this->notifyAllPlayers( "stonePlayed", clienttranslate( '${player_name} dropped a stone on ${coord_x},${coord_y}' ), array(
            'player_id' => $player_id,
            'player_name' => $this->getActivePlayerName(),
            'coord_x' => $coord_x,
            'coord_y' => $coord_y,
            'color' => $color
        ) );

        // Go to next game state
        $this->gamestate->nextState( "stonePlayed" );
    }

Catch the notification in .js->setupNotifications() and link it to a javascript function to execute when the notification is received.

        setupNotifications: function()
        {
            console.log( 'notifications subscriptions setup' );
        
            const notifs = [
                ['stonePlayed', 1],
            ];
    
            notifs.forEach((notif) => {
                dojo.subscribe(notif[0], this, `notif_${notif[0]}`);
                this.notifqueue.setSynchronous(notif[0], notif[1]);
            });
        }

Implement this function in javascript to update the intersection to show the stone, and register it inside the setNotifications function.

        notif_stonePlayed: function( notif )
        {
	    console.log( '**** Notification : stonePlayed' );
            console.log( notif );

            // Create a stone
            document.getElementById( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ).insertAdjacentHTML('beforeend', `<div class="gmk_stone" data-color="${notif.args.color}" id="stone_${notif.args.coord_x}_${notif.args.coord_y}"></div>`);

            // Place it on the player panel
            this.placeOnObject( $( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'player_board_' + notif.args.player_id ) );

            // Animate a slide from the player panel to the intersection
            dojo.style( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y, 'zIndex', 1 );
            var slide = this.slideToObject( $( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ), 1000 );
            dojo.connect( slide, 'onEnd', this, dojo.hitch( this, function() {
                        // At the end of the slide, update the intersection 
                        document.getElementById( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y).dataset.color = notif.args.color;
                        dojo.removeClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'clickable' );
        			
                        // We can now destroy the stone since it is now visible through the change in style of the intersection
                        dojo.destroy( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y );
       	    }));
            slide.play();
        },

For this function to work properly, you also need to define the css styles for the stones

.gmk_intersection, .gmk_stone  {
	width: 30px;
	height: 30px;
    background-image: url( 'img/stones.png');
	background-position:-60px 0px;
}

.gmk_intersection[data-color="black"], .gmk_stone[data-color="black"] {
	background-position:0px 0px;
}

.gmk_intersection[data-color="white"], .gmk_stone[data-color="white"] {
	background-position:-30px 0px
}

These styles rely on an PNG image (with transparent background) of both the white and black stones, and positions the background appropriately to show only the part of the background image matching the appropriate stone (or the transparent space if there is no stone). Here is what the image looks like:

Gomoku stones.png

The red circle is used to highlight intersections where you can drop a stone when the player's cursor hovers over them (we also change the cursor to a hand). To do this:

  • we define in the css file the 'clickable' css class
.clickable {
	cursor: pointer;
}
.clickable:hover { background-position: -90px 0px; }
  • in .js, when we enter the 'playerTurn' state, we add the 'clickable' style to the intersections where there is no stone
        onEnteringState: function( stateName, args )
        {
            console.log( 'Entering state: '+stateName );
            
            switch( stateName )
            {
            
                case 'playerTurn':
                    if( this.isCurrentPlayerActive() )
                    {
                        document.querySelectorAll('.gmk_intersection[data-color="empty"]').forEach(elem => elem.classList.add('clickable'));
                    }            
            }
        },

Finally, make sure to modify the default colors for players to white and black

         $default_colors = array( "000000", "ffffff", );

The basic game turn is implemented: you can now drop some stones!

Gomoku tuto4.png

Cleanup your styles

Remove temporary css visualisation helpers : looks good!

Gomoku tuto5.png

Add some counters in the player panels

Edit .js->setup() to setup the player panels with this extra information

            // Setting up player boards
            for( var player_id in gamedatas.players )
            {
                var player = gamedatas.players[player_id];
                         
                // Setting up players boards if needed
                document.getElementById('player_board_'+player_id).insertAdjacentHTML('beforeend', `
                <div class="cp_board">
                    <div id="stoneicon_p${player.player_id}" class="gmk_stoneicon" data-color="${player.color}"></div><span id="stonecount_p${player.player_id}">0</span>
                </div>
                `);
            }

Add some styles in your .css

.gmk_stoneicon {
	width: 14px;
	height: 14px;
	display: inline-block;
    position: relative;
    background-repeat: no-repeat;
    background-image: url( 'img/stone_icons.png');
    background-position: -28px 0px;
    margin-top: 4px;
    margin-right: 3px;
}

.gmk_stoneicon[data-color="000000"] {
	background-position:0px 0px
}

.gmk_stoneicon[data-color="ffffff"] {
	background-position:-14px 0px
}

.cp_board {
	clear: both;
}

Stone icons.png

In your Game.php, create a function to return your game counters

    /*
        getGameCounters:
        
        Gather all relevant counters about current game situation (visible by the current player).
    */
    function getGameCounters($player_id) {
    	$sql = "
    		SELECT
    			concat('stonecount_p', cast(p.player_id as char)) counter_name,
    			case when p.player_color = 'white' then 180 - count(id) else 181 - count(id) end counter_value
    		FROM (select player_id, case when player_color = 'ffffff' then 'white' else 'black' end player_color FROM player) p
    		LEFT JOIN intersection i on i.stone_color = p.player_color
    		GROUP BY p.player_color, p.player_id
    	";
    	if ($player_id != null) {
    		// Player private counters: concatenate extra SQL request with UNION using the $player_id parameter
    	}
    
    	return $this->getNonEmptyCollectionFromDB( $sql );
    }

Return your game counters in your Game.php->getAllDatas()

        // Counters
        $result['counters'] = $this->getGameCounters($current_player_id);

And pass them in any notification that needs to update them e.g. Include 'counters' in the "stonePlayed" notification code

        // Notify all players
        $this->notifyAllPlayers( "stonePlayed", clienttranslate( '${player_name} dropped a stone ${coordinates}' ), array(
            'player_id' => $player_id,
            'player_name' => $this->getActivePlayerName(),
            'coord_x' => $coord_x,
            'coord_y' => $coord_y,
            'color' => $color,
            'counters' => $this->getGameCounters($this->getCurrentPlayerId())
        ) );

Finally, in your .js->setup() function call

this.updateCounters(gamedatas.counters);

and in notif_stonePlayed() notification handler function call

this.updateCounters(notif.args.counters);

And define the updateCounters function in your js file (above setup() to avoid error)

updateCounters: function( counters )
{
    for( var counter_container_id in counters )
    {
        var counter_value = counters[counter_container_id]['counter_value'];
        document.getElementById(counters[counter_container_id]['counter_name']).innerText = counter_value;
    }
}

You now have a working counter!

Gomoku tuto7.png

Implement rules and end of game conditions

Implement specific rules for the game. For example in Gomoku, black plays first. So in Game.php->setupNewGame(), at the end of the setup make the black player active:

        // Black plays first
        $sql = "SELECT player_id, player_name FROM player WHERE player_color = '000000' ";
        $black_player = $this->getNonEmptyObjectFromDb( $sql );

        $this->gamestate->changeActivePlayer( $black_player['player_id'] );

Implement rule for computing game progression in Game.php->getGameProgression(). For Gomoku we will use the rate of occupied intersections over the total number of intersections. This will often be wildly inaccurate as the game can end pretty quickly, but it's about the best we can do (the game can drag to a stalemate with all intersections occupied and no winner).

    function getGameProgression()
    {
        // Compute and return the game progression

        // Number of stones laid down on the goban over the total number of intersections * 100
        $sql = "
	    	SELECT round(100 * count(id) / (19*19) ) as value from intersection WHERE stone_color is not null
    	";
    	$counter = $this->getNonEmptyObjectFromDB( $sql );

        return $counter['value'];
    }

Implement end of game detection and update the score according to who is the winner. It is easier to check for a win directly after setting the stone, so:

  • declare a global 'end_of_game' variable in Game.php->__construct()
       $this->initGameStateLabels( array(
                 "end_of_game" => 10,
       ) );
  • init that global variable to 0 in Game.php->setupNewGame()
       $this->setGameStateInitialValue( 'end_of_game', 0 );
  • add the appropriate code in Game.php before proceeding to the next state, using a checkForWin() function implemented separately for clarity. If the game has been won, we set the score, send a score update notification to the client side, and set the 'end_of_game' global variable to 1 as a flag signaling that the game has ended.
        // Check if end of game has been met
        if ($this->checkForWin( $coord_x, $coord_y, $color )) {

            // Set active player score to 1 (he is the winner)
            $sql = "UPDATE player SET player_score = 1 WHERE player_id = $player_id";
            $this->DbQuery($sql);

            // Notify final score
            $this->notifyAllPlayers( "finalScore",
    					clienttranslate( '${player_name} wins the game!' ),
    					array(
    							"player_name" => $this->getActivePlayerName(),
    							"player_id" => $player_id,
    							"score_delta" => 1,
    					)
   			);

            // Set global variable flag to pass on the information that the game has ended
            $this->setGameStateValue('end_of_game', 1);

            // End of game message
            $this->notifyAllPlayers( "message",
    				clienttranslate('Thanks for playing!'),
    				array(
    				)
    		);

        }

Add the checkForWin() function into the Utility Functions area in your Game.php.

    function checkForWin( $coord_x, $coord_y, $color )
    {
        // Get intersections in the same row
        $sql = "SELECT
                    id, coord_x, coord_y, stone_color
                FROM
                    intersection
                WHERE 
                    coord_y = $coord_y
                ORDER BY
                    coord_x
               ";
        $intersections = $this->getCollectionFromDb( $sql );

        if ( $this->checkFiveInARow( $intersections, $coord_x, $coord_y, $color ) ) {
            return true;
        }

        // Get intersections in the same column
        $sql = "SELECT
                    id, coord_x, coord_y, stone_color
                FROM
                    intersection
                WHERE 
                    coord_x = $coord_x
                ORDER BY
                    coord_y
               ";
        $intersections = $this->getCollectionFromDb( $sql );

        if ( $this->checkFiveInARow( $intersections, $coord_x, $coord_y, $color ) ) {
            return true;
        }

        // Get intersections in the same top to bottom diagonal
        $sql = "SELECT
                    id, coord_x, coord_y, stone_color
                FROM
                    intersection
                WHERE 
                    ( cast(coord_x as signed ) - cast( coord_y as signed) ) = $coord_x - $coord_y
                ORDER BY
                    coord_x
               ";
        $intersections = $this->getCollectionFromDb( $sql );

        if ($this->checkFiveInARow( $intersections, $coord_x, $coord_y, $color)) return true;

        // Get intersections in the same bottom to top diagonal
        $sql = "SELECT
                    id, coord_x, coord_y, stone_color
                FROM
                    intersection
                WHERE 
                    coord_x + coord_y = $coord_x + $coord_y
                ORDER BY
                    coord_x
               ";
        $intersections = $this->getCollectionFromDb( $sql );

        if ($this->checkFiveInARow( $intersections, $coord_x, $coord_y, $color)) return true;

        return false;
    }

Then add the checkFiveInARow() function into the Utility Functions area in your Game.php.

    function checkFiveInARow( $intersections, $coord_x, $coord_y, $color )
    {
        $j=0;
        foreach ($intersections as $intersection)
        {
            if ($intersection['stone_color'] == $color) {
                $j=$j+1;
            }
        }
        if ($j>=5) return true;

        return false;
    }
  • Then in the gomoku->stCheckEndOfGame() function which is called when your state machine goes to the 'checkEndOfGame' state, check for this variable and for other possible 'end of game' conditions (draw).
    function stCheckEndOfGame()
    {
        $this->trace( "stCheckEndOfGame" );

        $transition = "notEndedYet";

        // If there is no more free intersections, the game ends
        $sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection WHERE stone_color is null";
        $free = $this->getCollectionFromDb( $sql );

        if (count($free) == 0) {
            $transition = "gameEnded";
        }        

        // If the 'end of game' flag has been set, end the game
        if ($this->getGameStateValue('end_of_game') == 1) {
            $transition = "gameEnded";
        }
                
        $this->gamestate->nextState( $transition );
    }
  • Catch the score notification on the client side in .js->setupNotifications(). It is advised to set up a small delay after that so that end of game popup doesn't show too quickly.
                dojo.subscribe( 'finalScore', this, "notif_finalScore" );
	        this.notifqueue.setSynchronous( 'finalScore', 1500 );
  • Implement the function declared to handle the notification.
            notif_finalScore: function( notif )
	    {
	        console.log( '**** Notification : finalScore' );
	        console.log( notif );
	      
                // Update score
                this.scoreCtrl[ notif.args.player_id ].incValue( notif.args.score_delta );
	    },

Test everything thoroughly... you are done!

Gomoku tuto6.png