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

Tutorial gomoku

Извор: Board Game Arena
Пређи на навигацију Пређи на претрагу

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 'emtpy game' template

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

Gomoku tuto1.png

Setup the board

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.

<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/gomoku/goban.jpg');
	width: 620px;
	height: 620px;
	position: absolute;	
}

Gomoku tuto2.png

Setup 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 gomoku.game.php->setupNewGame() 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, ',' );
        self::DbQuery( $sql );

Edit gomoku.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'] = self::getCollectionFromDb( $sql );

Edit .tpl to create a template for intersections (jstpl_intersection).

var jstpl_intersection='<div class="gmk_intersection ${stone_type}" id="intersection_${x}_${y}"></div>';

Define the styles for the intersection divs

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

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.

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

                dojo.place( this.format_block('jstpl_intersection', {
                    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 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
                    dojo.removeClass( 'intersection_' + intersection.coord_x + '_' + intersection.coord_y, '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 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.

  • Declare your constants in material.inc.php (this will be automatically included in your gomoku.game.php)
$this->gameConstants = array(
		"INTERSECTION_WIDTH" => 30,
		"INTERSECTION_HEIGHT" => 30,		
		"INTERSECTION_X_SPACER" => 2.8, // Float
		"INTERSECTION_Y_SPACER" => 2.8, // Float
		"X_ORIGIN" => 0,
		"Y_ORIGIN" => 0,
);
  • 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
       getXPixelCoordinates: function( intersection_x )
       {
       	return this.gameConstants['X_ORIGIN'] + intersection_x * (this.gameConstants['INTERSECTION_WIDTH'] + this.gameConstants['INTERSECTION_X_SPACER']); 
       },
       
       getYPixelCoordinates: function( intersection_y )
       {
       	return this.gameConstants['Y_ORIGIN'] + intersection_y * (this.gameConstants['INTERSECTION_HEIGHT'] + this.gameConstants['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. 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( "playStone" ),
        "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 )
    ),

Add onclick events on intersections in gomoku.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 gomoku.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( 'playStone' ) )
           { 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.ajaxcall( "/gomoku/gomoku/playStone.html", { lock: true, coord_x: coord_x, coord_y: coord_y }, this, function( result ) {}, function( is_error ) {} );
           }
       },

Add this action function in gomoku.action.php, retrieving parameters and calling the appropriate game action

   public function playStone()
   {
       self::setAjaxMode();     
       // 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( );
   }

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.

   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
       $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 = self::getObjectFromDb( $sql );
       if ($intersection == null) {
           throw new BgaUserException( self::_("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 = self::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
              ";
       self::DbQuery($sql);
       
       // Notify all players
       self::notifyAllPlayers( "stonePlayed", clienttranslate( '${player_name} dropped a stone ${coordinates}' ), array(
           'player_id' => $player_id,
           'player_name' => self::getActivePlayerName(),
           'coordinates' => $this->getFormattedCoordinates($coord_x, $coord_y),
           'coord_x' => $coord_x,
           'coord_y' => $coord_y,
           'color' => $color
       ) );
       // Go to next game state
       $this->gamestate->nextState( "stonePlayed" );
   }

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

       setupNotifications: function()
       {
           console.log( 'notifications subscriptions setup' );
       
           dojo.subscribe( 'stonePlayed', this, "notif_stonePlayed" );
       }

Implement this method 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
           dojo.place( this.format_block('jstpl_stone', {
                   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
           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', 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.style( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y, 'zIndex', 'auto' );
      		}));
           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' );
       },

For this function, you need to declare a stone javascript template in your .tpl file

var jstpl_stone='

';

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

Implement rules and end of game conditions

Implement specific rules for the game (if any)

 Nothing special for Gomoku

Implement rule for computing game progression in .game.php->getGameProgression()

Implement end of game detection and update the score according to who is the winner in .game.php->stCheckEndOfGame()

Notify the score and implement the corresponding interface update in .js

Test everything thoroughly... you are done!

Gomoku tuto6.png