https://en.doc.boardgamearena.com/api.php?action=feedcontributions&user=Wucnuc&feedformat=atomBoard Game Arena - User contributions [en]2024-03-29T09:29:57ZUser contributionsMediaWiki 1.39.0https://en.doc.boardgamearena.com/index.php?title=Main_game_logic:_yourgamename.game.php&diff=1548Main game logic: yourgamename.game.php2015-05-17T23:52:12Z<p>Wucnuc: Removed duplication</p>
<hr />
<div><br />
This file is the main file for your game logic. Here you initialize the game, persist data, implement the rules and notify changes to the client interface.<br />
<br />
== File Structure ==<br />
<br />
The details on how the file is structured is described directly with comments on the code skeleton provided to you.<br />
<br />
Basically, here's this structure:<br />
* EmptyGame (constructor): where you define global variables.<br />
* setupNewGame: initial setup of the game.<br />
* getAllDatas: where you retrieve all game data during a complete reload of the game.<br />
* getGameProgression: where you compute the game progression indicator.<br />
* Utility functions: your utility functions.<br />
* Player actions: the entry points for players actions. <br />
* Game state arguments: methods to return additional data on specific game states ([http://en.doc.boardgamearena.com/Your_game_state_machine:_states.inc.php#args more info here]).<br />
* Game state actions: the logic to run when entering a new game state ([http://en.doc.boardgamearena.com/Your_game_state_machine:_states.inc.php#action more info here]).<br />
* zombieTurn: what to do it's the turn of a zombie player.<br />
<br />
== Accessing player informations ==<br />
<br />
; getPlayersNumber()<br />
: Returns the number of players playing at the table<br />
: Note: doesn't work in setupNewGame so use count($players) instead<br />
<br />
; getActivePlayerId()<br />
: Get the "active_player", whatever what is the current state type.<br />
: Note: it does NOT mean that this player is active right now, because state type could be "game" or "multiplayer"<br />
: Note: avoid using this method in a "multiplayer" state because it does not mean anything.<br />
<br />
; getActivePlayerName()<br />
: Get the "active_player" name<br />
: Note: avoid using this method in a "multiplayer" state because it does not mean anything.<br />
<br />
; loadPlayersBasicInfos()<br />
: Get an associative array with generic data about players (ie: not game specific data).<br />
: The key of the associative array is the player id.<br />
: The content of each value is:<br />
: * player_name<br />
: * player_color (ex: ff0000)<br />
<br />
; getCurrentPlayerId()<br />
: Get the "current_player". The current player is the one from which the action originated (the one who send the request).<br />
: '''Be careful''': It is not always the active player.<br />
: In general, you shouldn't use this method, unless you are in "multiplayer" state.<br />
<br />
; getCurrentPlayerName()<br />
: Get the "current_player" name<br />
: Be careful using this method (see above).<br />
<br />
; getCurrentPlayerColor()<br />
: Get the "current_player" color<br />
: Be careful using this method (see above).<br />
<br />
; isCurrentPlayerZombie()<br />
: Check the "current_player" zombie status. If true, player leave the game.<br />
<br />
== Accessing database ==<br />
<br />
The main game logic should be the only point from where you should access to the game database. You access your database using SQL queries with the methods below.<br />
<br />
'''IMPORTANT'''<br />
<br />
BGA is using [http://dev.mysql.com/doc/refman/5.0/en/sql-syntax-transactions.html database transactions]. It means that your database changes WON'T BE APPLIED to the database until your request ends normally. Using transaction is in fact very useful for you: at any time, if your game logic detects that something is wrong (ex: unallowed move), you just have to throw an exception and all the changes already performed on the game situation will be removed.<br />
<br />
; DbQuery( $sql )<br />
: This is the generic method to access the database.<br />
: It can execute any type of SELECT/UPDATE/DELETE/REPLACE query on the database.<br />
: You should use it for UPDATE/DELETE/REPLACE query. For SELECT queries, the specialized methods below are much better.<br />
<br />
; getUniqueValueFromDB( $sql )<br />
: Returns a unique value from DB or null if no value is found.<br />
: $sql must be a SELECT query.<br />
: Raise an exception if more than 1 row is returned.<br />
<br />
; getCollectionFromDB( $sql, $bSingleValue=false )<br />
: Returns an associative array of rows for a sql SELECT query.<br />
: The key of the resulting associative array is the first field specified in the SELECT query.<br />
: The value of the resulting associative array if an associative array with all the field specified in the SELECT query and associated values.<br />
: First column must be a primary or alternate key.<br />
: The resulting collection can be empty.<br />
: If you specified $bSingleValue=true and if your SQL query request 2 fields A and B, the method returns an associative array "A=>B"<br />
<br />
Example 1:<br />
<pre><br />
self::getCollectionFromDB( "SELECT player_id id, player_name name, player_score score FROM player" );<br />
<br />
Result:<br />
array(<br />
1234 => array( 'id'=>1234, 'name'=>'myuser0', 'score'=>1 ),<br />
1235 => array( 'id'=>1235, 'name'=>'myuser1', 'score'=>0 )<br />
)<br />
</pre><br />
Example 2:<br />
<pre><br />
self::getCollectionFromDB( "SELECT player_id id, player_name name FROM player", true );<br />
<br />
Result:<br />
array(<br />
1234 => 'myuser0',<br />
1235 => 'myuser1'<br />
)<br />
<br />
</pre><br />
<br />
; getNonEmptyCollectionFromDB( $sql )<br />
: Idem than previous one, but raise an exception if the collection is empty<br />
<br />
; function getObjectFromDB( $sql )<br />
: Returns one row for the sql SELECT query as an associative array or null if there is no result<br />
: Raise an exception if the query return more than one row<br />
<br />
Example:<br />
<pre><br />
self::getObjectFromDB( "SELECT player_id id, player_name name, player_score score FROM player WHERE player_id='$player_id'" );<br />
<br />
Result:<br />
array(<br />
'id'=>1234, 'name'=>'myuser0', 'score'=>1 <br />
)<br />
</pre><br />
<br />
; getNonEmptyObjectFromDB( $sql )<br />
: Idem than previous one, but raise an exception if no row is found<br />
<br />
; getObjectListFromDB( $sql, $bUniqueValue=false )<br />
: Return an array of rows for a sql SELECT query.<br />
: the result if the same than "getCollectionFromDB" except that the result is a simple array (and not an associative array).<br />
: The result can be empty.<br />
: If you specified $bUniqueValue=true and if your SQL query request 1 field, the method returns directly an array of values.<br />
<br />
Example 1:<br />
<pre><br />
self::getObjectListFromDB( "SELECT player_id id, player_name name, player_score score FROM player" );<br />
<br />
Result:<br />
array(<br />
array( 'id'=>1234, 'name'=>'myuser0', 'score'=>1 ),<br />
array( 'id'=>1235, 'name'=>'myuser1', 'score'=>0 )<br />
)<br />
</pre><br />
Example 2:<br />
<pre><br />
self::getObjectListFromDB( "SELECT player_id id, player_name name FROM player", true );<br />
<br />
Result:<br />
array(<br />
'myuser0',<br />
'myuser1'<br />
)<br />
<br />
</pre><br />
<br />
; getDoubleKeyCollectionFromDB( $sql, $bSingleValue=false )<br />
: Return an associative array of associative array, from a SQL SELECT query.<br />
: First array level correspond to first column specified in SQL query.<br />
: Second array level correspond to second column specified in SQL query.<br />
: If bSingleValue = true, keep only third column on result<br />
<br />
<br />
; DbGetLastId()<br />
: Return the PRIMARY key of the last inserted row (see PHP mysql_insert_id function).<br />
<br />
; DbAffectedRow()<br />
: Return the number of row affected by the last operation<br />
<br />
; escapeStringForDB( $string )<br />
: You must use this function on every string type data in your database that contains unsafe data.<br />
: (unsafe = can be modified by a player).<br />
: This method makes sure that no SQL injection will be done through the string used.<br />
<br />
<pre><br />
self::getObjectFromDB( "SELECT player_id id, player_name name, player_color color FROM player WHERE player_id='1234'" );<br />
<br />
Result:<br />
array(<br />
'id' => 1234,<br />
'name' => 'myuser1',<br />
'color' => 'ff0000'<br />
)<br />
<br />
</pre><br />
<br />
Note: see Editing [[Game database model: dbmodel.sql]] to know how to define your database model.<br />
<br />
== Use globals ==<br />
<br />
Sometimes, you have to keep a single integer value that is global to your game, and you don't want to create a DB table specifically for it.<br />
<br />
Using a BGA framework "global", you can do such a thing. Your value will be stored in the "global" table in database, and you can access it with simple methods.<br />
<br />
'''initGameStateLabels'''<br />
<br />
This method is located at the beginning of your game logic. This is the place you defines the globals used in your game logic, by assigning them IDs.<br />
<br />
You can define up to 89 globals, with IDs from 10 to 89. You must NOT use globals outside this range as globals are used by other components of the framework.<br />
<br />
<pre><br />
self::initGameStateLabels( array( <br />
"my_first_global_variable" => 10,<br />
"my_second_global_variable" => 11<br />
) );<br />
</pre><br />
<br />
'''setGameStateInitialValue( $value_label, $value_value )'''<br />
<br />
Init your global value. Must be called before any use of your global, so you should call this method from your "setupNewGame" method.<br />
<br />
'''getGameStateValue( $value_label )'''<br />
<br />
Retrieve the current value of a global.<br />
<br />
'''setGameStateValue( $value_label, $value_value )'''<br />
<br />
Set the current value of a global.<br />
<br />
'''incGameStateValue( $value_label, $increment )'''<br />
<br />
Increment the current value of a global. If increment is negative, decrement the value of the global.<br />
<br />
Return the final value of the global.<br />
<br />
== Game states and active players ==<br />
<br />
; checkAction( $actionName, $bThrowException=true )<br />
: Check if action is valid regarding current game state (exception if fails)<br />
: The action is valid if it is listed as a "possibleactions" in the current game state (see game state description).<br />
: This method MUST be called in the first place in ALL your PHP methods that handle players action, in order to make sure a player can't do an action when the rules disallow it at this moment of the game.<br />
: if "bThrowException" is set to "false", the function return false in case of failure instead of throwing and exception. This is useful when several actions are possible in order to test each of them without throwing exceptions.<br />
<br />
; activeNextPlayer()<br />
: Make the next player active in the natural player order.<br />
: Note: you CANT use this method in a "activeplayer" or "multipleactiveplayer" state. You must use a "game" type game state for this.<br />
<br />
; activePrevPlayer()<br />
: Make the previous player active (in the natural player order).<br />
: Note: you CANT use this method in a "activeplayer" or "multipleactiveplayer" state. You must use a "game" type game state for this.<br />
<br />
; $this->gamestate->changeActivePlayer( $player_id )<br />
: You can call this method to make any player active.<br />
: Note: you CANT use this method in a "activeplayer" or "multipleactiveplayer" state. You must use a "game" type game state for this.<br />
<br />
<br />
; $this->gamestate->getActivePlayerList()<br />
: With this method you can retrieve the list of the active player at any time.<br />
: During a "game" type gamestate, it will return a void array.<br />
: During a "activeplayer" type gamestate, it will return an array with one value (the active player id).<br />
: during a "multipleactiveplayer" type gamestate, it will return an array of the active players id.<br />
: Note: you should only use this method is the latter case.<br />
<br />
; $this->gamestate->setAllPlayersMultiactive()<br />
: With this method, all playing players are made active.<br />
: Usually, you use this method at the beginning (ex: "st" action method) of a multiplayer game state when all players have to do some action.<br />
<br />
; $this->gamestate->setPlayersMultiactive( $players, $next_state )<br />
: Make a specific list of players active during a multiactive gamestate.<br />
: Bare in mind it doesn't deactivate other previously active players.<br />
: "players" is the array of player id that should be made active.<br />
: In case "players" is empty, the method trigger the "next_state" transition to go to the next game state.<br />
<br />
; $this->gamestate->setPlayerNonMultiactive( $player_id, $next_state )<br />
: During a multiactive game state, make the specified player inactive.<br />
: Usually, you call this method during a multiactive game state after a player did his action.<br />
: If this player was the last active player, the method trigger the "next_state" transition to go to the next game state.<br />
<br />
; $this->gamestate->checkPossibleAction( $action )<br />
: (rarely used)<br />
: This works exactly like "checkAction", except that it do NOT check if current player is active.<br />
: This is used specifically in certain game states when you want to authorize some additional actions for players that are not active at the moment.<br />
: Example: in Libertalia game, you want to authorize players to change their mind about card played. They are of course not active at the time they change their mind, so you cannot use "checkAction" and use "checkPossibleAction" instead.<br />
<br />
== Players turn order ==<br />
<br />
'''getNextPlayerTable()'''<br />
<br />
Return an associative array which associate each player with the next player around the table.<br />
<br />
In addition, key 0 is associated to the first player to play.<br />
<br />
Example: if three player with ID 1, 2 and 3 are around the table, in this order, the method returns:<br />
<pre><br />
array( <br />
1 => 2, <br />
2 => 3, <br />
3 => 1, <br />
0 => 1 <br />
);<br />
</pre><br />
<br />
'''getPrevPlayerTable()'''<br />
<br />
Same as above, but the associative array associate the previous player around the table.<br />
<br />
'''getPlayerAfter( $player_id )'''<br />
<br />
Get player playing after given player in natural playing order.<br />
<br />
'''getPlayerBefore( $player_id )'''<br />
<br />
Get player playing before given player in natural playing order.<br />
<br />
== Notify players ==<br />
<br />
To understand notifications, please read [http://www.slideshare.net/boardgamearena/the-bga-framework-at-a-glance The BGA Framework at a glance] first.<br />
<br />
'''IMPORTANT'''<br />
<br />
Notifications are sent at the very end of the request, when it ends normally. It means that if you throw an exception for any reason (ex: move not allowed), no notifications will be sent to players.<br />
Notifications sent between the game start (setupNewGame) and the end of the "action" method of the first active state will never reach their destination.<br />
<br />
'''notifyAllPlayers( $notification_type, $notification_log, $notification_args )'''<br />
<br />
Send a notification to all players of the game.<br />
<br />
* notification_type:<br />
A string that defines the type of your notification.<br />
<br />
Your game interface Javascript logic will use this to know what is the type of the received notification (and to trigger the corresponding method).<br />
<br />
* notification_log:<br />
A string that defines what is to be displayed in the game log.<br />
<br />
You can use an empty string here (""). In this case, nothing is displayed in the game log.<br />
<br />
If you define a real string here, you should use "clienttranslate" method to make sure it can be translate.<br />
<br />
You can use arguments in your notification_log strings, that refers to values defines in the "notification_args" argument (see below). NB: Make sure you only use single quotes ('), otherwise PHP will try to interpolate the variable and will ignore the values in the args array.<br />
<br />
Note: you CAN use some HTML inside your notification log, and it is working. However:<br />
_ pay attention to keep the log clear.<br />
_ try to not include some HTML tags inside the "clienttranslate" method, otherwise it will make the translators work more difficult. You can use a notification argument instead, and provide your HTML through this argument.<br />
<br />
* notification_args:<br />
The arguments of your notifications, as an associative array.<br />
<br />
This array will be transmitted to the game interface logic, in order the game interface can be updated.<br />
<br />
Complete notifyAllPlayers example (from "Reversi"):<br />
<br />
<pre><br />
self::notifyAllPlayers( "playDisc", clienttranslate( '${player_name} plays a disc and turns over ${returned_nbr} disc(s)' ), array(<br />
'player_id' => $player_id,<br />
'player_name' => self::getActivePlayerName(),<br />
'returned_nbr' => count( $turnedOverDiscs ),<br />
'x' => $x,<br />
'y' => $y<br />
) );<br />
</pre><br />
<br />
You can see in the example above the use of the "clienttranslate" method, and the use of 2 arguments "player_name" and "returned_nbr" in the notification log.<br />
<br />
Important: NO private date must be sent with this method, as a cheater could see it even it is not used explicitly by the game interface logic. If you want to send private information to a player, please use notifyPlayer below.<br />
<br />
'''notifyPlayer( $player_id, $notification_type, $notification_log, $notification_args )'''<br />
<br />
Same as above, except that the notification is sent to one player only.<br />
<br />
This method must be used each time some private information must be transmitted to a player.<br />
<br />
== Game statistics ==<br />
<br />
There are 2 types of statistics:<br />
* a "player" statistic is a statistic associated to a player<br />
* a "table" statistics is a statistic not associated to a player (global statistic for this game).<br />
<br />
See [[Game statistics: stats.inc.php]] to see how you defines statistics for your game.<br />
<br />
'''initStat( $table_or_player, $name, $value, $player_id=null )'''<br />
Create a statistic entry for the specified statistics with a default value.<br />
This method must be called for each statistics of your game, in your setupNewGame method.<br />
<br />
'table_or_player' must be set to "table" if this is a table statistics, or "player" if this is a player statistics.<br />
<br />
'name' is the name of your statistics, as it has been defined in your stats.inc.php file.<br />
<br />
'value' is the initial value of the statistics. If this is a player statistics and if the player is not specified by "player_id" argument, the value is set for ALL players.<br />
<br />
<br />
'''function setStat( $value, $name, $player_id = null )'''<br />
<br />
Set a statistic value.<br />
<br />
If "player_id" is not specified, setStat consider it is a TABLE statistic.<br />
<br />
If "player_id" is specified, setStat consider it is a PLAYER statistic.<br />
<br />
'''incStat( $delta, $name, $player_id = null )'''<br />
<br />
Increment (or decrement) specified statistic value. Same behavior as above.<br />
<br />
== Translations ==<br />
<br />
See [[Translations]]<br />
<br />
== Manage player scores and Tie breaker ==<br />
<br />
At the end of the game, players automatically get a rank depending on their score: the player with the biggest score is #1, the player with the second biggest score is #2, and so on...<br />
<br />
During the game, you update player's score directly by updating "player_score" field of "player" table in database.<br />
<br />
Examples:<br />
<pre><br />
<br />
// +2 points to active player<br />
self::DbQuery( "UPDATE player SET player_score=player_score+2 WHERE player_id='".self::getActivePlayerId()."'" );<br />
<br />
// Set score of active player to 5<br />
self::DbQuery( "UPDATE player SET player_score=5 WHERE player_id='".self::getActivePlayerId()."'" );<br />
<br />
</pre><br />
<br />
Note: don't forget to notify the client side in order the score control can be updated accordingly.<br />
<br />
'''Tie breaker'''<br />
<br />
Tie breaker is used when two players get the same score at the end of a game.<br />
<br />
Tie breaker is using "player_score_aux" field of "player" table. It is updated exactly like the "player_score" field.<br />
<br />
Tie breaker score is displayed only for players who are tied at the end of the game. Most of the time, it is not supposed to be displayed explicitly during the game.<br />
<br />
When you are using "player_score_aux" functionality, you must describe the formula to use in your gameinfos.inc.php file like this:<br />
<br />
<pre><br />
'tie_breaker_description' => totranslate("Describe here your tie breaker formula"),<br />
</pre><br />
<br />
This description will be used as a tooltip to explain to players how this auxiliary score has been calculated.<br />
<br />
== Reflexion time ==<br />
<br />
; function giveExtraTime( $player_id, $specific_time=null )<br />
: Give standard extra time to this player.<br />
: Standard extra time depends on the speed of the game (small with "slow" game option, bigger with other options).<br />
: You can also specify an exact time to add, in seconds, with the "specified_time" argument (rarely used).<br />
<br />
== Managing errors and exceptions ==<br />
<br />
Note: when you throw an exception, all database changes and all notifications are cancelled immediately. This way, the game situation that were existing before the request is completely restored.<br />
<br />
; throw new BgaUserException ( $error_message)<br />
: Base class to notify a user error<br />
: You must throw this exception when a player want to do something that he is not allowed to do.<br />
: The error message will be shown to the player as a "red message", so it must be translated.<br />
: Throwing such an exception is NOT considered as a bug, so it is not traced in BGA error logs.<br />
<br />
Example from Gomoku:<br />
<pre><br />
throw new BgaUserException( self::_("There is already a stone on this intersection, you can't play there") );<br />
</pre><br />
<br />
<br />
; throw new BgaVisibleSystemException ( $error_message)<br />
: You must throw this exception when you detect something that is not supposed to happened into your code.<br />
: The error message is shown to the user as an "Unexpected error", in order he can report it in the forum.<br />
: The error message is logged in BGA error logs. If it happens regularly, we will report it to you.<br />
<br />
; throw new BgaSystemException ( $error_message)<br />
: Base class to notify a system exception. The message will be hidden from the user, but show in the logs. Use this if the message contains technical information.<br />
: You shouldn't use this type of exception except if you think the information shown could be critical. Indeed: a generic error message will be shown to the user, so it's going to be difficult for you to see what happened.<br />
<br />
== Zombie mode ==<br />
<br />
When a player leaves a game for any reason (expelled, quit), he becomes a "zombie player". In this case, the results of the game won't count for statistics, but this is cool if the other players can finish the game anyway. That's why zombie mode exists: allow the other player to finish the game, even if the situation is not ideal.<br />
<br />
While developing your zombie mode, keep in mind that:<br />
* Do not refer to the rules, because this situation is not planned by the rules.<br />
* Try to figure that you are playing with your friends and one of them has to leave: how can we finish the game without killing the spirit of the game?<br />
* The idea is NOT to develop an artificial intelligence for the game.<br />
<br />
Most of the time, the best thing to do when it is zombie player turn is to jump immediately to a state where he is not active anymore. For example, if he is in a game state where he has a choice between playing A and playing B, the best thing to do is NOT to choose A or B, but to pass. So, even if there's no "pass" action in the rules, add a "zombiepass" transitition in your game state and use it.<br />
<br />
Each time a zombie player must play, your "zombieTurn" method is called.<br />
<br />
Parameters:<br />
* $state: the name of the current game state.<br />
* $active_player: the id of the active player.<br />
<br />
Most of the time, your zombieTurn method looks like this:<br />
<br />
<pre><br />
function zombieTurn( $state, $active_player )<br />
{<br />
$statename = $state['name'];<br />
<br />
if( $statename == 'myFirstGameState'<br />
|| $statename == 'my2ndGameState'<br />
|| $statename == 'my3rdGameState'<br />
....<br />
)<br />
{<br />
$this->gamestate->nextState( "zombiePass" );<br />
}<br />
else<br />
throw new BgaVisibleSystemException( "Zombie mode not supported at this game state: ".$statename );<br />
}<br />
</pre><br />
<br />
Note that in the example above, all corresponding game state should implement "zombiePass" as a transition.<br />
<br />
== Player elimination ==<br />
<br />
In some games, this is useful to eliminate a player from the game in order he/she can start another game without waiting for the current game end.<br />
<br />
This case should be rare. Please don't use player elimination feature if some player just has to wait the last 10% of the game for game end. This feature should be used only in games where players are eliminated all along the game (typical examples: "Perudo" or "The Werewolves of Miller's Hollow").<br />
<br />
Usage:<br />
<br />
* Player to eliminate should NOT be active anymore (preferably use the feature in a "game" type game state).<br />
* In your PHP code:<br />
self::eliminatePlayer( <player_to_eliminate_id> );<br />
* the player is informed in a dialog box that he no longer have to played and can start another game if he/she wants too (whith buttons "stay at this table" "quit table and back to main site"). In any case, the player is free to tart&join another table from now.<br />
* When your game is over, all players who have been eliminated before receive a "notification" (the small "!" icon on the top right of the BGA interface) that indicate them that "the game has ended" and invite them to review the game results.</div>Wucnuchttps://en.doc.boardgamearena.com/index.php?title=Tutorial_gomoku&diff=1545Tutorial gomoku2015-05-11T02:53:33Z<p>Wucnuc: Moved modifying default player colors to this section</p>
<hr />
<div>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).<br />
<br />
== You will start from our 'emtpy game' template ==<br />
<br />
Here is how your games looks by default when it has just been created:<br />
<br />
[[File:Gomoku tuto1.png]]<br />
<br />
== Setup the board ==<br />
<br />
Gather useful images for the game and edit them as needed. Upload them in the 'img' folder of your SFTP access.<br />
<br />
Edit .tpl to add some divs for the board in the HTML. For example:<br />
<br />
<pre><br />
<div id="gmk_game_area"><br />
<div id="gmk_background"><br />
<div id="gmk_goban"><br />
</div><br />
</div> <br />
</div><br />
</pre><br />
<br />
Edit .css to set the div sizes and positions and show the image of the board as background.<br />
<br />
<pre><br />
#gmk_game_area {<br />
text-align: center;<br />
position: relative;<br />
}<br />
<br />
#gmk_background {<br />
width: 620px;<br />
height: 620px; <br />
position: relative;<br />
display: inline-block;<br />
}<br />
<br />
#gmk_goban { <br />
background-image: url( 'img/goban.jpg');<br />
width: 620px;<br />
height: 620px;<br />
position: absolute; <br />
}<br />
</pre><br />
<br />
[[File:Gomoku tuto2.png]]<br />
<br />
== Setup the backbone of your game ==<br />
<br />
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).<br />
<br />
<pre><br />
CREATE TABLE IF NOT EXISTS `intersection` (<br />
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,<br />
`coord_x` tinyint(2) unsigned NOT NULL,<br />
`coord_y` tinyint(2) unsigned NOT NULL,<br />
`stone_color` varchar(8) NULL,<br />
PRIMARY KEY (`id`)<br />
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;<br />
</pre><br />
<br />
Edit .game.php->setupNewGame() to insert the empty intersections (19x19) with coordinates into the database.<br />
<br />
<pre><br />
// Insert (empty) intersections into database<br />
$sql = "INSERT INTO intersection (coord_x, coord_y) VALUES ";<br />
$values = array();<br />
for ($x = 0; $x < 19; $x++) {<br />
for ($y = 0; $y < 19; $y++) {<br />
<br />
$values[] = "($x, $y)"; <br />
}<br />
}<br />
$sql .= implode( $values, ',' );<br />
self::DbQuery( $sql );<br />
</pre><br />
<br />
Edit .game.php->getAllDatas() to retrieve the state of the intersections from the database.<br />
<br />
<pre><br />
// Intersections<br />
$sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection ";<br />
$result['intersections'] = self::getCollectionFromDb( $sql );<br />
</pre><br />
<br />
Edit .tpl to create a template for intersections.<br />
<br />
<pre><br />
var jstpl_intersection='<div class="gmk_intersection ${stone_type}" id="intersection_${x}_${y}"></div>';<br />
</pre><br />
<br />
Define the styles for the intersection divs.<br />
<br />
<pre><br />
.gmk_intersection {<br />
width: 30px;<br />
height: 30px;<br />
position: relative;<br />
}<br />
</pre><br />
<br />
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.<br />
<br />
<pre><br />
// Setup intersections<br />
for( var id in gamedatas.intersections )<br />
{<br />
var intersection = gamedatas.intersections[id];<br />
<br />
dojo.place( this.format_block('jstpl_intersection', {<br />
x:intersection.coord_x,<br />
y:intersection.coord_y,<br />
stone_type:(intersection.stone_color == null ? "no_stone" : 'stone_' + intersection.stone_color)<br />
} ), $ ( 'gmk_background' ) );<br />
<br />
var x_pix = this.getXPixelCoordinates(intersection.coord_x);<br />
var y_pix = this.getYPixelCoordinates(intersection.coord_y);<br />
<br />
this.slideToObjectPos( $('intersection_'+intersection.coord_x+'_'+intersection.coord_y), $('gmk_background'), x_pix, y_pix, 10 ).play();<br />
<br />
if (intersection.stone_color != null) {<br />
// This intersection is taken, it shouldn't appear as clickable anymore<br />
dojo.removeClass( 'intersection_' + intersection.coord_x + '_' + intersection.coord_y, 'clickable' );<br />
}<br />
} <br />
</pre><br />
<br />
Use some temporary css border-color or background-color and opacity to see the divs and make sure you have them positioned right. <br />
<br />
<pre><br />
.gmk_intersection {<br />
width: 30px;<br />
height: 30px;<br />
position: relative;<br />
background-color: blue;<br />
opacity: 0.3;<br />
}<br />
</pre><br />
<br />
You can declare some constants in material.inc.php and pass them to your .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.<br />
<br />
* Declare your constants in material.inc.php (this will be automatically included in your .game.php)<br />
<br />
<pre><br />
$this->gameConstants = array(<br />
"INTERSECTION_WIDTH" => 30,<br />
"INTERSECTION_HEIGHT" => 30, <br />
"INTERSECTION_X_SPACER" => 2.8, // Float<br />
"INTERSECTION_Y_SPACER" => 2.8, // Float<br />
"X_ORIGIN" => 0,<br />
"Y_ORIGIN" => 0,<br />
);<br />
</pre><br />
<br />
* In .game.php->getAllDatas(), add the constants to the result array<br />
<br />
// Constants<br />
$result['constants'] = $this->gameConstants;<br />
<br />
* In .js constructor, define a class variable for constants<br />
<br />
// Game constants<br />
this.gameConstants = null;<br />
<br />
* In .js->setup() assign the constants to this variable<br />
<br />
this.gameConstants = gamedatas.constants;<br />
<br />
* Then use it in your getXPixelCoordinates and getYPixelCoordinates functions<br />
<br />
getXPixelCoordinates: function( intersection_x )<br />
{<br />
return this.gameConstants['X_ORIGIN'] + intersection_x * (this.gameConstants['INTERSECTION_WIDTH'] + this.gameConstants['INTERSECTION_X_SPACER']); <br />
},<br />
<br />
getYPixelCoordinates: function( intersection_y )<br />
{<br />
return this.gameConstants['Y_ORIGIN'] + intersection_y * (this.gameConstants['INTERSECTION_HEIGHT'] + this.gameConstants['INTERSECTION_Y_SPACER']); <br />
},<br />
<br />
Here is what you should get:<br />
<br />
[[File:Gomoku tuto3.png]]<br />
<br />
== Manage states and events ==<br />
<br />
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.<br />
<br />
The first state requires an action from the player, so its type is 'activeplayer'.<br />
<br />
The two others are automatic actions for the game, so their type is 'game'.<br />
<br />
We will update the progression while checking for the end of the game, so for this state we set the 'updateGameProgression' flag to true.<br />
<br />
<pre><br />
2 => array(<br />
"name" => "playerTurn",<br />
"description" => clienttranslate('${actplayer} must play a stone'),<br />
"descriptionmyturn" => clienttranslate('${you} must play a stone'),<br />
"type" => "activeplayer",<br />
"possibleactions" => array( "playStone" ),<br />
"transitions" => array( "stonePlayed" => 3, "zombiePass" => 3 )<br />
),<br />
<br />
3 => array(<br />
"name" => "checkEndOfGame",<br />
"description" => '',<br />
"type" => "game",<br />
"action" => "stCheckEndOfGame",<br />
"updateGameProgression" => true,<br />
"transitions" => array( "gameEnded" => 99, "notEndedYet" => 4 )<br />
),<br />
<br />
4 => array(<br />
"name" => "nextPlayer",<br />
"description" => '',<br />
"type" => "game",<br />
"action" => "stNextPlayer",<br />
"transitions" => array( "" => 2 )<br />
),<br />
</pre><br />
<br />
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:<br />
<br />
<pre><br />
function stNextPlayer()<br />
{<br />
self::trace( "stNextPlayer" );<br />
<br />
// Go to next player<br />
$active_player = self::activeNextPlayer();<br />
self::giveExtraTime( $active_player ); <br />
<br />
$this->gamestate->nextState();<br />
}<br />
</pre><br />
<br />
Add onclick events on intersections in .js->setup()<br />
<br />
// 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)<br />
this.addEventToClass( "gmk_intersection", "onclick", "onClickIntersection");<br />
<br />
Declare the corresponding .js->onClickIntersection() function, which calls an action function on the server with appropriate parameters<br />
<br />
<pre><br />
onClickIntersection: function( evt )<br />
{<br />
console.log( '$$$$ Event : onClickIntersection' );<br />
dojo.stopEvent( evt );<br />
<br />
if( ! this.checkAction( 'playStone' ) )<br />
{ return; }<br />
<br />
var node = evt.currentTarget.id;<br />
var coord_x = node.split('_')[1];<br />
var coord_y = node.split('_')[2];<br />
<br />
console.log( '$$$$ Selected intersection : (' + coord_x + ', ' + coord_y + ')' );<br />
<br />
if ( this.isCurrentPlayerActive() ) {<br />
this.ajaxcall( "/gomoku/gomoku/playStone.html", { lock: true, coord_x: coord_x, coord_y: coord_y }, this, function( result ) {}, function( is_error ) {} );<br />
}<br />
},<br />
</pre><br />
<br />
Add this action function in .action.php, retrieving parameters and calling the appropriate game action<br />
<br />
<pre><br />
public function playStone()<br />
{<br />
self::setAjaxMode(); <br />
<br />
// Retrieve arguments<br />
// Note: these arguments correspond to what has been sent through the javascript "ajaxcall" method<br />
$coord_x = self::getArg( "coord_x", AT_posint, true );<br />
$coord_y = self::getArg( "coord_y", AT_posint, true );<br />
<br />
// Then, call the appropriate method in your game logic, like "playCard" or "myAction"<br />
$this->game->playStone( $coord_x, $coord_y );<br />
<br />
self::ajaxResponse( );<br />
}<br />
</pre><br />
<br />
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.<br />
<br />
<pre><br />
function playStone( $coord_x, $coord_y )<br />
{<br />
// Check that this is player's turn and that it is a "possible action" at this game state (see states.inc.php)<br />
self::checkAction( 'playStone' ); <br />
<br />
$player_id = self::getActivePlayerId();<br />
<br />
// Check that this intersection is free<br />
$sql = "SELECT<br />
id, coord_x, coord_y, stone_color<br />
FROM<br />
intersection <br />
WHERE <br />
coord_x = $coord_x <br />
AND coord_y = $coord_y<br />
AND stone_color is null<br />
";<br />
$intersection = self::getObjectFromDb( $sql );<br />
<br />
if ($intersection == null) {<br />
throw new BgaUserException( self::_("There is already a stone on this intersection, you can't play there") );<br />
}<br />
<br />
// Get player color<br />
$sql = "SELECT<br />
player_id, player_color<br />
FROM<br />
player <br />
WHERE <br />
player_id = $player_id<br />
";<br />
$player = self::getNonEmptyObjectFromDb( $sql );<br />
$color = ($player['player_color'] == 'ffffff' ? 'white' : 'black');<br />
<br />
// Update the intersection with a stone of the appropriate color<br />
$intersection_id = $intersection['id'];<br />
$sql = "UPDATE<br />
intersection<br />
SET<br />
stone_color = '$color'<br />
WHERE <br />
id = $intersection_id<br />
";<br />
self::DbQuery($sql);<br />
<br />
// Notify all players<br />
self::notifyAllPlayers( "stonePlayed", clienttranslate( '${player_name} dropped a stone on ${coord_x},${coord_y}' ), array(<br />
'player_id' => $player_id,<br />
'player_name' => self::getActivePlayerName(),<br />
'coord_x' => $coord_x,<br />
'coord_y' => $coord_y,<br />
'color' => $color<br />
) );<br />
<br />
// Go to next game state<br />
$this->gamestate->nextState( "stonePlayed" );<br />
}<br />
</pre><br />
<br />
Catch the notification in .js->setupNotifications() and link it to a javascript function to execute when the notification is received.<br />
<br />
<pre><br />
setupNotifications: function()<br />
{<br />
console.log( 'notifications subscriptions setup' );<br />
<br />
dojo.subscribe( 'stonePlayed', this, "notif_stonePlayed" );<br />
}<br />
</pre><br />
<br />
Implement this function in javascript to update the intersection to show the stone, and register it inside the setNotifications function.<br />
<br />
<pre><br />
notif_stonePlayed: function( notif )<br />
{<br />
console.log( '**** Notification : stonePlayed' );<br />
console.log( notif );<br />
<br />
// Create a stone<br />
dojo.place( this.format_block('jstpl_stone', {<br />
stone_type:'stone_' + notif.args.color,<br />
x:notif.args.coord_x,<br />
y:notif.args.coord_y<br />
} ), $( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ) );<br />
<br />
// Place it on the player panel<br />
this.placeOnObject( $( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'player_board_' + notif.args.player_id ) );<br />
<br />
// Animate a slide from the player panel to the intersection<br />
dojo.style( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y, 'zIndex', 1 );<br />
var slide = this.slideToObject( $( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ), 1000 );<br />
dojo.connect( slide, 'onEnd', this, dojo.hitch( this, function() {<br />
// At the end of the slide, update the intersection <br />
dojo.removeClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'no_stone' );<br />
dojo.addClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'stone_' + notif.args.color );<br />
dojo.removeClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'clickable' );<br />
<br />
// We can now destroy the stone since it is now visible through the change in style of the intersection<br />
dojo.destroy( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y );<br />
}));<br />
slide.play();<br />
},<br />
</pre><br />
<br />
For this function to work properly, you also need: <br />
<br />
* to declare a stone javascript template in your .tpl file.<br />
<br />
<pre><br />
var jstpl_stone='<div class="gmk_stone ${stone_type}" id="stone_${x}_${y}"></div>';<br />
</pre><br />
<br />
* to define the css styles for the stones<br />
<br />
<pre><br />
.gmk_intersection {<br />
width: 30px;<br />
height: 30px;<br />
position: relative;<br />
background-image: url( 'img/stones.png' );<br />
}<br />
<br />
.gmk_stone {<br />
width: 30px;<br />
height: 30px;<br />
position: absolute;<br />
background-image: url( 'img/stones.png' );<br />
}<br />
<br />
.no_stone { background-position: -60px 0px; }<br />
<br />
.stone_black { background-position: 0px 0px; }<br />
.stone_white { background-position: -30px 0px; }<br />
</pre><br />
<br />
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:<br />
<br />
[[File:Gomoku stones.png]]<br />
<br />
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:<br />
<br />
* we define in the css file the 'clickable' css class<br />
<br />
<pre><br />
.clickable {<br />
cursor: pointer;<br />
}<br />
.clickable:hover { background-position: -90px 0px; }<br />
</pre><br />
<br />
* in .js, when we enter the 'playerTurn' state, we add the 'clickable' style to the intersections where there is no stone<br />
<br />
<pre><br />
onEnteringState: function( stateName, args )<br />
{<br />
console.log( 'Entering state: '+stateName );<br />
<br />
switch( stateName )<br />
{<br />
<br />
case 'playerTurn':<br />
if( this.isCurrentPlayerActive() )<br />
{<br />
var queueEntries = dojo.query( '.no_stone' );<br />
for(var i=0; i<queueEntries.length; i++) { <br />
dojo.addClass( queueEntries[i], 'clickable' );<br />
}<br />
} <br />
}<br />
},<br />
</pre><br />
<br />
Finally, make sure to modify the default colors for players to white and black<br />
<br />
$default_colors = array( "000000", "ffffff", );<br />
<br />
The basic game turn is implemented: you can now drop some stones!<br />
<br />
[[File:Gomoku tuto4.png]]<br />
<br />
== Cleanup your styles ==<br />
<br />
Remove temporary css visualisation helpers : looks good!<br />
<br />
[[File:Gomoku tuto5.png]]<br />
<br />
== Implement rules and end of game conditions ==<br />
<br />
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:<br />
<br />
<pre><br />
// Black plays first<br />
$sql = "SELECT player_id, player_name FROM player WHERE player_color = '000000' ";<br />
$black_player = self::getNonEmptyObjectFromDb( $sql );<br />
<br />
$this->gamestate->changeActivePlayer( $black_player['player_id'] );<br />
</pre><br />
<br />
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).<br />
<br />
<pre><br />
function getGameProgression()<br />
{<br />
// Compute and return the game progression<br />
<br />
// Number of stones laid down on the goban over the total number of intersections * 100<br />
$sql = "<br />
SELECT round(100 * count(id) / (19*19) ) as value from intersection WHERE stone_color is not null<br />
";<br />
$counter = self::getNonEmptyObjectFromDB( $sql );<br />
<br />
return $counter['value'];<br />
}<br />
</pre><br />
<br />
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: <br />
<br />
* declare a global 'end_of_game' variable in .game.php->Gomoku()<br />
<br />
self::initGameStateLabels( array(<br />
"end_of_game" => 10,<br />
) );<br />
<br />
* init that global variable to 0 in .game.php->setupNewGame()<br />
<br />
self::setGameStateInitialValue( 'end_of_game', 0 );<br />
<br />
* 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.<br />
<br />
<pre><br />
// Check if end of game has been met<br />
if ($this->checkForWin( $coord_x, $coord_y, $color )) {<br />
<br />
// Set active player score to 1 (he is the winner)<br />
$sql = "UPDATE player SET player_score = 1 WHERE player_id = $player_id";<br />
self::DbQuery($sql);<br />
<br />
// Notify final score<br />
$this->notifyAllPlayers( "finalScore",<br />
clienttranslate( '${player_name} wins the game!' ),<br />
array(<br />
"player_name" => self::getActivePlayerName(),<br />
"player_id" => $player_id,<br />
"score_delta" => 1,<br />
)<br />
);<br />
<br />
// Set global variable flag to pass on the information that the game has ended<br />
self::setGameStateValue('end_of_game', 1);<br />
<br />
// End of game message<br />
$this->notifyAllPlayers( "message",<br />
clienttranslate('Thanks for playing!'),<br />
array(<br />
)<br />
);<br />
<br />
}<br />
</pre><br />
<br />
* 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).<br />
<br />
<pre><br />
function stCheckEndOfGame()<br />
{<br />
self::trace( "stCheckEndOfGame" );<br />
<br />
$transition = "notEndedYet";<br />
<br />
// If there is no more free intersections, the game ends<br />
$sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection WHERE stone_color is null";<br />
$free = self::getCollectionFromDb( $sql );<br />
<br />
if (count($free) == 0) {<br />
$transition = "gameEnded";<br />
} <br />
<br />
// If the 'end of game' flag has been set, end the game<br />
if (self::getGameStateValue('end_of_game') == 1) {<br />
$transition = "gameEnded";<br />
}<br />
<br />
$this->gamestate->nextState( $transition );<br />
}<br />
</pre><br />
<br />
* 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.<br />
<br />
<pre><br />
dojo.subscribe( 'finalScore', this, "notif_finalScore" );<br />
this.notifqueue.setSynchronous( 'finalScore', 1500 );<br />
</pre><br />
<br />
* Implement the function declared to handle the notification.<br />
<br />
<pre><br />
notif_finalScore: function( notif )<br />
{<br />
console.log( '**** Notification : finalScore' );<br />
console.log( notif );<br />
<br />
// Update score<br />
this.scoreCtrl[ notif.args.player_id ].incValue( notif.args.score_delta );<br />
},<br />
</pre><br />
<br />
'''Test everything thoroughly... you are done!'''<br />
<br />
[[File:Gomoku tuto6.png]]</div>Wucnuchttps://en.doc.boardgamearena.com/index.php?title=Tutorial_gomoku&diff=1544Tutorial gomoku2015-05-11T02:53:22Z<p>Wucnuc: Moved modifying default player colors to the previous section</p>
<hr />
<div>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).<br />
<br />
== You will start from our 'emtpy game' template ==<br />
<br />
Here is how your games looks by default when it has just been created:<br />
<br />
[[File:Gomoku tuto1.png]]<br />
<br />
== Setup the board ==<br />
<br />
Gather useful images for the game and edit them as needed. Upload them in the 'img' folder of your SFTP access.<br />
<br />
Edit .tpl to add some divs for the board in the HTML. For example:<br />
<br />
<pre><br />
<div id="gmk_game_area"><br />
<div id="gmk_background"><br />
<div id="gmk_goban"><br />
</div><br />
</div> <br />
</div><br />
</pre><br />
<br />
Edit .css to set the div sizes and positions and show the image of the board as background.<br />
<br />
<pre><br />
#gmk_game_area {<br />
text-align: center;<br />
position: relative;<br />
}<br />
<br />
#gmk_background {<br />
width: 620px;<br />
height: 620px; <br />
position: relative;<br />
display: inline-block;<br />
}<br />
<br />
#gmk_goban { <br />
background-image: url( 'img/goban.jpg');<br />
width: 620px;<br />
height: 620px;<br />
position: absolute; <br />
}<br />
</pre><br />
<br />
[[File:Gomoku tuto2.png]]<br />
<br />
== Setup the backbone of your game ==<br />
<br />
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).<br />
<br />
<pre><br />
CREATE TABLE IF NOT EXISTS `intersection` (<br />
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,<br />
`coord_x` tinyint(2) unsigned NOT NULL,<br />
`coord_y` tinyint(2) unsigned NOT NULL,<br />
`stone_color` varchar(8) NULL,<br />
PRIMARY KEY (`id`)<br />
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;<br />
</pre><br />
<br />
Edit .game.php->setupNewGame() to insert the empty intersections (19x19) with coordinates into the database.<br />
<br />
<pre><br />
// Insert (empty) intersections into database<br />
$sql = "INSERT INTO intersection (coord_x, coord_y) VALUES ";<br />
$values = array();<br />
for ($x = 0; $x < 19; $x++) {<br />
for ($y = 0; $y < 19; $y++) {<br />
<br />
$values[] = "($x, $y)"; <br />
}<br />
}<br />
$sql .= implode( $values, ',' );<br />
self::DbQuery( $sql );<br />
</pre><br />
<br />
Edit .game.php->getAllDatas() to retrieve the state of the intersections from the database.<br />
<br />
<pre><br />
// Intersections<br />
$sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection ";<br />
$result['intersections'] = self::getCollectionFromDb( $sql );<br />
</pre><br />
<br />
Edit .tpl to create a template for intersections.<br />
<br />
<pre><br />
var jstpl_intersection='<div class="gmk_intersection ${stone_type}" id="intersection_${x}_${y}"></div>';<br />
</pre><br />
<br />
Define the styles for the intersection divs.<br />
<br />
<pre><br />
.gmk_intersection {<br />
width: 30px;<br />
height: 30px;<br />
position: relative;<br />
}<br />
</pre><br />
<br />
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.<br />
<br />
<pre><br />
// Setup intersections<br />
for( var id in gamedatas.intersections )<br />
{<br />
var intersection = gamedatas.intersections[id];<br />
<br />
dojo.place( this.format_block('jstpl_intersection', {<br />
x:intersection.coord_x,<br />
y:intersection.coord_y,<br />
stone_type:(intersection.stone_color == null ? "no_stone" : 'stone_' + intersection.stone_color)<br />
} ), $ ( 'gmk_background' ) );<br />
<br />
var x_pix = this.getXPixelCoordinates(intersection.coord_x);<br />
var y_pix = this.getYPixelCoordinates(intersection.coord_y);<br />
<br />
this.slideToObjectPos( $('intersection_'+intersection.coord_x+'_'+intersection.coord_y), $('gmk_background'), x_pix, y_pix, 10 ).play();<br />
<br />
if (intersection.stone_color != null) {<br />
// This intersection is taken, it shouldn't appear as clickable anymore<br />
dojo.removeClass( 'intersection_' + intersection.coord_x + '_' + intersection.coord_y, 'clickable' );<br />
}<br />
} <br />
</pre><br />
<br />
Use some temporary css border-color or background-color and opacity to see the divs and make sure you have them positioned right. <br />
<br />
<pre><br />
.gmk_intersection {<br />
width: 30px;<br />
height: 30px;<br />
position: relative;<br />
background-color: blue;<br />
opacity: 0.3;<br />
}<br />
</pre><br />
<br />
You can declare some constants in material.inc.php and pass them to your .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.<br />
<br />
* Declare your constants in material.inc.php (this will be automatically included in your .game.php)<br />
<br />
<pre><br />
$this->gameConstants = array(<br />
"INTERSECTION_WIDTH" => 30,<br />
"INTERSECTION_HEIGHT" => 30, <br />
"INTERSECTION_X_SPACER" => 2.8, // Float<br />
"INTERSECTION_Y_SPACER" => 2.8, // Float<br />
"X_ORIGIN" => 0,<br />
"Y_ORIGIN" => 0,<br />
);<br />
</pre><br />
<br />
* In .game.php->getAllDatas(), add the constants to the result array<br />
<br />
// Constants<br />
$result['constants'] = $this->gameConstants;<br />
<br />
* In .js constructor, define a class variable for constants<br />
<br />
// Game constants<br />
this.gameConstants = null;<br />
<br />
* In .js->setup() assign the constants to this variable<br />
<br />
this.gameConstants = gamedatas.constants;<br />
<br />
* Then use it in your getXPixelCoordinates and getYPixelCoordinates functions<br />
<br />
getXPixelCoordinates: function( intersection_x )<br />
{<br />
return this.gameConstants['X_ORIGIN'] + intersection_x * (this.gameConstants['INTERSECTION_WIDTH'] + this.gameConstants['INTERSECTION_X_SPACER']); <br />
},<br />
<br />
getYPixelCoordinates: function( intersection_y )<br />
{<br />
return this.gameConstants['Y_ORIGIN'] + intersection_y * (this.gameConstants['INTERSECTION_HEIGHT'] + this.gameConstants['INTERSECTION_Y_SPACER']); <br />
},<br />
<br />
Here is what you should get:<br />
<br />
[[File:Gomoku tuto3.png]]<br />
<br />
== Manage states and events ==<br />
<br />
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.<br />
<br />
The first state requires an action from the player, so its type is 'activeplayer'.<br />
<br />
The two others are automatic actions for the game, so their type is 'game'.<br />
<br />
We will update the progression while checking for the end of the game, so for this state we set the 'updateGameProgression' flag to true.<br />
<br />
<pre><br />
2 => array(<br />
"name" => "playerTurn",<br />
"description" => clienttranslate('${actplayer} must play a stone'),<br />
"descriptionmyturn" => clienttranslate('${you} must play a stone'),<br />
"type" => "activeplayer",<br />
"possibleactions" => array( "playStone" ),<br />
"transitions" => array( "stonePlayed" => 3, "zombiePass" => 3 )<br />
),<br />
<br />
3 => array(<br />
"name" => "checkEndOfGame",<br />
"description" => '',<br />
"type" => "game",<br />
"action" => "stCheckEndOfGame",<br />
"updateGameProgression" => true,<br />
"transitions" => array( "gameEnded" => 99, "notEndedYet" => 4 )<br />
),<br />
<br />
4 => array(<br />
"name" => "nextPlayer",<br />
"description" => '',<br />
"type" => "game",<br />
"action" => "stNextPlayer",<br />
"transitions" => array( "" => 2 )<br />
),<br />
</pre><br />
<br />
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:<br />
<br />
<pre><br />
function stNextPlayer()<br />
{<br />
self::trace( "stNextPlayer" );<br />
<br />
// Go to next player<br />
$active_player = self::activeNextPlayer();<br />
self::giveExtraTime( $active_player ); <br />
<br />
$this->gamestate->nextState();<br />
}<br />
</pre><br />
<br />
Add onclick events on intersections in .js->setup()<br />
<br />
// 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)<br />
this.addEventToClass( "gmk_intersection", "onclick", "onClickIntersection");<br />
<br />
Declare the corresponding .js->onClickIntersection() function, which calls an action function on the server with appropriate parameters<br />
<br />
<pre><br />
onClickIntersection: function( evt )<br />
{<br />
console.log( '$$$$ Event : onClickIntersection' );<br />
dojo.stopEvent( evt );<br />
<br />
if( ! this.checkAction( 'playStone' ) )<br />
{ return; }<br />
<br />
var node = evt.currentTarget.id;<br />
var coord_x = node.split('_')[1];<br />
var coord_y = node.split('_')[2];<br />
<br />
console.log( '$$$$ Selected intersection : (' + coord_x + ', ' + coord_y + ')' );<br />
<br />
if ( this.isCurrentPlayerActive() ) {<br />
this.ajaxcall( "/gomoku/gomoku/playStone.html", { lock: true, coord_x: coord_x, coord_y: coord_y }, this, function( result ) {}, function( is_error ) {} );<br />
}<br />
},<br />
</pre><br />
<br />
Add this action function in .action.php, retrieving parameters and calling the appropriate game action<br />
<br />
<pre><br />
public function playStone()<br />
{<br />
self::setAjaxMode(); <br />
<br />
// Retrieve arguments<br />
// Note: these arguments correspond to what has been sent through the javascript "ajaxcall" method<br />
$coord_x = self::getArg( "coord_x", AT_posint, true );<br />
$coord_y = self::getArg( "coord_y", AT_posint, true );<br />
<br />
// Then, call the appropriate method in your game logic, like "playCard" or "myAction"<br />
$this->game->playStone( $coord_x, $coord_y );<br />
<br />
self::ajaxResponse( );<br />
}<br />
</pre><br />
<br />
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.<br />
<br />
<pre><br />
function playStone( $coord_x, $coord_y )<br />
{<br />
// Check that this is player's turn and that it is a "possible action" at this game state (see states.inc.php)<br />
self::checkAction( 'playStone' ); <br />
<br />
$player_id = self::getActivePlayerId();<br />
<br />
// Check that this intersection is free<br />
$sql = "SELECT<br />
id, coord_x, coord_y, stone_color<br />
FROM<br />
intersection <br />
WHERE <br />
coord_x = $coord_x <br />
AND coord_y = $coord_y<br />
AND stone_color is null<br />
";<br />
$intersection = self::getObjectFromDb( $sql );<br />
<br />
if ($intersection == null) {<br />
throw new BgaUserException( self::_("There is already a stone on this intersection, you can't play there") );<br />
}<br />
<br />
// Get player color<br />
$sql = "SELECT<br />
player_id, player_color<br />
FROM<br />
player <br />
WHERE <br />
player_id = $player_id<br />
";<br />
$player = self::getNonEmptyObjectFromDb( $sql );<br />
$color = ($player['player_color'] == 'ffffff' ? 'white' : 'black');<br />
<br />
// Update the intersection with a stone of the appropriate color<br />
$intersection_id = $intersection['id'];<br />
$sql = "UPDATE<br />
intersection<br />
SET<br />
stone_color = '$color'<br />
WHERE <br />
id = $intersection_id<br />
";<br />
self::DbQuery($sql);<br />
<br />
// Notify all players<br />
self::notifyAllPlayers( "stonePlayed", clienttranslate( '${player_name} dropped a stone on ${coord_x},${coord_y}' ), array(<br />
'player_id' => $player_id,<br />
'player_name' => self::getActivePlayerName(),<br />
'coord_x' => $coord_x,<br />
'coord_y' => $coord_y,<br />
'color' => $color<br />
) );<br />
<br />
// Go to next game state<br />
$this->gamestate->nextState( "stonePlayed" );<br />
}<br />
</pre><br />
<br />
Catch the notification in .js->setupNotifications() and link it to a javascript function to execute when the notification is received.<br />
<br />
<pre><br />
setupNotifications: function()<br />
{<br />
console.log( 'notifications subscriptions setup' );<br />
<br />
dojo.subscribe( 'stonePlayed', this, "notif_stonePlayed" );<br />
}<br />
</pre><br />
<br />
Implement this function in javascript to update the intersection to show the stone, and register it inside the setNotifications function.<br />
<br />
<pre><br />
notif_stonePlayed: function( notif )<br />
{<br />
console.log( '**** Notification : stonePlayed' );<br />
console.log( notif );<br />
<br />
// Create a stone<br />
dojo.place( this.format_block('jstpl_stone', {<br />
stone_type:'stone_' + notif.args.color,<br />
x:notif.args.coord_x,<br />
y:notif.args.coord_y<br />
} ), $( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ) );<br />
<br />
// Place it on the player panel<br />
this.placeOnObject( $( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'player_board_' + notif.args.player_id ) );<br />
<br />
// Animate a slide from the player panel to the intersection<br />
dojo.style( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y, 'zIndex', 1 );<br />
var slide = this.slideToObject( $( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ), 1000 );<br />
dojo.connect( slide, 'onEnd', this, dojo.hitch( this, function() {<br />
// At the end of the slide, update the intersection <br />
dojo.removeClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'no_stone' );<br />
dojo.addClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'stone_' + notif.args.color );<br />
dojo.removeClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'clickable' );<br />
<br />
// We can now destroy the stone since it is now visible through the change in style of the intersection<br />
dojo.destroy( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y );<br />
}));<br />
slide.play();<br />
},<br />
</pre><br />
<br />
For this function to work properly, you also need: <br />
<br />
* to declare a stone javascript template in your .tpl file.<br />
<br />
<pre><br />
var jstpl_stone='<div class="gmk_stone ${stone_type}" id="stone_${x}_${y}"></div>';<br />
</pre><br />
<br />
* to define the css styles for the stones<br />
<br />
<pre><br />
.gmk_intersection {<br />
width: 30px;<br />
height: 30px;<br />
position: relative;<br />
background-image: url( 'img/stones.png' );<br />
}<br />
<br />
.gmk_stone {<br />
width: 30px;<br />
height: 30px;<br />
position: absolute;<br />
background-image: url( 'img/stones.png' );<br />
}<br />
<br />
.no_stone { background-position: -60px 0px; }<br />
<br />
.stone_black { background-position: 0px 0px; }<br />
.stone_white { background-position: -30px 0px; }<br />
</pre><br />
<br />
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:<br />
<br />
[[File:Gomoku stones.png]]<br />
<br />
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:<br />
<br />
* we define in the css file the 'clickable' css class<br />
<br />
<pre><br />
.clickable {<br />
cursor: pointer;<br />
}<br />
.clickable:hover { background-position: -90px 0px; }<br />
</pre><br />
<br />
* in .js, when we enter the 'playerTurn' state, we add the 'clickable' style to the intersections where there is no stone<br />
<br />
<pre><br />
onEnteringState: function( stateName, args )<br />
{<br />
console.log( 'Entering state: '+stateName );<br />
<br />
switch( stateName )<br />
{<br />
<br />
case 'playerTurn':<br />
if( this.isCurrentPlayerActive() )<br />
{<br />
var queueEntries = dojo.query( '.no_stone' );<br />
for(var i=0; i<queueEntries.length; i++) { <br />
dojo.addClass( queueEntries[i], 'clickable' );<br />
}<br />
} <br />
}<br />
},<br />
</pre><br />
<br />
The basic game turn is implemented: you can now drop some stones!<br />
<br />
[[File:Gomoku tuto4.png]]<br />
<br />
== Cleanup your styles ==<br />
<br />
Remove temporary css visualisation helpers : looks good!<br />
<br />
[[File:Gomoku tuto5.png]]<br />
<br />
== Implement rules and end of game conditions ==<br />
<br />
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:<br />
<br />
<pre><br />
// Black plays first<br />
$sql = "SELECT player_id, player_name FROM player WHERE player_color = '000000' ";<br />
$black_player = self::getNonEmptyObjectFromDb( $sql );<br />
<br />
$this->gamestate->changeActivePlayer( $black_player['player_id'] );<br />
</pre><br />
<br />
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).<br />
<br />
<pre><br />
function getGameProgression()<br />
{<br />
// Compute and return the game progression<br />
<br />
// Number of stones laid down on the goban over the total number of intersections * 100<br />
$sql = "<br />
SELECT round(100 * count(id) / (19*19) ) as value from intersection WHERE stone_color is not null<br />
";<br />
$counter = self::getNonEmptyObjectFromDB( $sql );<br />
<br />
return $counter['value'];<br />
}<br />
</pre><br />
<br />
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: <br />
<br />
* declare a global 'end_of_game' variable in .game.php->Gomoku()<br />
<br />
self::initGameStateLabels( array(<br />
"end_of_game" => 10,<br />
) );<br />
<br />
* init that global variable to 0 in .game.php->setupNewGame()<br />
<br />
self::setGameStateInitialValue( 'end_of_game', 0 );<br />
<br />
* 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.<br />
<br />
<pre><br />
// Check if end of game has been met<br />
if ($this->checkForWin( $coord_x, $coord_y, $color )) {<br />
<br />
// Set active player score to 1 (he is the winner)<br />
$sql = "UPDATE player SET player_score = 1 WHERE player_id = $player_id";<br />
self::DbQuery($sql);<br />
<br />
// Notify final score<br />
$this->notifyAllPlayers( "finalScore",<br />
clienttranslate( '${player_name} wins the game!' ),<br />
array(<br />
"player_name" => self::getActivePlayerName(),<br />
"player_id" => $player_id,<br />
"score_delta" => 1,<br />
)<br />
);<br />
<br />
// Set global variable flag to pass on the information that the game has ended<br />
self::setGameStateValue('end_of_game', 1);<br />
<br />
// End of game message<br />
$this->notifyAllPlayers( "message",<br />
clienttranslate('Thanks for playing!'),<br />
array(<br />
)<br />
);<br />
<br />
}<br />
</pre><br />
<br />
* 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).<br />
<br />
<pre><br />
function stCheckEndOfGame()<br />
{<br />
self::trace( "stCheckEndOfGame" );<br />
<br />
$transition = "notEndedYet";<br />
<br />
// If there is no more free intersections, the game ends<br />
$sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection WHERE stone_color is null";<br />
$free = self::getCollectionFromDb( $sql );<br />
<br />
if (count($free) == 0) {<br />
$transition = "gameEnded";<br />
} <br />
<br />
// If the 'end of game' flag has been set, end the game<br />
if (self::getGameStateValue('end_of_game') == 1) {<br />
$transition = "gameEnded";<br />
}<br />
<br />
$this->gamestate->nextState( $transition );<br />
}<br />
</pre><br />
<br />
* 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.<br />
<br />
<pre><br />
dojo.subscribe( 'finalScore', this, "notif_finalScore" );<br />
this.notifqueue.setSynchronous( 'finalScore', 1500 );<br />
</pre><br />
<br />
* Implement the function declared to handle the notification.<br />
<br />
<pre><br />
notif_finalScore: function( notif )<br />
{<br />
console.log( '**** Notification : finalScore' );<br />
console.log( notif );<br />
<br />
// Update score<br />
this.scoreCtrl[ notif.args.player_id ].incValue( notif.args.score_delta );<br />
},<br />
</pre><br />
<br />
'''Test everything thoroughly... you are done!'''<br />
<br />
[[File:Gomoku tuto6.png]]</div>Wucnuchttps://en.doc.boardgamearena.com/index.php?title=Tutorial_gomoku&diff=1543Tutorial gomoku2015-05-11T02:41:43Z<p>Wucnuc: .gmk_intersection also requires the stones background image</p>
<hr />
<div>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).<br />
<br />
== You will start from our 'emtpy game' template ==<br />
<br />
Here is how your games looks by default when it has just been created:<br />
<br />
[[File:Gomoku tuto1.png]]<br />
<br />
== Setup the board ==<br />
<br />
Gather useful images for the game and edit them as needed. Upload them in the 'img' folder of your SFTP access.<br />
<br />
Edit .tpl to add some divs for the board in the HTML. For example:<br />
<br />
<pre><br />
<div id="gmk_game_area"><br />
<div id="gmk_background"><br />
<div id="gmk_goban"><br />
</div><br />
</div> <br />
</div><br />
</pre><br />
<br />
Edit .css to set the div sizes and positions and show the image of the board as background.<br />
<br />
<pre><br />
#gmk_game_area {<br />
text-align: center;<br />
position: relative;<br />
}<br />
<br />
#gmk_background {<br />
width: 620px;<br />
height: 620px; <br />
position: relative;<br />
display: inline-block;<br />
}<br />
<br />
#gmk_goban { <br />
background-image: url( 'img/goban.jpg');<br />
width: 620px;<br />
height: 620px;<br />
position: absolute; <br />
}<br />
</pre><br />
<br />
[[File:Gomoku tuto2.png]]<br />
<br />
== Setup the backbone of your game ==<br />
<br />
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).<br />
<br />
<pre><br />
CREATE TABLE IF NOT EXISTS `intersection` (<br />
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,<br />
`coord_x` tinyint(2) unsigned NOT NULL,<br />
`coord_y` tinyint(2) unsigned NOT NULL,<br />
`stone_color` varchar(8) NULL,<br />
PRIMARY KEY (`id`)<br />
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;<br />
</pre><br />
<br />
Edit .game.php->setupNewGame() to insert the empty intersections (19x19) with coordinates into the database.<br />
<br />
<pre><br />
// Insert (empty) intersections into database<br />
$sql = "INSERT INTO intersection (coord_x, coord_y) VALUES ";<br />
$values = array();<br />
for ($x = 0; $x < 19; $x++) {<br />
for ($y = 0; $y < 19; $y++) {<br />
<br />
$values[] = "($x, $y)"; <br />
}<br />
}<br />
$sql .= implode( $values, ',' );<br />
self::DbQuery( $sql );<br />
</pre><br />
<br />
Edit .game.php->getAllDatas() to retrieve the state of the intersections from the database.<br />
<br />
<pre><br />
// Intersections<br />
$sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection ";<br />
$result['intersections'] = self::getCollectionFromDb( $sql );<br />
</pre><br />
<br />
Edit .tpl to create a template for intersections.<br />
<br />
<pre><br />
var jstpl_intersection='<div class="gmk_intersection ${stone_type}" id="intersection_${x}_${y}"></div>';<br />
</pre><br />
<br />
Define the styles for the intersection divs.<br />
<br />
<pre><br />
.gmk_intersection {<br />
width: 30px;<br />
height: 30px;<br />
position: relative;<br />
}<br />
</pre><br />
<br />
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.<br />
<br />
<pre><br />
// Setup intersections<br />
for( var id in gamedatas.intersections )<br />
{<br />
var intersection = gamedatas.intersections[id];<br />
<br />
dojo.place( this.format_block('jstpl_intersection', {<br />
x:intersection.coord_x,<br />
y:intersection.coord_y,<br />
stone_type:(intersection.stone_color == null ? "no_stone" : 'stone_' + intersection.stone_color)<br />
} ), $ ( 'gmk_background' ) );<br />
<br />
var x_pix = this.getXPixelCoordinates(intersection.coord_x);<br />
var y_pix = this.getYPixelCoordinates(intersection.coord_y);<br />
<br />
this.slideToObjectPos( $('intersection_'+intersection.coord_x+'_'+intersection.coord_y), $('gmk_background'), x_pix, y_pix, 10 ).play();<br />
<br />
if (intersection.stone_color != null) {<br />
// This intersection is taken, it shouldn't appear as clickable anymore<br />
dojo.removeClass( 'intersection_' + intersection.coord_x + '_' + intersection.coord_y, 'clickable' );<br />
}<br />
} <br />
</pre><br />
<br />
Use some temporary css border-color or background-color and opacity to see the divs and make sure you have them positioned right. <br />
<br />
<pre><br />
.gmk_intersection {<br />
width: 30px;<br />
height: 30px;<br />
position: relative;<br />
background-color: blue;<br />
opacity: 0.3;<br />
}<br />
</pre><br />
<br />
You can declare some constants in material.inc.php and pass them to your .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.<br />
<br />
* Declare your constants in material.inc.php (this will be automatically included in your .game.php)<br />
<br />
<pre><br />
$this->gameConstants = array(<br />
"INTERSECTION_WIDTH" => 30,<br />
"INTERSECTION_HEIGHT" => 30, <br />
"INTERSECTION_X_SPACER" => 2.8, // Float<br />
"INTERSECTION_Y_SPACER" => 2.8, // Float<br />
"X_ORIGIN" => 0,<br />
"Y_ORIGIN" => 0,<br />
);<br />
</pre><br />
<br />
* In .game.php->getAllDatas(), add the constants to the result array<br />
<br />
// Constants<br />
$result['constants'] = $this->gameConstants;<br />
<br />
* In .js constructor, define a class variable for constants<br />
<br />
// Game constants<br />
this.gameConstants = null;<br />
<br />
* In .js->setup() assign the constants to this variable<br />
<br />
this.gameConstants = gamedatas.constants;<br />
<br />
* Then use it in your getXPixelCoordinates and getYPixelCoordinates functions<br />
<br />
getXPixelCoordinates: function( intersection_x )<br />
{<br />
return this.gameConstants['X_ORIGIN'] + intersection_x * (this.gameConstants['INTERSECTION_WIDTH'] + this.gameConstants['INTERSECTION_X_SPACER']); <br />
},<br />
<br />
getYPixelCoordinates: function( intersection_y )<br />
{<br />
return this.gameConstants['Y_ORIGIN'] + intersection_y * (this.gameConstants['INTERSECTION_HEIGHT'] + this.gameConstants['INTERSECTION_Y_SPACER']); <br />
},<br />
<br />
Here is what you should get:<br />
<br />
[[File:Gomoku tuto3.png]]<br />
<br />
== Manage states and events ==<br />
<br />
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.<br />
<br />
The first state requires an action from the player, so its type is 'activeplayer'.<br />
<br />
The two others are automatic actions for the game, so their type is 'game'.<br />
<br />
We will update the progression while checking for the end of the game, so for this state we set the 'updateGameProgression' flag to true.<br />
<br />
<pre><br />
2 => array(<br />
"name" => "playerTurn",<br />
"description" => clienttranslate('${actplayer} must play a stone'),<br />
"descriptionmyturn" => clienttranslate('${you} must play a stone'),<br />
"type" => "activeplayer",<br />
"possibleactions" => array( "playStone" ),<br />
"transitions" => array( "stonePlayed" => 3, "zombiePass" => 3 )<br />
),<br />
<br />
3 => array(<br />
"name" => "checkEndOfGame",<br />
"description" => '',<br />
"type" => "game",<br />
"action" => "stCheckEndOfGame",<br />
"updateGameProgression" => true,<br />
"transitions" => array( "gameEnded" => 99, "notEndedYet" => 4 )<br />
),<br />
<br />
4 => array(<br />
"name" => "nextPlayer",<br />
"description" => '',<br />
"type" => "game",<br />
"action" => "stNextPlayer",<br />
"transitions" => array( "" => 2 )<br />
),<br />
</pre><br />
<br />
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:<br />
<br />
<pre><br />
function stNextPlayer()<br />
{<br />
self::trace( "stNextPlayer" );<br />
<br />
// Go to next player<br />
$active_player = self::activeNextPlayer();<br />
self::giveExtraTime( $active_player ); <br />
<br />
$this->gamestate->nextState();<br />
}<br />
</pre><br />
<br />
Add onclick events on intersections in .js->setup()<br />
<br />
// 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)<br />
this.addEventToClass( "gmk_intersection", "onclick", "onClickIntersection");<br />
<br />
Declare the corresponding .js->onClickIntersection() function, which calls an action function on the server with appropriate parameters<br />
<br />
<pre><br />
onClickIntersection: function( evt )<br />
{<br />
console.log( '$$$$ Event : onClickIntersection' );<br />
dojo.stopEvent( evt );<br />
<br />
if( ! this.checkAction( 'playStone' ) )<br />
{ return; }<br />
<br />
var node = evt.currentTarget.id;<br />
var coord_x = node.split('_')[1];<br />
var coord_y = node.split('_')[2];<br />
<br />
console.log( '$$$$ Selected intersection : (' + coord_x + ', ' + coord_y + ')' );<br />
<br />
if ( this.isCurrentPlayerActive() ) {<br />
this.ajaxcall( "/gomoku/gomoku/playStone.html", { lock: true, coord_x: coord_x, coord_y: coord_y }, this, function( result ) {}, function( is_error ) {} );<br />
}<br />
},<br />
</pre><br />
<br />
Add this action function in .action.php, retrieving parameters and calling the appropriate game action<br />
<br />
<pre><br />
public function playStone()<br />
{<br />
self::setAjaxMode(); <br />
<br />
// Retrieve arguments<br />
// Note: these arguments correspond to what has been sent through the javascript "ajaxcall" method<br />
$coord_x = self::getArg( "coord_x", AT_posint, true );<br />
$coord_y = self::getArg( "coord_y", AT_posint, true );<br />
<br />
// Then, call the appropriate method in your game logic, like "playCard" or "myAction"<br />
$this->game->playStone( $coord_x, $coord_y );<br />
<br />
self::ajaxResponse( );<br />
}<br />
</pre><br />
<br />
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.<br />
<br />
<pre><br />
function playStone( $coord_x, $coord_y )<br />
{<br />
// Check that this is player's turn and that it is a "possible action" at this game state (see states.inc.php)<br />
self::checkAction( 'playStone' ); <br />
<br />
$player_id = self::getActivePlayerId();<br />
<br />
// Check that this intersection is free<br />
$sql = "SELECT<br />
id, coord_x, coord_y, stone_color<br />
FROM<br />
intersection <br />
WHERE <br />
coord_x = $coord_x <br />
AND coord_y = $coord_y<br />
AND stone_color is null<br />
";<br />
$intersection = self::getObjectFromDb( $sql );<br />
<br />
if ($intersection == null) {<br />
throw new BgaUserException( self::_("There is already a stone on this intersection, you can't play there") );<br />
}<br />
<br />
// Get player color<br />
$sql = "SELECT<br />
player_id, player_color<br />
FROM<br />
player <br />
WHERE <br />
player_id = $player_id<br />
";<br />
$player = self::getNonEmptyObjectFromDb( $sql );<br />
$color = ($player['player_color'] == 'ffffff' ? 'white' : 'black');<br />
<br />
// Update the intersection with a stone of the appropriate color<br />
$intersection_id = $intersection['id'];<br />
$sql = "UPDATE<br />
intersection<br />
SET<br />
stone_color = '$color'<br />
WHERE <br />
id = $intersection_id<br />
";<br />
self::DbQuery($sql);<br />
<br />
// Notify all players<br />
self::notifyAllPlayers( "stonePlayed", clienttranslate( '${player_name} dropped a stone on ${coord_x},${coord_y}' ), array(<br />
'player_id' => $player_id,<br />
'player_name' => self::getActivePlayerName(),<br />
'coord_x' => $coord_x,<br />
'coord_y' => $coord_y,<br />
'color' => $color<br />
) );<br />
<br />
// Go to next game state<br />
$this->gamestate->nextState( "stonePlayed" );<br />
}<br />
</pre><br />
<br />
Catch the notification in .js->setupNotifications() and link it to a javascript function to execute when the notification is received.<br />
<br />
<pre><br />
setupNotifications: function()<br />
{<br />
console.log( 'notifications subscriptions setup' );<br />
<br />
dojo.subscribe( 'stonePlayed', this, "notif_stonePlayed" );<br />
}<br />
</pre><br />
<br />
Implement this function in javascript to update the intersection to show the stone, and register it inside the setNotifications function.<br />
<br />
<pre><br />
notif_stonePlayed: function( notif )<br />
{<br />
console.log( '**** Notification : stonePlayed' );<br />
console.log( notif );<br />
<br />
// Create a stone<br />
dojo.place( this.format_block('jstpl_stone', {<br />
stone_type:'stone_' + notif.args.color,<br />
x:notif.args.coord_x,<br />
y:notif.args.coord_y<br />
} ), $( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ) );<br />
<br />
// Place it on the player panel<br />
this.placeOnObject( $( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'player_board_' + notif.args.player_id ) );<br />
<br />
// Animate a slide from the player panel to the intersection<br />
dojo.style( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y, 'zIndex', 1 );<br />
var slide = this.slideToObject( $( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ), 1000 );<br />
dojo.connect( slide, 'onEnd', this, dojo.hitch( this, function() {<br />
// At the end of the slide, update the intersection <br />
dojo.removeClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'no_stone' );<br />
dojo.addClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'stone_' + notif.args.color );<br />
dojo.removeClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'clickable' );<br />
<br />
// We can now destroy the stone since it is now visible through the change in style of the intersection<br />
dojo.destroy( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y );<br />
}));<br />
slide.play();<br />
},<br />
</pre><br />
<br />
For this function to work properly, you also need: <br />
<br />
* to declare a stone javascript template in your .tpl file.<br />
<br />
<pre><br />
var jstpl_stone='<div class="gmk_stone ${stone_type}" id="stone_${x}_${y}"></div>';<br />
</pre><br />
<br />
* to define the css styles for the stones<br />
<br />
<pre><br />
.gmk_intersection {<br />
width: 30px;<br />
height: 30px;<br />
position: relative;<br />
background-image: url( 'img/stones.png' );<br />
}<br />
<br />
.gmk_stone {<br />
width: 30px;<br />
height: 30px;<br />
position: absolute;<br />
background-image: url( 'img/stones.png' );<br />
}<br />
<br />
.no_stone { background-position: -60px 0px; }<br />
<br />
.stone_black { background-position: 0px 0px; }<br />
.stone_white { background-position: -30px 0px; }<br />
</pre><br />
<br />
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:<br />
<br />
[[File:Gomoku stones.png]]<br />
<br />
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:<br />
<br />
* we define in the css file the 'clickable' css class<br />
<br />
<pre><br />
.clickable {<br />
cursor: pointer;<br />
}<br />
.clickable:hover { background-position: -90px 0px; }<br />
</pre><br />
<br />
* in .js, when we enter the 'playerTurn' state, we add the 'clickable' style to the intersections where there is no stone<br />
<br />
<pre><br />
onEnteringState: function( stateName, args )<br />
{<br />
console.log( 'Entering state: '+stateName );<br />
<br />
switch( stateName )<br />
{<br />
<br />
case 'playerTurn':<br />
if( this.isCurrentPlayerActive() )<br />
{<br />
var queueEntries = dojo.query( '.no_stone' );<br />
for(var i=0; i<queueEntries.length; i++) { <br />
dojo.addClass( queueEntries[i], 'clickable' );<br />
}<br />
} <br />
}<br />
},<br />
</pre><br />
<br />
The basic game turn is implemented: you can now drop some stones!<br />
<br />
[[File:Gomoku tuto4.png]]<br />
<br />
== Cleanup your styles ==<br />
<br />
Remove temporary css visualisation helpers : looks good!<br />
<br />
[[File:Gomoku tuto5.png]]<br />
<br />
== Implement rules and end of game conditions ==<br />
<br />
Implement specific rules for the game. For example in Gomoku, black plays first. So in .game.php->setupNewGame(): <br />
<br />
* modify the default colors for players to white and black<br />
<br />
$default_colors = array( "000000", "ffffff", );<br />
<br />
* and at the end of the setup make the black player active<br />
<br />
<pre><br />
// Black plays first<br />
$sql = "SELECT player_id, player_name FROM player WHERE player_color = '000000' ";<br />
$black_player = self::getNonEmptyObjectFromDb( $sql );<br />
<br />
$this->gamestate->changeActivePlayer( $black_player['player_id'] );<br />
</pre><br />
<br />
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).<br />
<br />
<pre><br />
function getGameProgression()<br />
{<br />
// Compute and return the game progression<br />
<br />
// Number of stones laid down on the goban over the total number of intersections * 100<br />
$sql = "<br />
SELECT round(100 * count(id) / (19*19) ) as value from intersection WHERE stone_color is not null<br />
";<br />
$counter = self::getNonEmptyObjectFromDB( $sql );<br />
<br />
return $counter['value'];<br />
}<br />
</pre><br />
<br />
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: <br />
<br />
* declare a global 'end_of_game' variable in .game.php->Gomoku()<br />
<br />
self::initGameStateLabels( array(<br />
"end_of_game" => 10,<br />
) );<br />
<br />
* init that global variable to 0 in .game.php->setupNewGame()<br />
<br />
self::setGameStateInitialValue( 'end_of_game', 0 );<br />
<br />
* 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.<br />
<br />
<pre><br />
// Check if end of game has been met<br />
if ($this->checkForWin( $coord_x, $coord_y, $color )) {<br />
<br />
// Set active player score to 1 (he is the winner)<br />
$sql = "UPDATE player SET player_score = 1 WHERE player_id = $player_id";<br />
self::DbQuery($sql);<br />
<br />
// Notify final score<br />
$this->notifyAllPlayers( "finalScore",<br />
clienttranslate( '${player_name} wins the game!' ),<br />
array(<br />
"player_name" => self::getActivePlayerName(),<br />
"player_id" => $player_id,<br />
"score_delta" => 1,<br />
)<br />
);<br />
<br />
// Set global variable flag to pass on the information that the game has ended<br />
self::setGameStateValue('end_of_game', 1);<br />
<br />
// End of game message<br />
$this->notifyAllPlayers( "message",<br />
clienttranslate('Thanks for playing!'),<br />
array(<br />
)<br />
);<br />
<br />
}<br />
</pre><br />
<br />
* 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).<br />
<br />
<pre><br />
function stCheckEndOfGame()<br />
{<br />
self::trace( "stCheckEndOfGame" );<br />
<br />
$transition = "notEndedYet";<br />
<br />
// If there is no more free intersections, the game ends<br />
$sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection WHERE stone_color is null";<br />
$free = self::getCollectionFromDb( $sql );<br />
<br />
if (count($free) == 0) {<br />
$transition = "gameEnded";<br />
} <br />
<br />
// If the 'end of game' flag has been set, end the game<br />
if (self::getGameStateValue('end_of_game') == 1) {<br />
$transition = "gameEnded";<br />
}<br />
<br />
$this->gamestate->nextState( $transition );<br />
}<br />
</pre><br />
<br />
* 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.<br />
<br />
<pre><br />
dojo.subscribe( 'finalScore', this, "notif_finalScore" );<br />
this.notifqueue.setSynchronous( 'finalScore', 1500 );<br />
</pre><br />
<br />
* Implement the function declared to handle the notification.<br />
<br />
<pre><br />
notif_finalScore: function( notif )<br />
{<br />
console.log( '**** Notification : finalScore' );<br />
console.log( notif );<br />
<br />
// Update score<br />
this.scoreCtrl[ notif.args.player_id ].incValue( notif.args.score_delta );<br />
},<br />
</pre><br />
<br />
'''Test everything thoroughly... you are done!'''<br />
<br />
[[File:Gomoku tuto6.png]]</div>Wucnuchttps://en.doc.boardgamearena.com/index.php?title=Tutorial_gomoku&diff=1542Tutorial gomoku2015-05-11T02:34:34Z<p>Wucnuc: getFormattedCoordinates is undefined</p>
<hr />
<div>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).<br />
<br />
== You will start from our 'emtpy game' template ==<br />
<br />
Here is how your games looks by default when it has just been created:<br />
<br />
[[File:Gomoku tuto1.png]]<br />
<br />
== Setup the board ==<br />
<br />
Gather useful images for the game and edit them as needed. Upload them in the 'img' folder of your SFTP access.<br />
<br />
Edit .tpl to add some divs for the board in the HTML. For example:<br />
<br />
<pre><br />
<div id="gmk_game_area"><br />
<div id="gmk_background"><br />
<div id="gmk_goban"><br />
</div><br />
</div> <br />
</div><br />
</pre><br />
<br />
Edit .css to set the div sizes and positions and show the image of the board as background.<br />
<br />
<pre><br />
#gmk_game_area {<br />
text-align: center;<br />
position: relative;<br />
}<br />
<br />
#gmk_background {<br />
width: 620px;<br />
height: 620px; <br />
position: relative;<br />
display: inline-block;<br />
}<br />
<br />
#gmk_goban { <br />
background-image: url( 'img/goban.jpg');<br />
width: 620px;<br />
height: 620px;<br />
position: absolute; <br />
}<br />
</pre><br />
<br />
[[File:Gomoku tuto2.png]]<br />
<br />
== Setup the backbone of your game ==<br />
<br />
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).<br />
<br />
<pre><br />
CREATE TABLE IF NOT EXISTS `intersection` (<br />
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,<br />
`coord_x` tinyint(2) unsigned NOT NULL,<br />
`coord_y` tinyint(2) unsigned NOT NULL,<br />
`stone_color` varchar(8) NULL,<br />
PRIMARY KEY (`id`)<br />
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;<br />
</pre><br />
<br />
Edit .game.php->setupNewGame() to insert the empty intersections (19x19) with coordinates into the database.<br />
<br />
<pre><br />
// Insert (empty) intersections into database<br />
$sql = "INSERT INTO intersection (coord_x, coord_y) VALUES ";<br />
$values = array();<br />
for ($x = 0; $x < 19; $x++) {<br />
for ($y = 0; $y < 19; $y++) {<br />
<br />
$values[] = "($x, $y)"; <br />
}<br />
}<br />
$sql .= implode( $values, ',' );<br />
self::DbQuery( $sql );<br />
</pre><br />
<br />
Edit .game.php->getAllDatas() to retrieve the state of the intersections from the database.<br />
<br />
<pre><br />
// Intersections<br />
$sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection ";<br />
$result['intersections'] = self::getCollectionFromDb( $sql );<br />
</pre><br />
<br />
Edit .tpl to create a template for intersections.<br />
<br />
<pre><br />
var jstpl_intersection='<div class="gmk_intersection ${stone_type}" id="intersection_${x}_${y}"></div>';<br />
</pre><br />
<br />
Define the styles for the intersection divs.<br />
<br />
<pre><br />
.gmk_intersection {<br />
width: 30px;<br />
height: 30px;<br />
position: relative;<br />
}<br />
</pre><br />
<br />
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.<br />
<br />
<pre><br />
// Setup intersections<br />
for( var id in gamedatas.intersections )<br />
{<br />
var intersection = gamedatas.intersections[id];<br />
<br />
dojo.place( this.format_block('jstpl_intersection', {<br />
x:intersection.coord_x,<br />
y:intersection.coord_y,<br />
stone_type:(intersection.stone_color == null ? "no_stone" : 'stone_' + intersection.stone_color)<br />
} ), $ ( 'gmk_background' ) );<br />
<br />
var x_pix = this.getXPixelCoordinates(intersection.coord_x);<br />
var y_pix = this.getYPixelCoordinates(intersection.coord_y);<br />
<br />
this.slideToObjectPos( $('intersection_'+intersection.coord_x+'_'+intersection.coord_y), $('gmk_background'), x_pix, y_pix, 10 ).play();<br />
<br />
if (intersection.stone_color != null) {<br />
// This intersection is taken, it shouldn't appear as clickable anymore<br />
dojo.removeClass( 'intersection_' + intersection.coord_x + '_' + intersection.coord_y, 'clickable' );<br />
}<br />
} <br />
</pre><br />
<br />
Use some temporary css border-color or background-color and opacity to see the divs and make sure you have them positioned right. <br />
<br />
<pre><br />
.gmk_intersection {<br />
width: 30px;<br />
height: 30px;<br />
position: relative;<br />
background-color: blue;<br />
opacity: 0.3;<br />
}<br />
</pre><br />
<br />
You can declare some constants in material.inc.php and pass them to your .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.<br />
<br />
* Declare your constants in material.inc.php (this will be automatically included in your .game.php)<br />
<br />
<pre><br />
$this->gameConstants = array(<br />
"INTERSECTION_WIDTH" => 30,<br />
"INTERSECTION_HEIGHT" => 30, <br />
"INTERSECTION_X_SPACER" => 2.8, // Float<br />
"INTERSECTION_Y_SPACER" => 2.8, // Float<br />
"X_ORIGIN" => 0,<br />
"Y_ORIGIN" => 0,<br />
);<br />
</pre><br />
<br />
* In .game.php->getAllDatas(), add the constants to the result array<br />
<br />
// Constants<br />
$result['constants'] = $this->gameConstants;<br />
<br />
* In .js constructor, define a class variable for constants<br />
<br />
// Game constants<br />
this.gameConstants = null;<br />
<br />
* In .js->setup() assign the constants to this variable<br />
<br />
this.gameConstants = gamedatas.constants;<br />
<br />
* Then use it in your getXPixelCoordinates and getYPixelCoordinates functions<br />
<br />
getXPixelCoordinates: function( intersection_x )<br />
{<br />
return this.gameConstants['X_ORIGIN'] + intersection_x * (this.gameConstants['INTERSECTION_WIDTH'] + this.gameConstants['INTERSECTION_X_SPACER']); <br />
},<br />
<br />
getYPixelCoordinates: function( intersection_y )<br />
{<br />
return this.gameConstants['Y_ORIGIN'] + intersection_y * (this.gameConstants['INTERSECTION_HEIGHT'] + this.gameConstants['INTERSECTION_Y_SPACER']); <br />
},<br />
<br />
Here is what you should get:<br />
<br />
[[File:Gomoku tuto3.png]]<br />
<br />
== Manage states and events ==<br />
<br />
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.<br />
<br />
The first state requires an action from the player, so its type is 'activeplayer'.<br />
<br />
The two others are automatic actions for the game, so their type is 'game'.<br />
<br />
We will update the progression while checking for the end of the game, so for this state we set the 'updateGameProgression' flag to true.<br />
<br />
<pre><br />
2 => array(<br />
"name" => "playerTurn",<br />
"description" => clienttranslate('${actplayer} must play a stone'),<br />
"descriptionmyturn" => clienttranslate('${you} must play a stone'),<br />
"type" => "activeplayer",<br />
"possibleactions" => array( "playStone" ),<br />
"transitions" => array( "stonePlayed" => 3, "zombiePass" => 3 )<br />
),<br />
<br />
3 => array(<br />
"name" => "checkEndOfGame",<br />
"description" => '',<br />
"type" => "game",<br />
"action" => "stCheckEndOfGame",<br />
"updateGameProgression" => true,<br />
"transitions" => array( "gameEnded" => 99, "notEndedYet" => 4 )<br />
),<br />
<br />
4 => array(<br />
"name" => "nextPlayer",<br />
"description" => '',<br />
"type" => "game",<br />
"action" => "stNextPlayer",<br />
"transitions" => array( "" => 2 )<br />
),<br />
</pre><br />
<br />
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:<br />
<br />
<pre><br />
function stNextPlayer()<br />
{<br />
self::trace( "stNextPlayer" );<br />
<br />
// Go to next player<br />
$active_player = self::activeNextPlayer();<br />
self::giveExtraTime( $active_player ); <br />
<br />
$this->gamestate->nextState();<br />
}<br />
</pre><br />
<br />
Add onclick events on intersections in .js->setup()<br />
<br />
// 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)<br />
this.addEventToClass( "gmk_intersection", "onclick", "onClickIntersection");<br />
<br />
Declare the corresponding .js->onClickIntersection() function, which calls an action function on the server with appropriate parameters<br />
<br />
<pre><br />
onClickIntersection: function( evt )<br />
{<br />
console.log( '$$$$ Event : onClickIntersection' );<br />
dojo.stopEvent( evt );<br />
<br />
if( ! this.checkAction( 'playStone' ) )<br />
{ return; }<br />
<br />
var node = evt.currentTarget.id;<br />
var coord_x = node.split('_')[1];<br />
var coord_y = node.split('_')[2];<br />
<br />
console.log( '$$$$ Selected intersection : (' + coord_x + ', ' + coord_y + ')' );<br />
<br />
if ( this.isCurrentPlayerActive() ) {<br />
this.ajaxcall( "/gomoku/gomoku/playStone.html", { lock: true, coord_x: coord_x, coord_y: coord_y }, this, function( result ) {}, function( is_error ) {} );<br />
}<br />
},<br />
</pre><br />
<br />
Add this action function in .action.php, retrieving parameters and calling the appropriate game action<br />
<br />
<pre><br />
public function playStone()<br />
{<br />
self::setAjaxMode(); <br />
<br />
// Retrieve arguments<br />
// Note: these arguments correspond to what has been sent through the javascript "ajaxcall" method<br />
$coord_x = self::getArg( "coord_x", AT_posint, true );<br />
$coord_y = self::getArg( "coord_y", AT_posint, true );<br />
<br />
// Then, call the appropriate method in your game logic, like "playCard" or "myAction"<br />
$this->game->playStone( $coord_x, $coord_y );<br />
<br />
self::ajaxResponse( );<br />
}<br />
</pre><br />
<br />
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.<br />
<br />
<pre><br />
function playStone( $coord_x, $coord_y )<br />
{<br />
// Check that this is player's turn and that it is a "possible action" at this game state (see states.inc.php)<br />
self::checkAction( 'playStone' ); <br />
<br />
$player_id = self::getActivePlayerId();<br />
<br />
// Check that this intersection is free<br />
$sql = "SELECT<br />
id, coord_x, coord_y, stone_color<br />
FROM<br />
intersection <br />
WHERE <br />
coord_x = $coord_x <br />
AND coord_y = $coord_y<br />
AND stone_color is null<br />
";<br />
$intersection = self::getObjectFromDb( $sql );<br />
<br />
if ($intersection == null) {<br />
throw new BgaUserException( self::_("There is already a stone on this intersection, you can't play there") );<br />
}<br />
<br />
// Get player color<br />
$sql = "SELECT<br />
player_id, player_color<br />
FROM<br />
player <br />
WHERE <br />
player_id = $player_id<br />
";<br />
$player = self::getNonEmptyObjectFromDb( $sql );<br />
$color = ($player['player_color'] == 'ffffff' ? 'white' : 'black');<br />
<br />
// Update the intersection with a stone of the appropriate color<br />
$intersection_id = $intersection['id'];<br />
$sql = "UPDATE<br />
intersection<br />
SET<br />
stone_color = '$color'<br />
WHERE <br />
id = $intersection_id<br />
";<br />
self::DbQuery($sql);<br />
<br />
// Notify all players<br />
self::notifyAllPlayers( "stonePlayed", clienttranslate( '${player_name} dropped a stone on ${coord_x},${coord_y}' ), array(<br />
'player_id' => $player_id,<br />
'player_name' => self::getActivePlayerName(),<br />
'coord_x' => $coord_x,<br />
'coord_y' => $coord_y,<br />
'color' => $color<br />
) );<br />
<br />
// Go to next game state<br />
$this->gamestate->nextState( "stonePlayed" );<br />
}<br />
</pre><br />
<br />
Catch the notification in .js->setupNotifications() and link it to a javascript function to execute when the notification is received.<br />
<br />
<pre><br />
setupNotifications: function()<br />
{<br />
console.log( 'notifications subscriptions setup' );<br />
<br />
dojo.subscribe( 'stonePlayed', this, "notif_stonePlayed" );<br />
}<br />
</pre><br />
<br />
Implement this function in javascript to update the intersection to show the stone, and register it inside the setNotifications function.<br />
<br />
<pre><br />
notif_stonePlayed: function( notif )<br />
{<br />
console.log( '**** Notification : stonePlayed' );<br />
console.log( notif );<br />
<br />
// Create a stone<br />
dojo.place( this.format_block('jstpl_stone', {<br />
stone_type:'stone_' + notif.args.color,<br />
x:notif.args.coord_x,<br />
y:notif.args.coord_y<br />
} ), $( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ) );<br />
<br />
// Place it on the player panel<br />
this.placeOnObject( $( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'player_board_' + notif.args.player_id ) );<br />
<br />
// Animate a slide from the player panel to the intersection<br />
dojo.style( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y, 'zIndex', 1 );<br />
var slide = this.slideToObject( $( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ), 1000 );<br />
dojo.connect( slide, 'onEnd', this, dojo.hitch( this, function() {<br />
// At the end of the slide, update the intersection <br />
dojo.removeClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'no_stone' );<br />
dojo.addClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'stone_' + notif.args.color );<br />
dojo.removeClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'clickable' );<br />
<br />
// We can now destroy the stone since it is now visible through the change in style of the intersection<br />
dojo.destroy( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y );<br />
}));<br />
slide.play();<br />
},<br />
</pre><br />
<br />
For this function to work properly, you also need: <br />
<br />
* to declare a stone javascript template in your .tpl file.<br />
<br />
<pre><br />
var jstpl_stone='<div class="gmk_stone ${stone_type}" id="stone_${x}_${y}"></div>';<br />
</pre><br />
<br />
* to define the css styles for the stones<br />
<br />
<pre><br />
.gmk_stone {<br />
width: 30px;<br />
height: 30px;<br />
position: absolute;<br />
background-image: url( 'img/stones.png');<br />
}<br />
<br />
.no_stone { background-position: -60px 0px; }<br />
<br />
.stone_black { background-position: 0px 0px; }<br />
.stone_white { background-position: -30px 0px; }<br />
</pre><br />
<br />
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:<br />
<br />
[[File:Gomoku stones.png]]<br />
<br />
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:<br />
<br />
* we define in the css file the 'clickable' css class<br />
<br />
<pre><br />
.clickable {<br />
cursor: pointer;<br />
}<br />
.clickable:hover { background-position: -90px 0px; }<br />
</pre><br />
<br />
* in .js, when we enter the 'playerTurn' state, we add the 'clickable' style to the intersections where there is no stone<br />
<br />
<pre><br />
onEnteringState: function( stateName, args )<br />
{<br />
console.log( 'Entering state: '+stateName );<br />
<br />
switch( stateName )<br />
{<br />
<br />
case 'playerTurn':<br />
if( this.isCurrentPlayerActive() )<br />
{<br />
var queueEntries = dojo.query( '.no_stone' );<br />
for(var i=0; i<queueEntries.length; i++) { <br />
dojo.addClass( queueEntries[i], 'clickable' );<br />
}<br />
} <br />
}<br />
},<br />
</pre><br />
<br />
The basic game turn is implemented: you can now drop some stones!<br />
<br />
[[File:Gomoku tuto4.png]]<br />
<br />
== Cleanup your styles ==<br />
<br />
Remove temporary css visualisation helpers : looks good!<br />
<br />
[[File:Gomoku tuto5.png]]<br />
<br />
== Implement rules and end of game conditions ==<br />
<br />
Implement specific rules for the game. For example in Gomoku, black plays first. So in .game.php->setupNewGame(): <br />
<br />
* modify the default colors for players to white and black<br />
<br />
$default_colors = array( "000000", "ffffff", );<br />
<br />
* and at the end of the setup make the black player active<br />
<br />
<pre><br />
// Black plays first<br />
$sql = "SELECT player_id, player_name FROM player WHERE player_color = '000000' ";<br />
$black_player = self::getNonEmptyObjectFromDb( $sql );<br />
<br />
$this->gamestate->changeActivePlayer( $black_player['player_id'] );<br />
</pre><br />
<br />
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).<br />
<br />
<pre><br />
function getGameProgression()<br />
{<br />
// Compute and return the game progression<br />
<br />
// Number of stones laid down on the goban over the total number of intersections * 100<br />
$sql = "<br />
SELECT round(100 * count(id) / (19*19) ) as value from intersection WHERE stone_color is not null<br />
";<br />
$counter = self::getNonEmptyObjectFromDB( $sql );<br />
<br />
return $counter['value'];<br />
}<br />
</pre><br />
<br />
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: <br />
<br />
* declare a global 'end_of_game' variable in .game.php->Gomoku()<br />
<br />
self::initGameStateLabels( array(<br />
"end_of_game" => 10,<br />
) );<br />
<br />
* init that global variable to 0 in .game.php->setupNewGame()<br />
<br />
self::setGameStateInitialValue( 'end_of_game', 0 );<br />
<br />
* 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.<br />
<br />
<pre><br />
// Check if end of game has been met<br />
if ($this->checkForWin( $coord_x, $coord_y, $color )) {<br />
<br />
// Set active player score to 1 (he is the winner)<br />
$sql = "UPDATE player SET player_score = 1 WHERE player_id = $player_id";<br />
self::DbQuery($sql);<br />
<br />
// Notify final score<br />
$this->notifyAllPlayers( "finalScore",<br />
clienttranslate( '${player_name} wins the game!' ),<br />
array(<br />
"player_name" => self::getActivePlayerName(),<br />
"player_id" => $player_id,<br />
"score_delta" => 1,<br />
)<br />
);<br />
<br />
// Set global variable flag to pass on the information that the game has ended<br />
self::setGameStateValue('end_of_game', 1);<br />
<br />
// End of game message<br />
$this->notifyAllPlayers( "message",<br />
clienttranslate('Thanks for playing!'),<br />
array(<br />
)<br />
);<br />
<br />
}<br />
</pre><br />
<br />
* 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).<br />
<br />
<pre><br />
function stCheckEndOfGame()<br />
{<br />
self::trace( "stCheckEndOfGame" );<br />
<br />
$transition = "notEndedYet";<br />
<br />
// If there is no more free intersections, the game ends<br />
$sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection WHERE stone_color is null";<br />
$free = self::getCollectionFromDb( $sql );<br />
<br />
if (count($free) == 0) {<br />
$transition = "gameEnded";<br />
} <br />
<br />
// If the 'end of game' flag has been set, end the game<br />
if (self::getGameStateValue('end_of_game') == 1) {<br />
$transition = "gameEnded";<br />
}<br />
<br />
$this->gamestate->nextState( $transition );<br />
}<br />
</pre><br />
<br />
* 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.<br />
<br />
<pre><br />
dojo.subscribe( 'finalScore', this, "notif_finalScore" );<br />
this.notifqueue.setSynchronous( 'finalScore', 1500 );<br />
</pre><br />
<br />
* Implement the function declared to handle the notification.<br />
<br />
<pre><br />
notif_finalScore: function( notif )<br />
{<br />
console.log( '**** Notification : finalScore' );<br />
console.log( notif );<br />
<br />
// Update score<br />
this.scoreCtrl[ notif.args.player_id ].incValue( notif.args.score_delta );<br />
},<br />
</pre><br />
<br />
'''Test everything thoroughly... you are done!'''<br />
<br />
[[File:Gomoku tuto6.png]]</div>Wucnuchttps://en.doc.boardgamearena.com/index.php?title=Tutorial_reversi&diff=1540Tutorial reversi2015-05-09T20:00:48Z<p>Wucnuc: </p>
<hr />
<div><br />
== Introduction ==<br />
<br />
Using this tutorial, you can build a complete working game on the BGA environment: Reversi.<br />
<br />
Before you read this tutorial, you must:<br />
* Read the overall presentations of the BGA Framework ([[Studio|see here]]).<br />
* Know the [http://en.wikipedia.org/wiki/Reversi#Rules rules of Reversi].<br />
* Know the languages used on BGA: PHP, SQL, HTML, CSS, Javascript.<br />
<br />
== Create your first game ==<br />
<br />
With the initial skeleton of code provided initially, you can already start a game from the BGA Studio. For now, we are going to work with 1 player only. Most of the time this is simpler to proceed with only one player during the early phase of development of your game, as it's easy and fast to start/stop games.<br />
<br />
Thus, you can start a "Reversi" game, and arrive on a void, empty game. Yeah.<br />
<br />
== Let it look like Reversi ==<br />
<br />
It's always a good idea to start with a little bit of graphic work. Why? Because this helps to figure out how the final game will be, and issues that may appear later.<br />
<br />
Be careful designing the layout of your game: you must always keep in mind that players with a 1024px screen width must be able to play. Usually, it means that the width of the play area can be 750px (in the worst case).<br />
<br />
For Reversi, it's useless to have a 750x750px board - much too big, so we choose this one which fit perfectly (536x528):<br />
<br />
[[File:Board.jpg]]<br />
<br />
Note that we are using a jpg file. Jpg are lighter than png, so faster to load. Later we are going to use PNGs for discs for transparency purpose.<br />
<br />
Now, let's make it appears on our game:<br />
* upload board.jpg in your "img/" directory.<br />
* edit "reversi_reversi.tpl" to add a 'div' for your board:<br />
<br />
<pre><br />
<div id="board"><br />
</div><br />
</pre><br />
<br />
* edit your reversi.css file to transform it into a visible board:<br />
<br />
#board {<br />
width: 536px;<br />
height: 528px;<br />
background-image: url('img/board.jpg');<br />
}<br />
<br />
Refresh your page. Here's your board:<br />
<br />
[[File:reversi1.jpg]]<br />
<br />
== Make the squares appears ==<br />
<br />
Now, what we need is to create some invisible HTML elements where squares are. These elements will be used as position references for white and black discs. <br />
Obviously, we need 64 squares. To avoid writing 64 'div' elements on our template, we are going to use the "block" feature.<br />
<br />
Let's modify our template like this:<br />
<br />
<pre><br />
<div id="board"><br />
<!-- BEGIN square --><br />
<div id="square_{X}_{Y}" class="square" style="left: {LEFT}px; top: {TOP}px;"></div><br />
<!-- END square --><br />
</div><br />
</pre><br />
<br />
As you can see, we created a "square" block, with 4 variable elements: X, Y, LEFT and TOP. Obviously, we are going to use this block 64 times during page load.<br />
<br />
Let's do it in our "reversi.view.php" file:<br />
<br />
<pre><br />
$this->page->begin_block( "reversi_reversi", "square" );<br />
<br />
$hor_scale = 64.8;<br />
$ver_scale = 64.4;<br />
for( $x=1; $x<=8; $x++ )<br />
{<br />
for( $y=1; $y<=8; $y++ )<br />
{<br />
$this->page->insert_block( "square", array(<br />
'X' => $x,<br />
'Y' => $y,<br />
'LEFT' => round( ($x-1)*$hor_scale+10 ),<br />
'TOP' => round( ($y-1)*$ver_scale+7 )<br />
) );<br />
} <br />
}<br />
</pre><br />
<br />
Note: as you can see, squares in our "board.jpg" files does not have an exact width/height in pixel, and that's the reason we are using floating point numbers here.<br />
<br />
Now, to finish our work and check if everything works fine, we are going to style our square a little bit in our CSS stylesheet:<br />
<br />
<pre><br />
#board {<br />
width: 536px;<br />
height: 528px;<br />
background-image: url('img/board.jpg');<br />
position: relative;<br />
}<br />
<br />
.square {<br />
width: 62px;<br />
height: 62px;<br />
position: absolute;<br />
background-color: red;<br />
}<br />
</pre><br />
<br />
Explanations:<br />
* With "position: relative" on board, we ensure square elements are positioned relatively to board.<br />
* For the test, we use a red background color for the square. This is a useful tip to figure out if everything is fine with invisible elements.<br />
<br />
Let's refresh and check our our (beautiful) squares:<br />
<br />
[[File:reversi2.jpg]]<br />
<br />
== The discs ==<br />
<br />
Now, our board is ready to receive some disc tokens!<br />
<br />
At first, we introduce a new 'div' element as a child of "board" to host all these tokens (in our template):<br />
<br />
<pre><br />
<!-- END square --><br />
<br />
<div id="tokens"><br />
</div><br />
</div><br />
</pre><br />
<br />
Then, let's introduce a new piece of art with the discs. We need some transparency here so we are using a png file:<br />
<br />
[[File:tokens.png]]<br />
<br />
Important: we are using ONE file for both discs. It's really important that you use a minimum number of graphic files for your game with this "CSS sprite" technique, because it makes the game loading faster and more reliable. [http://www.w3schools.com/css/css_image_sprites.asp Read more about CSS sprites].<br />
<br />
Now, let's separate the disc with some CSS stuff:<br />
<br />
<pre><br />
.token {<br />
width: 56px;<br />
height: 56px;<br />
position: absolute;<br />
background-image: url('img/tokens.png');<br />
}<br />
.tokencolor_ffffff { background-position: 0px 0px; }<br />
.tokencolor_000000 { background-position: -56px 0px; }<br />
</pre><br />
<br />
With this CSS code, we apply the classes "token" and "tokencolor_ffffff" to a div element and we've got a white token. Yeah.<br />
<br />
Note the "position: absolute" which allows us to position tokens on the board and make them "slide" to their positions.<br />
<br />
Now, let's make a first token appear on our board. Disc tokens are not visible at the beginning of the game: they appear dynamically during the game. For this reason, we are going to make them appear from our Javascript code, with a BGA Framework technique called "JS template".<br />
<br />
In our template file (reversi_reversi.tpl), let's create the piece of HTML needed to display our token:<br />
<br />
<pre><br />
<script type="text/javascript"><br />
<br />
// Templates<br />
<br />
var jstpl_token='<div class="token tokencolor_${color}" id="token_${x_y}"></div>';<br />
<br />
</script><br />
</pre><br />
<br />
Note: we already created the "templates" section for you in the game skeleton.<br />
<br />
As you can see, we defined a JS template named "jstpl_token" with a piece of HTML and two variables: the color of the token and its x/y coordinates. Note that the syntax of the argument is different for template block variables (brackets) and JS template variables (dollar and brackets).<br />
<br />
Now, let's create a method in our Javascript code that will make a token appear on the board, using this template:<br />
<br />
<pre><br />
addTokenOnBoard: function( x, y, player )<br />
{<br />
dojo.place( this.format_block( 'jstpl_token', {<br />
x_y: x+'_'+y,<br />
color: this.gamedatas.players[ player ].color<br />
} ) , 'tokens' );<br />
<br />
this.placeOnObject( 'token_'+x+'_'+y, 'overall_player_board_'+player );<br />
this.slideToObject( 'token_'+x+'_'+y, 'square_'+x+'_'+y ).play();<br />
},<br />
</pre><br />
<br />
At first, with "dojo.place" and "this.format_block" methods, we create a HTML piece of code and insert it as a new child of "tokens" div element.<br />
<br />
Then, with BGA "this.placeOnObject" method, we place this element over the panel of some player. Immediately after, using BGA "this.slidetoObject" method, we make the disc slide to the "square" element, its final destination.<br />
<br />
Note: don't forget to call the "play()", otherwise the token remains at its original location.<br />
<br />
Note: note that during all the process, the parent of the new disc HTML element will remain "tokens". placeOnObject and slideToObject methods are only moving the position of elements on screen, and they are not modifying the HTML tree.<br />
<br />
Before we can show a token, we need to set the player colors in the setupNewGame function in reversi.game.php:<br />
<pre><br />
$default_colors = array( "ffffff", "000000" );<br />
</pre><br />
<br />
Now, to test if everything works fine, just call "this.addTokenOnBoard( 2, 2, <your_player_id> )" in your "setup" Javascript method, and reload the page. A token should appear and slide immediately to its position, like this:<br />
<br />
[[File:reversi3.jpg]]<br />
<br />
== The database ==<br />
<br />
We did most of the client-side programming, so let's have a look on the other side now.<br />
<br />
To design the database model of our game, the best thing to do is to follow the "Go to game database" link at the bottom of our game, to access the database directly with a [http://www.phpmyadmin.net/ PhpMyAdmin] instance.<br />
<br />
Then, you can create the tables you need for your table (do not remove existing tables!), and report every SQL command used in your "dbmodel.sql" file.<br />
<br />
[[File:reversi4.jpg]]<br />
<br />
The database model of Reversi is very simple: just one table with the squares of the board. In our dbmodel.sql, we have this:<br />
<br />
<pre><br />
CREATE TABLE IF NOT EXISTS `board` (<br />
`board_x` smallint(5) unsigned NOT NULL,<br />
`board_y` smallint(5) unsigned NOT NULL,<br />
`board_player` int(10) unsigned DEFAULT NULL,<br />
PRIMARY KEY (`board_x`,`board_y`)<br />
) ENGINE=InnoDB;<br />
</pre><br />
<br />
Now, a new database with a "board" table will be created each time we start a Reversi game. This is why after modifying our dbmodel.sql it's a good time to stop & start again our game.<br />
<br />
== Setup the initial game position ==<br />
<br />
The "setupNewGame" method of our reversi.game.php is called during initial setup: this is the place to initialize our data and to place the initial tokens on the board (initially, there are 4 tokens on the board).<br />
<br />
Let's do this:<br />
<br />
<pre><br />
// Init the board<br />
$sql = "INSERT INTO board (board_x,board_y,board_player) VALUES ";<br />
$sql_values = array();<br />
list( $blackplayer_id, $whiteplayer_id ) = array_keys( $players );<br />
for( $x=1; $x<=8; $x++ )<br />
{<br />
for( $y=1; $y<=8; $y++ )<br />
{<br />
$token_value = "NULL";<br />
if( ($x==4 && $y==4) || ($x==5 && $y==5) ) // Initial positions of white player<br />
$token_value = "'$whiteplayer_id'";<br />
else if( ($x==4 && $y==5) || ($x==5 && $y==4) ) // Initial positions of black player<br />
$token_value = "'$blackplayer_id'";<br />
<br />
$sql_values[] = "('$x','$y',$token_value)";<br />
}<br />
}<br />
$sql .= implode( $sql_values, ',' );<br />
self::DbQuery( $sql );<br />
<br />
<br />
// Active first player<br />
self::activeNextPlayer(); <br />
</pre><br />
<br />
As you can see, we create one table entry for each square, with a "NULL" value which means "empty square". Of course, for 4 specific squares, we place an initial token.<br />
<br />
At the end, we call activeNextPlayer to make the first player active at the beginning of the game.<br />
<br />
Now, we need to make these tokens appear on the client side. To achieve this, the first step is to return the token positions with our "getAllDatas" PHP method (called during each page reload):<br />
<br />
<pre><br />
// Get reversi board token<br />
$result['board'] = self::getObjectListFromDB( "SELECT board_x x, board_y y, board_player player<br />
FROM board<br />
WHERE board_player IS NOT NULL" );<br />
</pre><br />
<br />
As you can see, we are using the BGA framework "getObjectListFromDB" method that formats the result of this SQL query in a PHP array with x, y and player attributes.<br />
<br />
The last thing we need to do is to process this array client side, and place a disc token on the board for each array item. Of course, we are doing this is our Javascript "setup" method:<br />
<br />
<pre><br />
for( var i in gamedatas.board )<br />
{<br />
var square = gamedatas.board[i];<br />
<br />
if( square.player !== null )<br />
{<br />
this.addTokenOnBoard( square.x, square.y, square.player );<br />
}<br />
}<br />
</pre><br />
<br />
As you can see, our "board" entry created in "getAllDatas" can be used here as "gamedatas.board" in our Javascript. We are using our previously developed "addTokenOnBoard" method.<br />
<br />
Reload... and here we are:<br />
<br />
[[File:reversi5.jpg]]<br />
<br />
It starts to smell Reversi here...<br />
<br />
== The game state machine ==<br />
<br />
Now, let's stop our game again, because we are going to start the core game logic.<br />
<br />
You already read the "Focus on BGA game state machine", so you know that this is the heart of your game logic. For reversi, it's very simple although. Here's a diagram of our game state machine:<br />
<br />
[[File:reversi6.jpg]]<br />
<br />
And here's our "states.inc.php", according to this diagram:<br />
<br />
<pre><br />
$machinestates = array(<br />
<br />
1 => array(<br />
"name" => "gameSetup",<br />
"description" => clienttranslate("Game setup"),<br />
"type" => "manager",<br />
"action" => "stGameSetup",<br />
"transitions" => array( "" => 10 )<br />
),<br />
<br />
10 => array(<br />
"name" => "playerTurn",<br />
"description" => clienttranslate('${actplayer} must play a disc'),<br />
"descriptionmyturn" => clienttranslate('${you} must play a disc'),<br />
"type" => "activeplayer",<br />
"args" => "argPlayerTurn",<br />
"possibleactions" => array( 'playDisc' ),<br />
"transitions" => array( "playDisc" => 11, "zombiePass" => 11 )<br />
),<br />
<br />
11 => array(<br />
"name" => "nextPlayer",<br />
"type" => "game",<br />
"action" => "stNextPlayer",<br />
"updateGameProgression" => true, <br />
"transitions" => array( "nextTurn" => 10, "cantPlay" => 11, "endGame" => 99 )<br />
),<br />
<br />
99 => array(<br />
"name" => "gameEnd",<br />
"description" => clienttranslate("End of game"),<br />
"type" => "manager",<br />
"action" => "stGameEnd",<br />
"args" => "argGameEnd"<br />
)<br />
<br />
);<br />
</pre><br />
<br />
Now, let's create in reversi.game.php the methods that are declared in this game states description file:<br />
* argPlayerTurn<br />
* stNextPlayer<br />
<br />
... and start a new Reversi game.<br />
<br />
As you can see on the screen capture below, the BGA framework makes the game jump to our first game state "playerTurn" right after the initial setup. That's why the status bar contains the description of playerTurn state ("XXXX must play a disc"):<br />
<br />
[[File:reversi7.jpg]]<br />
<br />
== The rules ==<br />
<br />
Now, what we would like to do is to indicate to the current player where she is allowed to play. The idea is to build a "getPossibleMoves" PHP method that return a list of coordinates where she is allowed to play. This method will be used in particular:<br />
* As we just said, to help the player to see where she can play.<br />
* When the player plays, to check if she has the right to play here.<br />
<br />
This is pure PHP programming here, and there are no special things from the BGA framework that can be used. This is why we won't go into details here. The overall idea is:<br />
* Create a "getTurnedOverDiscs(x,y)" method that return coordinates of discs that would be turned over if a token would be played at x,y.<br />
* Loop through all free squares of the board, call the "getTurnedOverDiscs" method on each of them. If at least 1 token is turned over, this is a valid move.<br />
<br />
One important thing to keep in mind is the following: making a database query is slow, so please don't load the entire game board with a SQL query multiple time. In our implementation, we load the entire board once at the beginning of "getPossibleMoves", and then pass the board as an argument to all methods.<br />
<br />
If you want to look into details, please look at the "utility method" sections of reversi.game.php.<br />
<br />
== Display allowed moves ==<br />
<br />
Now, what we want to do is highlight the squares where the player can place a disc.<br />
<br />
To do this, we are using the "argPlayerTurn" method. This method is called each time we enter into "playerTurn" game state, and its result is transfered automatically to the client-side:<br />
<br />
<pre><br />
<br />
function argPlayerTurn()<br />
{<br />
return array(<br />
'possibleMoves' => self::getPossibleMoves( self::getActivePlayerId() )<br />
);<br />
}<br />
</pre><br />
<br />
We are of course using the "getPossibleMoves" method we just developed.<br />
<br />
Now, let's go to the client side to use the data returned by the method above. We are using the "onEnteringState" Javascript method that is called each time we enter into a new game state:<br />
<br />
<pre><br />
onEnteringState: function( stateName, args )<br />
{<br />
console.log( 'Entering state: '+stateName );<br />
<br />
switch( stateName )<br />
{<br />
case 'playerTurn':<br />
this.updatePossibleMoves( args.args.possibleMoves );<br />
break;<br />
}<br />
},<br />
</pre><br />
<br />
So, when we are entering into "playerTurn" game state, we are calling our "updatePossibleMoves" method. This method looks like this:<br />
<br />
<pre><br />
updatePossibleMoves: function( possibleMoves )<br />
{<br />
// Remove current possible moves<br />
dojo.query( '.possibleMove' ).removeClass( 'possibleMove' );<br />
<br />
for( var x in possibleMoves )<br />
{<br />
for( var y in possibleMoves[ x ] )<br />
{<br />
// x,y is a possible move<br />
dojo.addClass( 'square_'+x+'_'+y, 'possibleMove' );<br />
} <br />
}<br />
<br />
this.addTooltipToClass( 'possibleMove', '', _('Place a disc here') );<br />
},<br />
</pre><br />
<br />
The idea here is that we've created a CSS class ("possibleMove") that can be applied to a "square" element to highlight it:<br />
<pre><br />
.possibleMove {<br />
background-color: white;<br />
opacity: 0.2;<br />
filter:alpha(opacity=20); /* For IE8 and earlier */ <br />
cursor: pointer; <br />
}<br />
</pre><br />
<br />
At first, we remove all "possibleMove" classes currently applied with the very useful combination of "dojo.query" and "removeClass" method.<br />
<br />
Then we loop through all possible moves our PHP "updatePossibleMoves" function created for us, and add the "possibleMove" class to each corresponding square.<br />
<br />
Finally, we use the BGA framework "addTooltipToClass" method to associate a tooltip to all those highlighted squares so that players can understand their meaning.<br />
<br />
And here we are:<br />
<br />
[[File:reversi8.jpg.jpg]]<br />
<br />
== Let's play ==<br />
<br />
From now, it's better to restart a game with 2 players, because we are going to implement a complete Reversi turn. The summary of what we are going to do is:<br />
* When we click on a "possibleMove" square, send the move to the server.<br />
* Server side, check the move is correct, apply Reversi rules and jump to next player.<br />
* Client side, change the disc position to reflect the move.<br />
<br />
Thus, what we do first is associate each click on a square to one of our method. We are doing this in our Javascript "setup" method:<br />
<br />
<pre><br />
dojo.query( '.square' ).connect( 'onclick', this, 'onPlayDisc' );<br />
</pre><br />
<br />
Note the use of the "dojo.query" method to get all HTML elements with "square" class in just one function call. Now, our "onPlayDisc" method is called each time someone clicks on a square.<br />
<br />
Here's our "onPlayDisc" method below:<br />
<br />
<pre><br />
onPlayDisc: function( evt )<br />
{<br />
// Stop this event propagation<br />
dojo.stopEvent( evt );<br />
<br />
// Get the cliqued square x and y<br />
// Note: square id format is "square_X_Y"<br />
var coords = evt.currentTarget.id.split('_');<br />
var x = coords[1];<br />
var y = coords[2];<br />
<br />
if( ! dojo.hasClass( 'square_'+x+'_'+y, 'possibleMove' ) )<br />
{<br />
// This is not a possible move => the click does nothing<br />
return ;<br />
}<br />
<br />
if( this.checkAction( 'playDisc' ) ) // Check that this action is possible at this moment<br />
{ <br />
this.ajaxcall( "/reversi/reversi/playDisc.html", {<br />
x:x,<br />
y:y<br />
}, this, function( result ) {} );<br />
} <br />
},<br />
</pre><br />
<br />
What we do here is:<br />
* We stop the propagation of the Javascript "onclick" event. Otherwise, it can lead to random behavior so it's always a good idea.<br />
* We get the x/y coordinates of the square by using "evt.currentTarget.id".<br />
* We check that clicked square has the "possibleMove" class, otherwise we know for sure that we can't play there.<br />
* We check that "playDisc" action is possible, according to current game state (see "possibleactions" entry in our "playerTurn" game state defined above). This check is important to avoid issues if a player double clicks on a square.<br />
* Finally, we make a call to the server using BGA "ajaxcall" method with argument x and y.<br />
<br />
Now, we have to manage this "playDisc" action on the server side. At first, we introduce a "playDisc" entry point in our "reversi.action.php":<br />
<br />
<pre><br />
public function playDisc()<br />
{<br />
self::setAjaxMode(); <br />
$x = self::getArg( "x", AT_posint, true );<br />
$y = self::getArg( "y", AT_posint, true );<br />
$result = $this->game->playDisc( $x, $y );<br />
self::ajaxResponse( );<br />
}<br />
</pre><br />
<br />
As you can see, we get the 2 arguments x and y from the javascript call, and call a corresponding "playDisc" method in our game logic.<br />
<br />
Now, let's have a look of this playDisc method:<br />
<br />
<pre><br />
function playDisc( $x, $y )<br />
{<br />
// Check that this player is active and that this action is possible at this moment<br />
self::checkAction( 'playDisc' ); <br />
</pre><br />
<br />
... at first, we check that this action is possible according to current game state (see "possible action"). We already did it on client side, but it's important to do it on server side too (otherwise it would be possible to cheat).<br />
<br />
<pre><br />
// Now, check if this is a possible move<br />
$board = self::getBoard();<br />
$player_id = self::getActivePlayerId();<br />
$turnedOverDiscs = self::getTurnedOverDiscs( $x, $y, $player_id, $board );<br />
<br />
if( count( $turnedOverDiscs ) > 0 )<br />
{<br />
// This move is possible!<br />
</pre><br />
<br />
...now, we are using the "getTurnedOverDiscs" method again to check that this move is possible.<br />
<br />
<pre><br />
// Let's place a disc at x,y and return all "$returned" discs to the active player<br />
<br />
$sql = "UPDATE board SET board_player='$player_id'<br />
WHERE ( board_x, board_y) IN ( ";<br />
<br />
foreach( $turnedOverDiscs as $turnedOver )<br />
{<br />
$sql .= "('".$turnedOver['x']."','".$turnedOver['y']."'),";<br />
}<br />
$sql .= "('$x','$y') ) ";<br />
<br />
self::DbQuery( $sql );<br />
<br />
</pre><br />
<br />
... we update the database to change the color of all turned over disc + the disc we just placed.<br />
<br />
<pre><br />
// Update scores according to the number of disc on board<br />
$sql = "UPDATE player<br />
SET player_score = (<br />
SELECT COUNT( board_x ) FROM board WHERE board_player=player_id<br />
)";<br />
self::DbQuery( $sql );<br />
<br />
// Statistics<br />
self::incStat( count( $turnedOverDiscs ), "turnedOver", $player_id );<br />
if( ($x==1 && $y==1) || ($x==8 && $y==1) || ($x==1 && $y==8) || ($x==8 && $y==8) )<br />
self::incStat( 1, 'discPlayedOnCorner', $player_id );<br />
else if( $x==1 || $x==8 || $y==1 || $y==8 )<br />
self::incStat( 1, 'discPlayedOnBorder', $player_id );<br />
else if( $x>=3 && $x<=6 && $y>=3 && $y<=6 )<br />
self::incStat( 1, 'discPlayedOnCenter', $player_id );<br />
<br />
</pre><br />
<br />
... now, we update both player score by counting all disc, and we manage game statistics.<br />
<br />
<pre><br />
// Notify<br />
self::notifyAllPlayers( "playDisc", clienttranslate( '${player_name} plays a disc and turns over ${returned_nbr} disc(s)' ), array(<br />
'player_id' => $player_id,<br />
'player_name' => self::getActivePlayerName(),<br />
'returned_nbr' => count( $turnedOverDiscs ),<br />
'x' => $x,<br />
'y' => $y<br />
) );<br />
<br />
self::notifyAllPlayers( "turnOverDiscs", '', array(<br />
'player_id' => $player_id,<br />
'turnedOver' => $turnedOverDiscs<br />
) );<br />
<br />
$newScores = self::getCollectionFromDb( "SELECT player_id, player_score FROM player", true );<br />
self::notifyAllPlayers( "newScores", "", array(<br />
"scores" => $newScores<br />
) );<br />
</pre><br />
<br />
... then we notify about all these changes. We are using for that 3 notifications ('playDisc', 'turnOverDiscs' and 'newScores' that we are going to implement on client side later). Note that the description of the 'playDisc' notification will be logged in the game log.<br />
<br />
<pre><br />
// Then, go to the next state<br />
$this->gamestate->nextState( 'playDisc' );<br />
}<br />
else<br />
throw new feException( "Impossible move" );<br />
}<br />
</pre><br />
<br />
... finally, we jump to the next game state if everything goes fine ('playDisc' is also the name of a transition in the 'playerTurn' game state description above).<br />
To make the statistics work, we have to initialize them in stats.inc.php:<br />
<br />
<pre><br />
// Statistics existing for each player<br />
"player" => array(<br />
<br />
"discPlayedOnCorner" => array( "id"=> 10,<br />
"name" => totranslate("Discs played on a corner"), <br />
"type" => "int" ),<br />
<br />
"discPlayedOnBorder" => array( "id"=> 11,<br />
"name" => totranslate("Discs played on a border"), <br />
"type" => "int" ),<br />
<br />
"discPlayedOnCenter" => array( "id"=> 12,<br />
"name" => totranslate("Discs played on board center part"), <br />
"type" => "int" ),<br />
<br />
"turnedOver" => array( "id"=> 13,<br />
"name" => totranslate("Number of discs turned over"), <br />
"type" => "int" ) <br />
)<br />
</pre><br />
<br />
A last thing to do on the server side is to activate the next player when we enter the "nextPlayer" game state:<br />
<br />
<pre><br />
function stNextPlayer()<br />
{<br />
// Activate next player<br />
$player_id = self::activeNextPlayer();<br />
<br />
self::giveExtraTime( $player_id );<br />
$this->gamestate->nextState( 'nextTurn' );<br />
<br />
}<br />
</pre><br />
<br />
Now, when we play a disc, the rules are checked and the disc appears in the database.<br />
<br />
[[File:reversi9.jpg]]<br />
<br />
Of course, as we don't manage notifications on client side, we need to press F5 after each move to see the changes on the board.<br />
<br />
== Make the move appear automatically ==<br />
<br />
Now, what we have to do is process the notifications sent by the server and make the move appear on the interface.<br />
<br />
In our "setupNotifications" method, we register 2 methods for the 2 notifications we created at the previous step ('playDisc' and 'turnOverDiscs'):<br />
<br />
<pre><br />
dojo.subscribe( 'playDisc', this, "notif_playDisc" );<br />
this.notifqueue.setSynchronous( 'playDisc', 500 );<br />
dojo.subscribe( 'turnOverDiscs', this, "notif_turnOverDiscs" );<br />
this.notifqueue.setSynchronous( 'turnOverDiscs', 1500 );<br />
</pre><br />
<br />
As you can see, we associate our 2 notifications with 2 methods with the "notif_" prefix. At the same time, we define these notifications as "synchronous", with a duration in millisecond (500 for the first one, and 1500 for the second one). It tells the user interface to wait some time after executing the notification, to let the animation end before starting the next notification. In our specific case, the animation will be the following:<br />
* Make a disc slide from the player panel to its place on the board<br />
* (wait 500ms)<br />
* Make all turned over discs blink (and of course turned them over)<br />
* (wait 1500ms)<br />
* Let the next player play.<br />
<br />
Let's have a look now on the "playDisc" notification handler method:<br />
<br />
<pre><br />
notif_playDisc: function( notif )<br />
{<br />
// Remove current possible moves (makes the board more clear)<br />
dojo.query( '.possibleMove' ).removeClass( 'possibleMove' ); <br />
<br />
this.addTokenOnBoard( notif.args.x, notif.args.y, notif.args.player_id );<br />
},<br />
</pre><br />
<br />
No surprise here, we re-used some existing stuff to:<br />
* Remove the highlighted squares.<br />
* Add a new disc on board, coming from player panel.<br />
<br />
Now, here's the method that handles the turnOverDiscs notification:<br />
<br />
<pre><br />
notif_turnOverDiscs: function( notif )<br />
{<br />
// Get the color of the player who is returning the discs<br />
var targetColor = this.gamedatas.players[ notif.args.player_id ].color;<br />
<br />
// Make these discs blink and set them to the specified color<br />
for( var i in notif.args.turnedOver )<br />
{<br />
var token = notif.args.turnedOver[ i ];<br />
<br />
// Make the token blink 2 times<br />
var anim = dojo.fx.chain( [<br />
dojo.fadeOut( { node: 'token_'+token.x+'_'+token.y } ),<br />
dojo.fadeIn( { node: 'token_'+token.x+'_'+token.y } ),<br />
dojo.fadeOut( { <br />
node: 'token_'+token.x+'_'+token.y,<br />
onEnd: function( node ) {<br />
<br />
// Remove any color class<br />
dojo.removeClass( node, [ 'tokencolor_000000', 'tokencolor_ffffff' ] );<br />
// ... and add the good one<br />
dojo.addClass( node, 'tokencolor_'+targetColor );<br />
<br />
} <br />
} ),<br />
dojo.fadeIn( { node: 'token_'+token.x+'_'+token.y } )<br />
<br />
] ); // end of dojo.fx.chain<br />
<br />
// ... and launch the animation<br />
anim.play(); <br />
}<br />
},<br />
</pre><br />
<br />
The list of the discs to be turned over has been made available by our server side code in "notif.args.turnedOver" (see previous paragraph). We loop through all these discs, and create a complex animation using dojo.Animation for each of them. The complete documentation on dojo animations [http://dojotoolkit.org/documentation/tutorials/1.6/animation/ can be found here].<br />
<br />
In few words: we create a chain of 4 animations to make the disc fade out, fade in, fade out again, and fade in again. At the end of the second fade out, we change the color of the disc. Finally, we launch the animation with "play()".</div>Wucnuchttps://en.doc.boardgamearena.com/index.php?title=Game_art:_img_directory&diff=1417Game art: img directory2015-04-04T04:13:05Z<p>Wucnuc: /* Requested images */</p>
<hr />
<div>== Requested images ==<br />
<br />
The following images are requested by BGA:<br />
<br />
;game_box.png<br />
* It is displayed on the main site on the game description page and when creating a table (280x280 px).<br />
* It should be a 3D image of a physical copy of the game box as it appears in an online shop.<br />
* It is better to take the version of the game that is coherent with the game art used in the adaptation, and from the original publisher of the game.<br />
* The background of the image must be transparent.<br />
* If you don't have a 3D version of the game box, you can use the following website to create one: http://www.3d-pack.com/<br />
<br />
;game_icon.png<br />
<br />
* It is the icon displayed in the lists of games and tables (50x50 px).<br />
* This one should not be transparent, and shouldn't have a border (a black border will be add by BGA).<br />
* The objective of this icon is to make the game recognizable among the other games. A good idea is to take a part of the game cover that is distinctive (ex: the game title).<br />
<br />
;publisher.png<br />
* It is the logo of the publisher of the game, displayed on the game description page.<br />
* The width must be 150 px. The height can be anything. The image could be transparent.<br />
<br />
;publisher2.png (optional)<br />
* If the game has been co-published by 2 publishers, you should upload a second image named "publisher2.png" (same characteristics as the first one).<br />
<br />
<br />
<br />
'''Important''': when you modify these images, you MUST click on "Reload game box image" from the Control Panel in order your update can be taken into account.<br />
<br />
== Game art ==<br />
<br />
You must upload in img directory all images of your game interface.<br />
<br />
=== Images loading ===<br />
<br />
'''Be careful''': by default, ALL images of your img directory are loaded on a player's browser when he loads the game. For this reason, don't let in your img directory images that are not useful, otherwise it's going to slowdown the game load.<br />
<br />
Note that you can tune the way images are loaded with Javascript method "dontPreloadImage" (see [[Game_interface_logic:_yourgamename.js|Game Interface Logic]]).<br />
<br />
=== Images format ===<br />
<br />
You can use 3 image format while building your game interface:<br />
;jpg images<br />
<br />
should be used for non-transparent images. Jpg are usually lighter than Pngs, so please choose Jpg for big pictures (ex: game board, cards) when you don't need transparency to accelerate game load.<br />
<br />
;png images<br />
<br />
should be used for transparent images.<br />
<br />
;gif images<br />
<br />
can be used for animated images. This is not recommended to use gif animated images as they can upset players, but for some specific interface element this could be useful.<br />
<br />
=== Use CSS Sprites ===<br />
<br />
To limit the number of images load and make the game load faster, you must use CSS sprites, ie you must gather several images in a single one.<br />
<br />
To learn more on CSS Sprites:<br />
* [http://www.w3schools.com/css/css_image_sprites.asp CSS sprites (W3C documentation)].<br />
* [[Game interface stylesheet: yourgamename.css]]</div>Wucnuc