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

Tutorial hearts: Difference between revisions

From Board Game Arena
Jump to navigation Jump to search
No edit summary
m (Address two issues in actPlayCard (duplicate implementation with template provided onCardClick handler and card_id argument for actPlayCard))
(69 intermediate revisions by 24 users not shown)
Line 1: Line 1:
{{Studio_Framework_Navigation}}
== Introduction ==
== Introduction ==


Line 7: Line 9:
* Know the rules for Hearts
* Know the rules for Hearts
* Some-what know the languages used on BGA: PHP, SQL, HTML, CSS, Javascript
* Some-what know the languages used on BGA: PHP, SQL, HTML, CSS, Javascript
* Setup you development environment [http://en.doc.boardgamearena.com/First_steps_with_BGA_Studio First Steps with BGA Studio]
* Set up your development environment [http://en.doc.boardgamearena.com/First_steps_with_BGA_Studio First Steps with BGA Studio]
* As part of setup you have to have access to your ftp home folder in studio, which would have 'hearts' game source code. We will be using some resources of this game in this tutorial, so copy it over to local disk if you have not done so.
* As part of setup you have to have access to your ftp home folder in studio, which would have the full 'hearts' game source code. We will be using some resources of this game in this tutorial, so copy it over to local disk if you have not done so.


If you stuck of have question about this tutorial post on [https://forum.boardgamearena.com/viewforum.php?f=12 BGA Developers forum]
If you are stuck or have question about this tutorial, post on [https://forum.boardgamearena.com/viewforum.php?f=12 BGA Developers forum]


== Create your first game ==
== Create your first game ==


If you have not already, you have to create a project in BGA Studio. For this tutorial you can create a project heartsYOUNAME where
If you have not already, you have to create a project in BGA Studio. For this tutorial you can create a project heartsYOURNAME where
YOUNAME is your developer login name. You can also re-use the project you have created for "First Steps" tutorial above.
YOURNAME is your developer login name. You can also re-use the project you have created for the "First Steps" tutorial above.
With the initial skeleton of code provided initially, you can already start a game from the BGA Studio.  
With the initial skeleton of code provided, you can already start a game from the BGA Studio.  


Find and start the game in turn based mode, make sure it works.
1. Find and express start the game in turn-based mode with 4 players. Make sure it works. If you want to see the game as 2nd player press red arrow button on the player panel to switch to that player. More details can be found in [[First_steps_with_BGA_Studio]]


Second modify the text in heartsYOUNAME_heartsYOUNAME.tpl, reload the page in the browser and make sure your ftp sync works as expected.
2. Modify the text in heartsYOURNAME_heartsYOURNAME.tpl, reload the page in the browser and make sure your ftp sync works as expected.
Note: if you have not setup auto-sync do it now, manually copying files is a no-starter.
Note: if you have not setup auto-sync do it now, manually copying files is a no-starter.
3. Express stop from settings menu (the gear icon).
<i>Note: please do '''not''' use the hearts project code as a base. This tutorial assumes you started with a TEMPLATE project with no prior modifications. Using the hearts project as a base will be very confusing and you won't be able to follow all the steps.
</i>
<b>Attention!!!</b> Very important note about reloading, if you don't remember this you may spend hours debugging. The browser caches images. If you change any of these files, you have to do "full reload" which is usually Ctrl+F5 (or Ctrl+reload button on browser) not just a regular reload.


== Hook version control system ==
== Hook version control system ==


If its a real game or even for this tutorial I would commit the code to version control right at start. You going to find yourself in the situation
For a real game, or even for this tutorial, we recommend committing the code to version control right from the start. You are going to find yourself in a situation where the game doesn't even start anymore and no way of debugging it, unless you have a way to revert. That is where version control becomes very handy. If you are not familiar with version control (e.g. [https://git-scm.com/docs/gittutorial git]) then at least back up your files after each major change. Start now.
when game does not even start anymore and no way of debugging it unless you have a way to revert. That is where version control becomes very handy.
If you don't know what I am talking about then at least back-up your files after each of major steps. Starting now.


Code for this tutorial available on github at https://github.com/elaskavaia/bga-heartsla
Code for this tutorial available is on github: https://github.com/elaskavaia/bga-heartsla


Different revisions represent different steps along the process, starting from original template to a complete game.
Different revisions represent different steps along the process, starting from original template to a PARTIAL game. The full game can be found in your FTP home folder.


== Update game infos and box graphics ==
== Update game infos and box graphics ==


Even it does not nothing yet I always start with making sure game looks descent in the game selector, meaning it has nice box graphics and information is correct. For that we need to edit [[Game_meta-information: gameinfos.inc.php|gameinfos.inc.php]].
Even it does nothing yet, always start by making sure the game looks decent in the game selector, meaning it has nice box graphics and its information is correct. For that we need to edit [[Game_meta-information: gameinfos.inc.php|gameinfos.inc.php]].
What you would do for real game you would go to http://boardgamegeek.com find the game and use the information from web-site to fill the gameinfos.
 
So lets do that. Find "hearts" on boardgamegeek. Original release 1850 :)
For a real game, you would go to [http://boardgamegeek.com BoardGameGeek], find the game, and use the information from BGG to fill in the gameinfos.
You can fill in year of publishing, bgg id, you can put Public Domain as publisher and publisher id is 171 for public domain. And as designer and author you can just put your own name just for fun. Set number of players to 4.
 
So let's do that. Find "hearts" on BoardGameGeek. (Hint: Original release 1850 :))
 
You can fill in the year of publishing and bgg id, put ''Public Domain'' under publisher (for a real game, leave an empty string so it won't be displayed), and a publisher id of 171 for public domain. And as designer and author you can just put your own name just for fun. Set number of players to 4.


   // Players configuration that can be played (ex: 2 to 4 players)
   // Players configuration that can be played (ex: 2 to 4 players)
   'players' => array( 4 ),   
   'players' => array( 4 ),   


The next step is to replace game_box.png with nicer images. For this tutorial just copy all files from img/ folder of hearts/ template into img/ directory of your project. And replace publisher.png with nicer image for example https://github.com/elaskavaia/bga-sharedcode/blob/master/img/publisher.png.
'''Important step''': you have to refresh the information in the Studio website through the control panel. So go to Control Panel -> Manage Games -> heartsYOURNAME
and press Reload for 'Reload game informations'.
 


Details about images can be found here: [[Game art: img directory]].


Now important step. You have to LOAD these files in studio website through control panel. So go to Control Panel -> Manager Games -> heartsYOUNAME
The next step would be to replace '''game_box.png''' with nicer images. This can be done from the [[Game metadata manager]].
and press Reload for 'Reload game informations' and 'Reload game box image'


Now try to start the game again. If you some-how introduced a syntax error in gameinfos file it may not actually work (game won't start).
Now try to start the game again. If you somehow introduced a syntax error in the gameinfos file it may not work (the game won't start).
Always use "Express Start" button to start the game. You should see a standard state prompt from template. You should see 4 players on the right, testdude0 .. testdude3.
Always use the "Express Start" button to start the game. You should see a standard state prompt from the template. You should see 4 players on the right: testdude0 .. testdude3.
To switch between them press the red arrow button near their names, it will open another tab. This way you don't need to login and logout from multiple accounts!
To switch between them press the red arrow button near their names, it will open another tab. This way you don't need to login and logout from multiple accounts!
<i>Note: if you had run the game before with less than 4 players there is a bug that will prevent you from running it with 4 only (if you did not run it before or run it with 4 players as instructed stop reading this note), to workaround revert back to original players array (i.e. 1,2,3,4), reload game options, then create a table with 4 players, exit that game table, then change gameoptions to 4 only as above, reload game options, create table again.</i>


Code Rev [https://github.com/elaskavaia/bga-heartsla/tree/4b3a73eeb5acae961ade18473af119e8ce8d1a8f]
Code Rev [https://github.com/elaskavaia/bga-heartsla/tree/4b3a73eeb5acae961ade18473af119e8ce8d1a8f]
Line 60: Line 73:
In this section we will do graphics of the game, and main layout of the game.
In this section we will do graphics of the game, and main layout of the game.


First copy a sprite with cards image from hearts img/cards.jpg  into img/ folder of your project.
First copy a sprite with cards image from [https://github.com/elaskavaia/bga-heartsla/blob/4b3a73eeb5acae961ade18473af119e8ce8d1a8f/img/cards.jpg hearts img/cards.jpg] into img/ folder of your project. Project hearts is mounted to your home directory on bga server.
<blockquote>There are better quality deck of cards image at https://en.doc.boardgamearena.com/Common_board_game_elements_image_resources</blockquote>
 
Details about images can be found here: [[Game art: img directory]].


Edit .tpl to add some divs to represent player table and hand area
Edit .tpl to add some divs to represent player table and hand area
Line 98: Line 114:
</pre>
</pre>


When you edit CSS remember that you have to FORCE-reload page, i.e. Ctrl-F5, otherwise its cached.
When you change existing graphics files remember that you have to FORCE-reload page, i.e. Ctrl-F5, otherwise its cached.
<i>Same when you change existing graphics files</i>.


You should see this:
You should see this:
Line 109: Line 124:
There are few ways of how html could have been generated, you could have start with nothing and generate
There are few ways of how html could have been generated, you could have start with nothing and generate
all by java script. Or you could have started with complete game markup in html and make java script just hide and move pieces around. BGA framework provides also a third way which is mix of both plus template engine to generate HTML using php. So lets do that.
all by java script. Or you could have started with complete game markup in html and make java script just hide and move pieces around. BGA framework provides also a third way which is mix of both plus template engine to generate HTML using php. So lets do that.
Note: generating blocks in js only is probably simpler now with template strings.


Change .tpl file to have this inside
Change .tpl file to have this inside
Line 114: Line 131:
<div id="playertables">
<div id="playertables">


     <!-- BEGIN player -->
     <!-- BEGIN playerhandblock -->
     <div class="playertable whiteblock playertable_{DIR}">
     <div class="playertable whiteblock playertable_{DIR}">
         <div class="playertablename" style="color:#{PLAYER_COLOR}">
         <div class="playertablename" style="color:#{PLAYER_COLOR}">
Line 122: Line 139:
         </div>
         </div>
     </div>
     </div>
     <!-- END player -->
     <!-- END playerhandblock -->


</div>
</div>
Line 129: Line 146:
     <h3>{MY_HAND}</h3>
     <h3>{MY_HAND}</h3>
     <div id="myhand">
     <div id="myhand">
      <div class="playertablecard"></div>
     </div>
     </div>
</div>
</div>
</pre>
</pre>


What we did is we added "block" player, it is marked up using html comments. {VAR} notation is used
What we did is we added "block" playerhandblock, it is marked up using html comments. {VAR} notation is used
to inject variables and  
to inject variables and  
<pre>
<pre>
Line 142: Line 160:
effectively allows us to do template loops.
effectively allows us to do template loops.


In .view.php insert this code after 'Place your code below' comment
In .view.php, fill in the implementation of <code>build_page</code> like so:
 
 
<pre>
<pre>
         $template = self::getGameName() . "_" . self::getGameName();
    public function build_page($viewArgs)
    {
        // Get players
        $players = $this->game->loadPlayersBasicInfos();
       
         $template = $this->getGameName() . "_" . $this->getGameName();
          
          
         $directions = array( 'S', 'W', 'N', 'E' );
         $directions = array( 'S', 'W', 'N', 'E' );
          
          
         // this will inflate our player block with actual players data
         // this will inflate our player block with actual players data
         $this->page->begin_block($template, "player");
         $this->page->begin_block($template, "playerhandblock");
         foreach ( $players as $player_id => $info ) {
         foreach ( $players as $player_id => $info ) {
             $dir = array_shift($directions);
             $dir = array_shift($directions);
             $this->page->insert_block("player", array ("PLAYER_ID" => $player_id,
             $this->page->insert_block("playerhandblock", array ("PLAYER_ID" => $player_id,
                     "PLAYER_NAME" => $players [$player_id] ['player_name'],
                     "PLAYER_NAME" => $players [$player_id] ['player_name'],
                     "PLAYER_COLOR" => $players [$player_id] ['player_color'],
                     "PLAYER_COLOR" => $players [$player_id] ['player_color'],
Line 160: Line 181:
         }
         }
         // this will make our My Hand text translatable
         // this will make our My Hand text translatable
         $this->tpl['MY_HAND'] = self::_("My hand");
         $this->tpl['MY_HAND'] = $this->_("My hand");
    }
</pre>
</pre>


What it does is for each player we have it will replicate the html between <!-- BEGIN player --> and <!-- END player --> tags, substituting the variable denoted by {XXX}
What it does is for each player we have it will replicate the html between <!-- BEGIN playerhandblock --> and <!-- END playerhandblock --> tags, substituting the variable denoted by {XXX}
with the values you provide. The DIR variable in this case we pulling from directions array (where array_shift will take first element and remove it from the array).
with the values you provide. The DIR variable in this case we pulling from directions array (where array_shift will take first element and remove it from the array).


Reload. If everything went well you should see this:
Reload. If everything went well you should see this:


[[File:Heartsla-tpl4.png]]
[[File:Hearts all rows.png|alt=Display a list of all players, with a dummy card in each position]]


These are "tableau" areas for 4 players plus My hand visible only to one player.
These are "tableau" areas for 4 players plus My hand visible only to one player. They are not exactly how we wanted them to be because we did not edit .css yet.
They not exactly how we wanted them to be because we did not edit .css yet.


Now edit .css, add these lines after import before our previous definition
Now edit .css, add these lines after import before our previous definition
Line 222: Line 243:


This is almost all we need for graphics and layout, there are few tweaks left there but lets do some more heavy lifting now.
This is almost all we need for graphics and layout, there are few tweaks left there but lets do some more heavy lifting now.
<i>Note: if you did not see changes you may have not force reloaded, force means you use Ctrl+F5 or Cltr+Shift-R, if you don't "force" browser will use cached version of images! Which is not what you just changed</i>
<i>Another Note: In general if you have auto-sync you don't need to reload if you change game.php file, you need normal reload if you change js, and force reload for images. If you changed state machine or database you likely need to restart the game.</i>


== Game Interface JS Stock ==
== Game Interface JS Stock ==


BGA framework provides few out of the box classes to deal with cards. The client side
The BGA framework provides a few out of the box classes to deal with cards. The client side
contains class called [[Stock]] and it can be used for any dynamic html 'pieces' management that uses
contains a class called [[Stock]] and it can be used for any dynamic html "pieces" management that uses
common sprite image. On the server side we will use Deck class which we discuss later.
common sprite images. On the server side we will use the [[Deck]] class which we discuss later.


If you open cards.jpg in an image viewer you can see that it is a "sprite" image - 13x4 grid of images stitched together,
If you open cards.jpg in an image viewer you will see that it is a "sprite" image - a 13x4 grid of images stitched together,
which is very efficient way to transport images. So we will use Stock class to mark up these images and create
which is a very efficient way to transport images. So we will use the Stock class to mark up these images and create
"card" divs for us.
"card" divs for us.


At first, we need to add "ebg/stock" as a dependency in the hearts.js file:
First, we need to add '''ebg/stock''' as a dependency in the hearts.js file:
<pre>
<pre>
define([
define([
Line 243: Line 269:
</pre>
</pre>


Then add this to js contructor, this will define size of our cards
Then add this to the Javascript constructor, this will define size of our cards
<pre>
<pre>
             console.log('hearts constructor');
             console.log('hearts constructor');
Line 250: Line 276:
</pre>
</pre>


The stock is initialized in Javascript "setup" method like this:
The stock is initialized in the Javascript "setup" method like this:
<pre>
<pre>
     // TODO: Set up your game interface here, according to "gamedatas"
     // TODO: Set up your game interface here, according to "gamedatas"
Line 259: Line 285:
</pre>
</pre>


As parameters of the "create" method, we provided the width/height of an item (a card), and the container div "myhand" - which is an id of "div" element from our .tpl file representing player hand.
As parameters of the "create" method, we provided the width/height of an item (a card), and the container div "myhand" - which is an id of "div" element from our .tpl file representing a player hand.




Then, we must tell the stock what are the items it is going to display during its life: the 52 cards of a standard card game from "CSS sprite" image named "cards.jpg" with all the cards arranged in 4 rows and 13 columns.
Then, we must tell the stock what items it is going to display during its life: the 52 cards of a standard card game from a CSS sprite image named "cards.jpg" with all the cards arranged in 4 rows and 13 columns.


Here's how we tell stock what are the items type to display:
Here's how we tell stock what item types to display:
<pre>
<pre>
             this.playerHand.image_items_per_row = 13; // 13 images per row
             this.playerHand.image_items_per_row = 13; // 13 images per row
Line 279: Line 305:
</pre>
</pre>


And add this function to utilities section
And add this function to the utilities section
<pre>
<pre>
         // Get card unique identifier based on its color and value
         // Get card unique identifier based on its color and value
Line 289: Line 315:
Explanations:
Explanations:
* At first, we tell the stock component that our CSS sprite contains 13 items per row. This way, it can find the correct image for each card type id.
* At first, we tell the stock component that our CSS sprite contains 13 items per row. This way, it can find the correct image for each card type id.
* Then for the 4x13 cards, we call "addItemType" method that create the type. The arguments are the type id, the weight of the card (for sorting purpose), the URL of our CSS sprite, and the position of our card image in the CSS sprite. It happens to be the same number in our case.
* Then for the 4x13 cards, we call the '''addItemType''' method that creates the type. The arguments are the type id, the weight of the card (for sorting purpose), the URL of our CSS sprite, and the position of our card image in the CSS sprite. It happens to be the same number in our case.


Note: we need to generate a unique ID for each type of card based on its color and value.  For that we create a function "getCardUniqueId". The type is unique identification of the type of the card i.e. queen of spades encoded in the integer, if our deck have 2 standard card decks we would have had 2 queen of spades, they would shared same type, same image but will have different ids. The type of the item either should be reversible function of its properties (i.e. kind of suite * 13 + value) or just enumerator described in material.inc.php. In this specific case its a synthetic type ID, which also same as number of the card in the sprite image (i.e. if you enumerate each image in sprite going left to right, then top to bottom).
Note: we need to generate a unique ID for each type of card based on its color and value.  For that we create a function '''getCardUniqueId'''. The type is the unique identifier of the TYPE of the card, e.g., the queen of spades encoded as an integer. If our deck had 2 standard card decks we would have had 2 queens of spades; they would share the same type and the same image but would have different ids. NOTE: It's unfortunate that they named this '''getCardUniqueId'''; it should have been '''getCardUniqueType''', because it really isn't an id, but a TYPE of card. The type of the item should either be a reversible function of its properties (i.e., kind of suite * 13 + value) or just an enumerator described in material.inc.php. In this specific case it's a synthetic type id, which also the same as the number of the card in the sprite image (i.e., if you enumerate each image in sprite going left to right, then top to bottom).


Now lets add the 5 of Heart to player's hand just for fun (this code will go in setup method after types initialization):
Now let's add the 5 of Hearts to the player's hand just for fun (this code will go in setup method after types initialization):


<pre>
<pre>
// 2 - hears, 5 is 5, and 42 is card id, it normally would come from db
// 2 = hearts, 5 is 5, and 42 is the card id, which normally would come from db
this.playerHand.addToStockWithId( this.getCardUniqueId( 2, 5 ), 42 );
this.playerHand.addToStockWithId( this.getCardUniqueId( 2, 5 ), 42 );
</pre>
</pre>


This will add card with id 42 and type 16 ( (2-1)*13+(5-2)=16 ).  
This will add the card with id 42 and type 16 ( (2-1)*13+(5-2)=16 ).  


Note that number 16 would be any anything you can see in database, Deck database will have separate field for type and type_arg where type is suite and type_arg is number, so its not the same thing, but you can use same formula to convert. But we get to database in the later section.
Note that number 16 would not be something you can see in database, Deck database will have separate field for type and type_arg where type is suite and type_arg is number, so its not the same thing, but you can use same formula to convert. Number 42 on the other hand would be id field in database. But we get to database in the later section.


If you reload now you should see 5 of hearts in "your hand".
If you reload now you should see the 5 of hearts in "your hand".


Stock control can handle clicking on items and forms the selection, you can immediately react to selection
Stock control can handle clicking on items and forms the selection. You can immediately react to selection
or you can query it later, for example when user presses some other button.
or you can query it later; for example when user presses some other button.


Lets hook it up, add this in setup method, after this.playerHand is initialised
Let's hook it up. Add this in the setup method in .js file, after this.playerHand is initialised:


     dojo.connect( this.playerHand, 'onChangeSelection', this, 'onPlayerHandSelectionChanged' );
     dojo.connect( this.playerHand, 'onChangeSelection', this, 'onPlayerHandSelectionChanged' );




Then find Player's action comment section and add handler after the comment
Then find the Player's action comment section in .js file and add a handler after the comment:
 
<pre>
<pre>
         onPlayerHandSelectionChanged : function() {
         onPlayerHandSelectionChanged: function() {
             var items = this.playerHand.getSelectedItems();
             var items = this.playerHand.getSelectedItems();


             if (items.length > 0) {
             if (items.length > 0) {
                 if (this.checkAction('playCard', true)) {
                 if (this.checkAction('actPlayCard', true)) {
                     // Can play a card
                     // Can play a card


Line 327: Line 354:


                     this.playerHand.unselectAll();
                     this.playerHand.unselectAll();
                 } else if (this.checkAction('giveCards')) {
                 } else if (this.checkAction('actGiveCards')) {
                     // Can give cards => let the player select some cards
                     // Can give cards => let the player select some cards
                 } else {
                 } else {
Line 336: Line 363:
</pre>
</pre>


Now if you reload, open js Console, click on card in My Hand and you should see  
''Note : If you already have an example handler <code>onCardClick: function( card_id )</code>, go ahead and remove that handler, it is a duplicate of what we are implementing in this tutorial and may cause issues later on.''
 
The function name of the handler is 4th parameter of the dojo.connect function. Make sure you spell it correctly or there will be unpredictable effects.
 
Now if you reload, open the Javascript Console (F12), and then click on the card in My Hand, you should see:
   on playCard 42
   on playCard 42
printed on the console
printed on the console
''Note : You need to be the active player to have rights to play a card and so log your message in the console''


== Game Database and Game Initialisation ==
== Game Database and Game Initialisation ==


Next step you want to design game database and setup new game (on server side).
Next step, you want to design a game database and setup a new game (on the server side).
For that we need to a) modify database schema to add our cards data b) add some global "variables" into
For that we need to a) modify the database schema to add our cards data b) add some global variables into
existing globals table.
the existing globals table.


To modify schema first exit you existing game(s). Open dbmodel.sql file and uncomment card table creation
To modify the schema, first exit your existing game(s). Open '''dbmodel.sql''' file and uncomment the card table creation.


<pre>
<pre>
Line 359: Line 392:
</pre>
</pre>


This is "card" table which would be managed by Deck php class.
This is the "card" table which will be managed by the Deck php class.


In addition we want a little piece of information in the players table:
In addition we want a little piece of information in the players table:
Line 366: Line 399:
   ALTER TABLE `player` ADD `player_first` BOOLEAN NOT NULL DEFAULT '0';
   ALTER TABLE `player` ADD `player_first` BOOLEAN NOT NULL DEFAULT '0';


Not sure why they put this into player table, we could have global db variable to hold first player as easily.
Not sure why they put this into the player table, as we could use a global db variable to hold first player as easily.
But I am just following existing code more-less.
But I am just following the existing code more-or-less.


Next we finally get into .game.php class, where the main logic and db interaction would be. Find php constructor which should be  
Next we finally get into .game.php class, where the main logic and db interaction would be. Find php constructor which should be  
   function __construct( )
   function __construct( )
This is first function in a file.
This is first function in a file. Add this code to constructor.
<pre>
<pre>
         parent::__construct();
         parent::__construct();
         self::initGameStateLabels( array(  
         $this->initGameStateLabels( array(  
                         "currentHandType" => 10,  
                         "currentHandType" => 10,  
                         "trickColor" => 11,  
                         "trickColor" => 11,  
Line 380: Line 413:
                           ) );
                           ) );


         $this->cards = self::getNew( "module.common.deck" );
         $this->cards = $this->getNew( "module.common.deck" );
         $this->cards->init( "card" );
         $this->cards->init( "card" );
</pre>
</pre>


Here we are initializing three "Game State Variables" which are variable stored in database. They are integers.
Here we are initializing three "Game State Variables" which are variables stored in the database. They are integers.
It must start with no lower then 10 since the others ones are reserved. These values are stored by numeric id's
It must start with values higher or equal to 10 since values lower than 10 are reserved. These values are stored by numeric ids
in the database, but in the php we associate them with string labels for convenience of access. The variables are "trickColor" - numbers from 1 to 4 that map to card suit (not sure why its called color maybe its translation from french); "alreadyPlayedHearts" - is boolean flag (well 0 or 1) indication either somebody use hearts on the trick;  "currentHandType" - stores the value to indicate who to give cards during exchange.
in the database, but in the php we associate them with string labels for convenience of access.  
 
The variables are:


Next 2 lines are creating $this->cards object and associating it with "card" table in the the database.
* "trickColor": numbers from 1 to 4 that map to card suit (not sure why it's called color; maybe it's a translation from French);
* "alreadyPlayedHearts": a boolean flag (0 or 1) indicating whether somebody used hearts on the trick;
* "currentHandType": stores the value to indicate who to give cards to during exchange.
 
The next 2 lines are creating $this->cards object and associating it with "card" table in the the database.


<i>If we called db table 'foo' instead of 'card' the last statement would have been  $this->cards->init( "foo" )</i>
<i>If we called db table 'foo' instead of 'card' the last statement would have been  $this->cards->init( "foo" )</i>




Line 398: Line 436:
<i>
<i>
If you made a mistake
If you made a mistake
in .sql or php constructor game won't start and good luck debugging it (that is why it important to check
in the .sql or php constructor the game won't start, and good luck debugging it. (That is why it's important to check
once in a while to make sure it still starts while you remember what you have changed)
once in a while to make sure it still starts while you remember what you have changed.)
</i>
</i>


Code Rev [https://github.com/elaskavaia/bga-heartsla/tree/e3a049257b592ff6167688d4d344f8a83d349b08]
Code Rev [https://github.com/elaskavaia/bga-heartsla/tree/e3a049257b592ff6167688d4d344f8a83d349b08]


Now we can go to game initialization setupNewGame, this method is called once when table is created.
Now we can go to game initialization '''setupNewGame''' in game.php. This method is called only once when the game is created.


In your template you should have code that deals with player table, just leave it as is. Start inserting the
In your template project you should have code that deals with player table, just leave it as is. Start inserting the
other code after "Start the game initialization" comment
other code after "Start the game initialization" comment.
<pre>
<pre>
         // Init global values with their initial values
         // Init global values with their initial values
Line 413: Line 451:
         // Note: hand types: 0 = give 3 cards to player on the left
         // Note: hand types: 0 = give 3 cards to player on the left
         //                  1 = give 3 cards to player on the right
         //                  1 = give 3 cards to player on the right
         //                  2 = give 3 cards to player on tthe front
         //                  2 = give 3 cards to player opposite
         //                  3 = keep cards
         //                  3 = keep cards
         self::setGameStateInitialValue( 'currentHandType', 0 );
         $this->setGameStateInitialValue( 'currentHandType', 0 );
          
          
         // Set current trick color to zero (= no trick color)
         // Set current trick color to zero (= no trick color)
         self::setGameStateInitialValue( 'trickColor', 0 );
         $this->setGameStateInitialValue( 'trickColor', 0 );
          
          
         // Mark if we already played some heart during this hand
         // Mark if we already played hearts during this hand
         self::setGameStateInitialValue( 'alreadyPlayedHearts', 0 );
         $this->setGameStateInitialValue( 'alreadyPlayedHearts', 0 );
</pre>
</pre>


Here we initialized all the globals to 0.
Here we initialize all the globals to 0.


Next is to create our cards in the database. We have one deck of cards so its pretty simple
Next is to create our cards in the database. We have one deck of cards so it's pretty simple.
<pre>
<pre>
         // Create cards
         // Create cards
Line 442: Line 480:


This code that will create one of each card. But don't run it yet, because we missing $this->colors.
This code that will create one of each card. But don't run it yet, because we missing $this->colors.
So we have state of the game in the database, but there is some static game information which never changes,
So we have state of the game in the database, but there is some static game information which never changes.
this information should be stored in material.inc.php and this way it can be accessed from all .php files.
This information should be stored in material.inc.php and this way it can be accessed from all .php files (and .js if you send it via getAllDatas()).
We will edit this file now by adding these lines
We will edit this file now by adding these lines


Line 449: Line 487:
$this->colors = array(
$this->colors = array(
     1 => array( 'name' => clienttranslate('spade'),
     1 => array( 'name' => clienttranslate('spade'),
                 'nametr' => self::_('spade') ),
                 'nametr' => $this->_('spade') ),
     2 => array( 'name' => clienttranslate('heart'),
     2 => array( 'name' => clienttranslate('heart'),
                 'nametr' => self::_('heart') ),
                 'nametr' => $this->_('heart') ),
     3 => array( 'name' => clienttranslate('club'),
     3 => array( 'name' => clienttranslate('club'),
                 'nametr' => self::_('club') ),
                 'nametr' => $this->_('club') ),
     4 => array( 'name' => clienttranslate('diamond'),
     4 => array( 'name' => clienttranslate('diamond'),
                 'nametr' => self::_('diamond') )
                 'nametr' => $this->_('diamond') )
);
);


Line 475: Line 513:
</pre>
</pre>


Where $this->colors will define Suit labels and $this->values_label defines value labels.
Where $this->colors will define Suit labels and $this->values_label will define value labels.
If you noticed we have two of each label for suits. This is because we need sometimes translated values on php
If you noticed, we have two of each label for suits. This is because sometimes we need translated values on the php
side and sometimes we don't. In this case nametr will return a translated value right in php, which is only usefull when you throw exceptions to show right strings. If you passing value to client via notification you should always
side and sometimes we don't. In this case '''nametr''' will return a translated value in php, which is only useful when you throw exceptions to show the right strings. If you pass a value to the client via notification you should always use untranslated strings, and the client will translate it. 'clienttranslate' marks the value for translation but does not actually change it for php. For more about this wonderful translation stuff see [[Translations]].
use untranslated strings, and client will translate it. 'clienttranslate' marks the value for translation but does not actually change it for php. For more about this wonderful translation stuff see [[Translations]] section.


== Full game model synchronisation ==
== Full game model synchronisation ==


 
Now at any point in the game we need to make sure that database information can be reflected back in the UI, so we must fix the '''getAllDatas''' function
Now at any point in the game we need to make sure that database information can be reflected back in UI, so we fix getAllDatas function
to return all possible data we need to reconstruct the game. This is in the game.php file. The template for getAllDatas() already takes care of player info. Let's just
to return all possible data we need to reconstruct the game. This is in the game.php file. The template for getAllDatas already taking care of player info, lets just
add hand and tableau data before we return a result.
add hand and tableau data before we return result.


<pre>
<pre>
Line 495: Line 531:
</pre>
</pre>


 
Now on the client side we should display this data, so in your .js file in the setup function (which is the receiver of getAllDatas) replace our hack of putting 5 of Hearts directly into the hand with:
Now on the client side we should display this data, so in your .js file in setup function (which is the receiver of getAllDatas) add replace our hack of putting hearts of 5 directly into hand with:


<pre>
<pre>
Line 517: Line 552:
</pre>
</pre>


This should show hand and tableau cards now, except we are missing playCardOnTable function. So find getCardUniqueId function which
This should show hand and tableau cards now, except we are missing the '''playCardOnTable''' function. So find the '''getCardUniqueId''' function which
should be in utilities section and add this after
should be in the utilities section and add this after it:


<pre>
<pre>
         playCardOnTable : function(player_id, color, value, card_id) {
         playCardOnTable : function(player_id, color, value, card_id) {
             // player_id => direction
             // player_id => direction
             dojo.place(this.format_block('jstpl_cardontable', {
             this.addTableCard(value, color, player_id, player_id);
                x : this.cardwidth * (value - 2),
                y : this.cardheight * (color - 1),
                player_id : player_id
            }), 'playertablecard_' + player_id);


             if (player_id != this.player_id) {
             if (player_id != this.player_id) {
Line 548: Line 579:
</pre>
</pre>


For that to work we also need to add card temple in .tpl file
For this to work we also need to define the addTableCard
<pre>
<pre>
// Javascript HTML templates
        addTableCard(value, color, card_player_id, playerTableId) {
 
            const x = value - 2;
var jstpl_cardontable = '<div class="cardontable" id="cardontable_${player_id}" style="background-position:-${x}px -${y}px">\
            const y = color - 1;
                        </div>';
            document.getElementById('playertablecard_' + playerTableId).insertAdjacentHTML('beforeend', `
                <div class="card cardontable" id="cardontable_${card_player_id}" style="background-position:-${x}00% -${y}00%"></div>
            `);
        },
</pre>
</pre>




What this does is basically it creates another card object, because if it is not our card its not in our hand (stock) so
What this does is basically create another card object, because if it is not our card it's not in our hand (Stock) so
we have to create it out of thin air. Technique to do that is to implode a js template object defined in .tpl file with some
we have to create it out of thin air. The technique to do that is to implement a Javascript template object defined in the .tpl file with some
parameters, which will just basically create a "div" string (yes you could have used string concatenation but it would not be fancy).
parameters, which will basically create a "div" string (yes you could have used string concatenation but it would not be fancy).
Now dojo.place places it (the div) on top of placeholder. Now we have an object with id of 'cardontable_' + player_id, depending
Now dojo.place places it (the div) on top of a placeholder. Now we have an object with an id of 'cardontable_' + player_id. Depending
on who is playing it we either place it on player miniboard or in hand (and remove from hand stock). Then we animate the card move.
on who is playing it we either place it on the player miniboard or in hand (and remove it from hand stock). Then we animate the card move.


We also should fix our .css file now to add style for cardontable and remove background for playertablecard which really is a placeholder div and not a card.
We also should fix our .css file now to add style for cardontable and REMOVE background for playertablecard which really is a placeholder div and not a card. (Don't miss the remove step; it will be all screwy if you do!)


<pre>
<pre>
Line 585: Line 619:
</pre>
</pre>


Now to test that it actually works lets deal cards to players during game initialization:
Now to test that it actually works let's deal cards to players during game initialization:


Add this after createCards in setupNewGame function in the game.php file
Add this after createCards in setupNewGame function in the game.php file
Line 592: Line 626:
         $this->cards->shuffle('deck');
         $this->cards->shuffle('deck');
         // Deal 13 cards to each players
         // Deal 13 cards to each players
         $players = self::loadPlayersBasicInfos();
         $players = $this->loadPlayersBasicInfos();
         foreach ( $players as $player_id => $player ) {
         foreach ( $players as $player_id => $player ) {
             $cards = $this->cards->pickCards(13, 'deck', $player_id);
             $cards = $this->cards->pickCards(13, 'deck', $player_id);
Line 603: Line 637:


Find onPlayerHandSelectionChanged function in the JS file, we should have logging there like  console.log("on playCard "+card_id);
Find onPlayerHandSelectionChanged function in the JS file, we should have logging there like  console.log("on playCard "+card_id);
So after that insert this:
So after that insert this (Note: this code is for testing we will replace it with server interaction after we test it.):
<pre>
<pre>
                     console.log("on playCard "+card_id);
                     console.log("on playCard "+card_id);
Line 613: Line 647:
                     this.playCardOnTable(this.player_id,color,value,card_id);
                     this.playCardOnTable(this.player_id,color,value,card_id);
</pre>
</pre>
Note: this code is for testing we will replace it with server interaction after we test it.
Now if you force reload (because we changed .css before) you should be able to click on card from your hand and see it moving,
 
Now if you force reload (because we changed .css before) you should be able to click on card from you have and see it moving,
you can click on few cards this way. When you done enjoying the animation, press F5 to get your hand back.
you can click on few cards this way. When you done enjoying the animation, press F5 to get your hand back.


Line 624: Line 656:
== State Machine ==
== State Machine ==


Now we need to create a game state machine. So the states are:
Now we need to create a game state machine. So the states are (excluding two more states we will add later to handle the exchange of cards at the beginning of the rounds):


* Cards are dealt to all players (lets call it "newHand")
* Cards are dealt to all players (lets call it "newHand")
Line 631: Line 663:
* Game control is passed to next player or trick is ended ("nextPlayer")
* Game control is passed to next player or trick is ended ("nextPlayer")
* End of hand processing (scoring and check for end of game) ("nextHand")
* End of hand processing (scoring and check for end of game) ("nextHand")
In addition players can exchange cards so we need two more states for that but we will skip it for now.




Line 650: Line 680:
         "type" => "manager",
         "type" => "manager",
         "action" => "stGameSetup",
         "action" => "stGameSetup",
         "transitions" => array( "" => 20 )
         "transitions" => array( "" => 2 )
     ),
     ),
      
      
      
      
     /// New hand
     /// New hand
     20 => array(
     2 => array(
         "name" => "newHand",
         "name" => "newHand",
         "description" => "",
         "description" => "",
Line 664: Line 694:
     ),     
     ),     


    21 => array(     
        "name" => "giveCards",
        "description" => clienttranslate('Some players must choose 3 cards to give to ${direction}'),
        "descriptionmyturn" => clienttranslate('${you} must choose 3 cards to give to ${direction}'),
        "type" => "multipleactiveplayer",
        "action" => "stGiveCards",
        "args" => "argGiveCards",
        "possibleactions" => array( "actGiveCards" ),
        "transitions" => array( "giveCards" => 22, "skip" => 22 )       
    ),
   
    22 => array(
        "name" => "takeCards",
        "description" => "",
        "type" => "game",
        "action" => "stTakeCards",
        "transitions" => array( "startHand" => 30, "skip" => 30  )
    ),       
        
        
      
      
Line 680: Line 728:
         "descriptionmyturn" => clienttranslate('${you} must play a card'),
         "descriptionmyturn" => clienttranslate('${you} must play a card'),
         "type" => "activeplayer",
         "type" => "activeplayer",
         "possibleactions" => array( "playCard" ),
         "possibleactions" => array( "actPlayCard" ),
         "transitions" => array( "playCard" => 32 )
         "transitions" => array( "playCard" => 32 )
     ),  
     ),  
Line 698: Line 746:
         "type" => "game",
         "type" => "game",
         "action" => "stEndHand",
         "action" => "stEndHand",
         "transitions" => array( "nextHand" => 20, "endGame" => 99 )
         "transitions" => array( "nextHand" => 2, "endGame" => 99 )
     ),     
     ),     
    
    
Line 714: Line 762:
</pre>
</pre>


The full details about what these fields are you can find in [[Your_game_state_machine:_states.inc.php]].
The full details on these fields are in [[Your_game_state_machine:_states.inc.php]].
 
But basically we have Player states, in which human player has to perform an "action" by pressing some button in UI or selecting some game item, which will trigger js handler, which will do ajax call to server into API define
by .action.php file. All functions in this file are API between client and server and has very simple
and repetitive structure. In this case there is only two action player can do - play a card or pass cards to other player. So these 2 functions go into .action.php file, we will only define one now since we not implementing card passing states yet:
 
<pre>
    public function playCard() {
        self::setAjaxMode();
        $card_id = self::getArg("id", AT_posint, true);
        $this->game->playCard($card_id);
        self::ajaxResponse();
    }
</pre>


But basically we have Player states, in which a human player has to perform an "action" by pressing some button in the UI or selecting some game item, which will trigger js handler, which will do an ajax call to the server.


Now to make it run we have define all handler functions that we referenced in states, which are - one function for state arguments argGiveCards, 4 functions for robot states (where game performs some action)
To make it run we have to define all the handler functions that we referenced in states, which are - one function for state arguments argGiveCards, 4 functions for robot states (where the game performs some action)
and 1 function for player actions handling.
and 1 function for player actions handling.
Find 'Game state arguments' section and paste this in:
In .game.php, find 'Game state arguments' section and paste this in:
<pre>
<pre>
     function argGiveCards() {
     function argGiveCards() {
Line 739: Line 775:
</pre>
</pre>


This normally pass some parameters to states, but we don't need anything yet. It good to have placeholder there anyway, so we can fix it later.
This normally passes some parameters to states, but we don't need anything yet. It's good to have a placeholder there anyway, so we can fix it later.
Important: even when its a stub this function must return array not scalar.
Important: even when it's a stub, this function must return an array not a scalar.


Lets do stubs for other functions, find game state actions section in .game.php file and insert these
Let's do stubs for other functions, find the game state actions section in .game.php file and insert these
<pre>
<pre>
     function stNewHand() {
     function stNewHand() {
Line 750: Line 786:
         // Deal 13 cards to each players
         // Deal 13 cards to each players
         // Create deck, shuffle it and give 13 initial cards
         // Create deck, shuffle it and give 13 initial cards
         $players = self::loadPlayersBasicInfos();
         $players = $this->loadPlayersBasicInfos();
         foreach ( $players as $player_id => $player ) {
         foreach ( $players as $player_id => $player ) {
             $cards = $this->cards->pickCards(13, 'deck', $player_id);
             $cards = $this->cards->pickCards(13, 'deck', $player_id);
             // Notify player about his cards
             // Notify player about his cards
             self::notifyPlayer($player_id, 'newHand', '', array ('cards' => $cards ));
             $this->notifyPlayer($player_id, 'newHand', '', array ('cards' => $cards ));
         }
         }
         self::setGameStateValue('alreadyPlayedHearts', 0);
         $this->setGameStateValue('alreadyPlayedHearts', 0);
         $this->gamestate->nextState("");
         $this->gamestate->nextState("");
     }
     }
Line 763: Line 799:
         // New trick: active the player who wins the last trick, or the player who own the club-2 card
         // New trick: active the player who wins the last trick, or the player who own the club-2 card
         // Reset trick color to 0 (= no color)
         // Reset trick color to 0 (= no color)
         self::setGameStateInitialValue('trickColor', 0);
         $this->setGameStateInitialValue('trickColor', 0);
         $this->gamestate->nextState();
         $this->gamestate->nextState();
     }
     }
Line 772: Line 808:
             // This is the end of the trick
             // This is the end of the trick
             // Move all cards to "cardswon" of the given player
             // Move all cards to "cardswon" of the given player
             $best_value_player_id = self::activeNextPlayer(); // TODO figure out winner of trick
             $best_value_player_id = $this->activeNextPlayer(); // TODO figure out winner of trick
             $this->cards->moveAllCardsInLocation('cardsontable', 'cardswon', null, $best_value_player_id);
             $this->cards->moveAllCardsInLocation('cardsontable', 'cardswon', null, $best_value_player_id);
          
          
Line 785: Line 821:
             // Standard case (not the end of the trick)
             // Standard case (not the end of the trick)
             // => just active the next player
             // => just active the next player
             $player_id = self::activeNextPlayer();
             $player_id = $this->activeNextPlayer();
             self::giveExtraTime($player_id);
             $this->giveExtraTime($player_id);
             $this->gamestate->nextState('nextPlayer');
             $this->gamestate->nextState('nextPlayer');
         }
         }
Line 796: Line 832:


</pre>
</pre>
Important: All state actions game or player must end with state transition (or thrown exception). Also make sure its ONLY one state transition,
Important: All state actions game or player must end with state transition (or thrown exception). Also make sure it's ONLY one state transition,
if you accidentally fall though after state transition and do another one it will be a real mess and head scratching for long time.
if you accidentally fall through after state transition and do another one it will be a real mess and head scratching for long time.


Now find 'player actions' section and paste this code there
Now find 'player actions' section and paste this code there
<pre>
<pre>
     function playCard($card_id) {
     function actPlayCard(int $card_id) {
        self::checkAction("playCard");
         $player_id = $this->getActivePlayerId();
         $player_id = self::getActivePlayerId();
         throw new BgaUserException($this->_("Not implemented: ") . "$player_id plays $card_id");
         throw new BgaUserException(self::_("Not implemented: ") . "$player_id plays $card_id");
     }
     }
</pre>
</pre>
We won't implement it yet but throw an exception which we will see if interaction is working properly
We won't implement it yet but throw an exception which we will see if interaction is working properly


Now the game should start but it would not be any different then before because we have to implement actual interactions.
Now the game should start but it would not be any different than before because we have to implement actual interactions.
Its good to check if it still working though (and if it was running before you have to exit because we changed state machine and normally it will break stuff)
It's good to check if it's still working though (and if it was running before you have to exit because we changed state machine and normally it will break stuff).  NOTE: When you play a card, you will not yet get the "Not implemented" message.  That is added in the next section.


== Client - Server interactions ==
== Client - Server interactions ==


Now to implement things for real we have hook UI actions to ajax calls, and process notifications send by server.
Now to implement things for real we have hook UI actions to ajax calls, and process notifications sent by the server.
So previously we hooked playCardOnTable right into js handler which caused client animation, in real game its a two
So previously we hooked playCardOnTable right into js handler which caused client animation, in real game its a two
step operation. When user clicks on game element js client sends an ajax call to server, server processes it and updates database, server sends
step operation. When user clicks on game element js client sends an ajax call to server, server processes it and updates database, server sends
Line 825: Line 860:


             if (items.length > 0) {
             if (items.length > 0) {
                 var action = 'playCard';
                 var action = 'actPlayCard';
                 if (this.checkAction(action, true)) {
                 if (this.checkAction(action, true)) {
                     // Can play a card
                     // Can play a card
                     var card_id = items[0].id;                     
                     var card_id = items[0].id;                     
                     this.ajaxcall("/" + this.game_name + "/" + this.game_name + "/" + action + ".html", {
                     this.bgaPerformAction(action, {
                         id : card_id,
                         card_id : card_id,
                        lock : true
                    }, this, function(result) {
                    }, function(is_error) {
                     });
                     });


                     this.playerHand.unselectAll();
                     this.playerHand.unselectAll();
                 } else if (this.checkAction('giveCards')) {
                 } else if (this.checkAction('actGiveCards')) {
                     // Can give cards => let the player select some cards
                     // Can give cards => let the player select some cards
                 } else {
                 } else {
Line 851: Line 883:
Lets implement it, in .game.php
Lets implement it, in .game.php
<pre>
<pre>
     function playCard($card_id) {
     function actPlayCard(int $card_id) {
        self::checkAction("playCard");
         $player_id = $this->getActivePlayerId();
         $player_id = self::getActivePlayerId();
         $this->cards->moveCard($card_id, 'cardsontable', $player_id);
         $this->cards->moveCard($card_id, 'cardsontable', $player_id);
         // XXX check rules here
         // XXX check rules here
         $currentCard = $this->cards->getCard($card_id);
         $currentCard = $this->cards->getCard($card_id);
         // And notify
         // And notify
         self::notifyAllPlayers('playCard', clienttranslate('${player_name} plays ${value_displayed} ${color_displayed}'), array (
         $this->notifyAllPlayers('playCard', clienttranslate('${player_name} plays ${value_displayed} ${color_displayed}'), array (
                 'i18n' => array ('color_displayed','value_displayed' ),'card_id' => $card_id,'player_id' => $player_id,
                 'i18n' => array ('color_displayed','value_displayed' ),'card_id' => $card_id,'player_id' => $player_id,
                 'player_name' => self::getActivePlayerName(),'value' => $currentCard ['type_arg'],
                 'player_name' => $this->getActivePlayerName(),'value' => $currentCard ['type_arg'],
                 'value_displayed' => $this->values_label [$currentCard ['type_arg']],'color' => $currentCard ['type'],
                 'value_displayed' => $this->values_label [$currentCard ['type_arg']],'color' => $currentCard ['type'],
                 'color_displayed' => $this->colors [$currentCard ['type']] ['name'] ));
                 'color_displayed' => $this->colors [$currentCard ['type']] ['name'] ));
Line 868: Line 899:
</pre>
</pre>


We get the card from client, we move it to the tableau (moveCard is hooked to database directly, its part of deck class),
We get the card from client, we move it to the table (moveCard is hooked to database directly, its part of deck class),
we notify all players and we change state. What we missing here is bunch of checks (rule enforcements), we will add it later.
we notify all players and we change state. What we are missing here is bunch of checks (rule enforcements), we will add it later.


Interesting part about this notify is that we use i18n array for string that needs to be translated by client, so
Interesting part about this notify is that we use i18n array for strings that needs to be translated by client, so
they sent as English text in notification, then client has to know which parameters needs translating.
they are sent as English text in notification, then client has to know which parameters needs translating.


On the client side .js we have to implement a notification handler to do the animation
On the client side .js we have to implement a notification handler to do the animation
Line 880: Line 911:
             console.log('notifications subscriptions setup');
             console.log('notifications subscriptions setup');


             dojo.subscribe('newHand', this, "notif_newHand");
             const notifs = [
             dojo.subscribe('playCard', this, "notif_playCard");
                ['newHand', 1],
                ['playCard', 100],
            ];
   
             notifs.forEach((notif) => {
                dojo.subscribe(notif[0], this, `notif_${notif[0]}`);
                this.notifqueue.setSynchronous(notif[0], notif[1]);
            });


         },
         },
Line 903: Line 941:
</pre>
</pre>


Now it actually works through the server when you click on card - the move is recorded. If you testing it now you will notice
Now it actually works through the server when you click on card - the move is recorded. If you test it now you will notice
after trick is done all cards remains on the table, but if you press F5 they would disappear, this is because
after trick is done all cards remains on the table, but if you press F5 they would disappear, this is because
we updated database to pick-up the cards but did not send notification about it, so we need to send notification about it
we updated database to pick-up the cards but did not send notification about it, so we need to send notification about it
Line 914: Line 952:
             // Note: we use 2 notifications here in order we can pause the display during the first notification
             // Note: we use 2 notifications here in order we can pause the display during the first notification
             //  before we move all cards to the winner (during the second)
             //  before we move all cards to the winner (during the second)
             $players = self::loadPlayersBasicInfos();
             $players = $this->loadPlayersBasicInfos();
             self::notifyAllPlayers( 'trickWin', clienttranslate('${player_name} wins the trick'), array(
             $this->notifyAllPlayers( 'trickWin', clienttranslate('${player_name} wins the trick'), array(
                 'player_id' => $best_value_player_id,
                 'player_id' => $best_value_player_id,
                 'player_name' => $players[ $best_value_player_id ]['player_name']
                 'player_name' => $players[ $best_value_player_id ]['player_name']
             ) );             
             ) );             
             self::notifyAllPlayers( 'giveAllCardsToPlayer','', array(
             $this->notifyAllPlayers( 'giveAllCardsToPlayer','', array(
                 'player_id' => $best_value_player_id
                 'player_id' => $best_value_player_id
             ) );
             ) );
Line 928: Line 966:
This is to subscribe in setupNotifications function
This is to subscribe in setupNotifications function
<pre>
<pre>
             dojo.subscribe( 'trickWin', this, "notif_trickWin" );
             const notifs = [
            this.notifqueue.setSynchronous( 'trickWin', 1000 );
                ...
            dojo.subscribe( 'giveAllCardsToPlayer', this, "notif_giveAllCardsToPlayer" );
                ['trickWin', 1000],
                ['giveAllCardsToPlayer', 600],
            ];
</pre>
</pre>


And this are handlers
And these are handlers
<pre>
<pre>
         notif_trickWin : function(notif) {
         notif_trickWin : function(notif) {
Line 952: Line 992:


So 'trickWin' notification does not do much except it will delay the processing of next notification by 1 second (1000 ms)
So 'trickWin' notification does not do much except it will delay the processing of next notification by 1 second (1000 ms)
and it will log the message (that happens independent of what handler does).
and it will log the message (that happens independently of what handler does).
<i>Note: if on the other hand you don't want to log but what what to do something else send empty message</i>
<i>Note: if on the other hand you don't want to log but want to do something else, send an empty message</i>


Now after the trick you see all cards move the "player's stash".
Now after the trick you see all cards move towards the "player's stash".


== Scoring and End of game handling ==
== Scoring and End of game handling ==
Line 963: Line 1,003:
For which we will use state variable 'trickColor' which we already conveniently created.
For which we will use state variable 'trickColor' which we already conveniently created.


In .game.php file find playCard function and add this before notify functions
In .game.php file find the playCard function and add this before notify functions
         $currentTrickColor = self::getGameStateValue( 'trickColor' ) ;
         $currentTrickColor = $this->getGameStateValue( 'trickColor' ) ;
         if( $currentTrickColor == 0 )
         if( $currentTrickColor == 0 )
             self::setGameStateValue( 'trickColor', $currentCard['type'] );
             $this->setGameStateValue( 'trickColor', $currentCard['type'] );


This will make sure we remember first suit being played, now to use it modify stNextPlayer function to fix our TODO comment
This will make sure we remember the first suit being played, now to use it modify the stNextPlayer function to fix our TODO comment
<pre>
<pre>
     function stNextPlayer() {
     function stNextPlayer() {
Line 977: Line 1,017:
             $best_value = 0;
             $best_value = 0;
             $best_value_player_id = null;
             $best_value_player_id = null;
             $currentTrickColor = self::getGameStateValue('trickColor');
             $currentTrickColor = $this->getGameStateValue('trickColor');
             foreach ( $cards_on_table as $card ) {
             foreach ( $cards_on_table as $card ) {
                 // Note: type = card color
                 // Note: type = card color
Line 1,006: Line 1,046:


In .js file we need to add one more subscription and notification handler:
In .js file we need to add one more subscription and notification handler:
            dojo.subscribe( 'newScores', this, "notif_newScores" );
            const notifs = [
                ...
                ['newScores', 1],
            ];
in setupNotifications
in setupNotifications


Line 1,023: Line 1,066:
     function stEndHand() {
     function stEndHand() {
             // Count and score points, then end the game or go to the next hand.
             // Count and score points, then end the game or go to the next hand.
         $players = self::loadPlayersBasicInfos();
         $players = $this->loadPlayersBasicInfos();
         // Gets all "hearts" + queen of spades
         // Gets all "hearts" + queen of spades


Line 1,042: Line 1,085:
             if ($points != 0) {
             if ($points != 0) {
                 $sql = "UPDATE player SET player_score=player_score-$points  WHERE player_id='$player_id'";
                 $sql = "UPDATE player SET player_score=player_score-$points  WHERE player_id='$player_id'";
                 self::DbQuery($sql);
                 $this->DbQuery($sql);
                 $heart_number = $player_to_points [$player_id];
                 $heart_number = $player_to_points [$player_id];
                 self::notifyAllPlayers("points", clienttranslate('${player_name} gets ${nbr} hearts and looses ${nbr} points'), array (
                 $this->notifyAllPlayers("points", clienttranslate('${player_name} gets ${nbr} hearts and looses ${nbr} points'), array (
                         'player_id' => $player_id,'player_name' => $players [$player_id] ['player_name'],
                         'player_id' => $player_id,'player_name' => $players [$player_id] ['player_name'],
                         'nbr' => $heart_number ));
                         'nbr' => $heart_number ));
             } else {
             } else {
                 // No point lost (just notify)
                 // No point lost (just notify)
                 self::notifyAllPlayers("points", clienttranslate('${player_name} did not get any hearts'), array (
                 $this->notifyAllPlayers("points", clienttranslate('${player_name} did not get any hearts'), array (
                         'player_id' => $player_id,'player_name' => $players [$player_id] ['player_name'] ));
                         'player_id' => $player_id,'player_name' => $players [$player_id] ['player_name'] ));
             }
             }
         }
         }
         $newScores = self::getCollectionFromDb("SELECT player_id, player_score FROM player", true );
         $newScores = $this->getCollectionFromDb("SELECT player_id, player_score FROM player", true );
         self::notifyAllPlayers( "newScores", '', array( 'newScores' => $newScores ) );
         $this->notifyAllPlayers( "newScores", '', array( 'newScores' => $newScores ) );


         ///// Test if this is the end of the game
         ///// Test if this is the end of the game
Line 1,086: Line 1,129:
* Add card exchange states
* Add card exchange states
* Add game option to start with 75 points instead of 100
* Add game option to start with 75 points instead of 100
[[Category:Studio]]

Revision as of 05:09, 10 September 2024


Game File Reference



Useful Components

Official

  • Deck: a PHP component to manage cards (deck, hands, picking cards, moving cards, shuffle deck, ...).
  • Draggable: a JS component to manage drag'n'drop actions.
  • Counter: a JS component to manage a counter that can increase/decrease (ex: player's score).
  • ExpandableSection: a JS component to manage a rectangular block of HTML than can be displayed/hidden.
  • Scrollmap: a JS component to manage a scrollable game area (useful when the game area can be infinite. Examples: Saboteur or Takenoko games).
  • Stock: a JS component to manage and display a set of game elements displayed at a position.
  • Zone: a JS component to manage a zone of the board where several game elements can come and leave, but should be well displayed together (See for example: token's places at Can't Stop).

Undocumented component (if somebody knows please help with docs)

  • Wrapper: a JS component to wrap a <div> element around its child, even if these elements are absolute positioned.

Unofficial



Game Development Process



Guides for Common Topics



Miscellaneous Resources

Introduction

Using this tutorial, you can build a complete working game on the BGA environment: Hearts.

Before you read this tutorial, you must:

  • Read the overall presentations of the BGA Framework (see here).
  • Know the rules for Hearts
  • Some-what know the languages used on BGA: PHP, SQL, HTML, CSS, Javascript
  • Set up your development environment First Steps with BGA Studio
  • As part of setup you have to have access to your ftp home folder in studio, which would have the full 'hearts' game source code. We will be using some resources of this game in this tutorial, so copy it over to local disk if you have not done so.

If you are stuck or have question about this tutorial, post on BGA Developers forum

Create your first game

If you have not already, you have to create a project in BGA Studio. For this tutorial you can create a project heartsYOURNAME where YOURNAME is your developer login name. You can also re-use the project you have created for the "First Steps" tutorial above. With the initial skeleton of code provided, you can already start a game from the BGA Studio.

1. Find and express start the game in turn-based mode with 4 players. Make sure it works. If you want to see the game as 2nd player press red arrow button on the player panel to switch to that player. More details can be found in First_steps_with_BGA_Studio

2. Modify the text in heartsYOURNAME_heartsYOURNAME.tpl, reload the page in the browser and make sure your ftp sync works as expected. Note: if you have not setup auto-sync do it now, manually copying files is a no-starter.

3. Express stop from settings menu (the gear icon).

Note: please do not use the hearts project code as a base. This tutorial assumes you started with a TEMPLATE project with no prior modifications. Using the hearts project as a base will be very confusing and you won't be able to follow all the steps.

Attention!!! Very important note about reloading, if you don't remember this you may spend hours debugging. The browser caches images. If you change any of these files, you have to do "full reload" which is usually Ctrl+F5 (or Ctrl+reload button on browser) not just a regular reload.

Hook version control system

For a real game, or even for this tutorial, we recommend committing the code to version control right from the start. You are going to find yourself in a situation where the game doesn't even start anymore and no way of debugging it, unless you have a way to revert. That is where version control becomes very handy. If you are not familiar with version control (e.g. git) then at least back up your files after each major change. Start now.

Code for this tutorial available is on github: https://github.com/elaskavaia/bga-heartsla

Different revisions represent different steps along the process, starting from original template to a PARTIAL game. The full game can be found in your FTP home folder.

Update game infos and box graphics

Even it does nothing yet, always start by making sure the game looks decent in the game selector, meaning it has nice box graphics and its information is correct. For that we need to edit gameinfos.inc.php.

For a real game, you would go to BoardGameGeek, find the game, and use the information from BGG to fill in the gameinfos.

So let's do that. Find "hearts" on BoardGameGeek. (Hint: Original release 1850 :))

You can fill in the year of publishing and bgg id, put Public Domain under publisher (for a real game, leave an empty string so it won't be displayed), and a publisher id of 171 for public domain. And as designer and author you can just put your own name just for fun. Set number of players to 4.

 // Players configuration that can be played (ex: 2 to 4 players)
 'players' => array( 4 ),  

Important step: you have to refresh the information in the Studio website through the control panel. So go to Control Panel -> Manage Games -> heartsYOURNAME and press Reload for 'Reload game informations'.


The next step would be to replace game_box.png with nicer images. This can be done from the Game metadata manager.

Now try to start the game again. If you somehow introduced a syntax error in the gameinfos file it may not work (the game won't start). Always use the "Express Start" button to start the game. You should see a standard state prompt from the template. You should see 4 players on the right: testdude0 .. testdude3. To switch between them press the red arrow button near their names, it will open another tab. This way you don't need to login and logout from multiple accounts!


Note: if you had run the game before with less than 4 players there is a bug that will prevent you from running it with 4 only (if you did not run it before or run it with 4 players as instructed stop reading this note), to workaround revert back to original players array (i.e. 1,2,3,4), reload game options, then create a table with 4 players, exit that game table, then change gameoptions to 4 only as above, reload game options, create table again.

Code Rev [1]

Layout and Graphics

In this section we will do graphics of the game, and main layout of the game.

First copy a sprite with cards image from hearts img/cards.jpg into img/ folder of your project. Project hearts is mounted to your home directory on bga server.

There are better quality deck of cards image at https://en.doc.boardgamearena.com/Common_board_game_elements_image_resources

Details about images can be found here: Game art: img directory.

Edit .tpl to add some divs to represent player table and hand area

<div id="myhand_wrap" class="whiteblock">
    <h3>My Hand</h3>
    <div id="myhand">
    </div>
</div>

If you refresh you should see now white area with My Hand title.


Heartsla-tpl2.png

Now lets add a card into the hand, just so you can feel it. Edit .tpl and a playertablecard div inside a hand div

...
    <div id="myhand">
       <div class="playertablecard"></div>
    </div>
...

Edit .css file

.playertablecard {
    display: inline-block;
    position: relative;
    margin-top: 5px;
    width: 72px;
    height: 96px;
    background-image: url('img/cards.jpg'); /* temp hack to see it */
}

When you change existing graphics files remember that you have to FORCE-reload page, i.e. Ctrl-F5, otherwise its cached.

You should see this:

Heartsla-tpl3.png

Awesome! Now lets do the rest of layout.

There are few ways of how html could have been generated, you could have start with nothing and generate all by java script. Or you could have started with complete game markup in html and make java script just hide and move pieces around. BGA framework provides also a third way which is mix of both plus template engine to generate HTML using php. So lets do that.

Note: generating blocks in js only is probably simpler now with template strings.

Change .tpl file to have this inside

<div id="playertables">

    <!-- BEGIN playerhandblock -->
    <div class="playertable whiteblock playertable_{DIR}">
        <div class="playertablename" style="color:#{PLAYER_COLOR}">
            {PLAYER_NAME}
        </div>
        <div class="playertablecard" id="playertablecard_{PLAYER_ID}">
        </div>
    </div>
    <!-- END playerhandblock -->

</div>

<div id="myhand_wrap" class="whiteblock">
    <h3>{MY_HAND}</h3>
    <div id="myhand">
       <div class="playertablecard"></div>
    </div>
</div>

What we did is we added "block" playerhandblock, it is marked up using html comments. {VAR} notation is used to inject variables and

  <!-- BEGIN xxx --> 
   inside 
  <!-- END xxx --> 

effectively allows us to do template loops.

In .view.php, fill in the implementation of build_page like so:

    public function build_page($viewArgs)
    {
        // Get players
        $players = $this->game->loadPlayersBasicInfos();
        
        $template = $this->getGameName() . "_" . $this->getGameName();
        
        $directions = array( 'S', 'W', 'N', 'E' );
        
        // this will inflate our player block with actual players data
        $this->page->begin_block($template, "playerhandblock");
        foreach ( $players as $player_id => $info ) {
            $dir = array_shift($directions);
            $this->page->insert_block("playerhandblock", array ("PLAYER_ID" => $player_id,
                    "PLAYER_NAME" => $players [$player_id] ['player_name'],
                    "PLAYER_COLOR" => $players [$player_id] ['player_color'],
                    "DIR" => $dir ));
        }
        // this will make our My Hand text translatable
        $this->tpl['MY_HAND'] = $this->_("My hand");
    }

What it does is for each player we have it will replicate the html between and tags, substituting the variable denoted by {XXX} with the values you provide. The DIR variable in this case we pulling from directions array (where array_shift will take first element and remove it from the array).

Reload. If everything went well you should see this:

Display a list of all players, with a dummy card in each position

These are "tableau" areas for 4 players plus My hand visible only to one player. They are not exactly how we wanted them to be because we did not edit .css yet.

Now edit .css, add these lines after import before our previous definition

/** Table layout **/

#playertables {
    position: relative;
    width: 710px;
    height: 340px;
}

.playertablename {
    font-weight: bold;
}

.playertable {
    position: absolute;
    text-align: center;
    width: 180px;
    height: 130px;
}

.playertable_N {
    left: 50%;
    top: 0px;
    margin-left: -90px; /* half of 180 */
}
.playertable_S {
    left: 50%;
    bottom: 0px;
    margin-left: -90px; /* half of 180 */
}
.playertable_W {
    left: 0px;
    top: 50%;
    margin-top: -55px; /* half of 130 */
}
.playertable_E {
    right: 0px;
    top: 50%;
    margin-top: -55px; /* half of 130 */
}


Now you force Reload and you should see this: Heartsla-tpl5.png

This is almost all we need for graphics and layout, there are few tweaks left there but lets do some more heavy lifting now.

Note: if you did not see changes you may have not force reloaded, force means you use Ctrl+F5 or Cltr+Shift-R, if you don't "force" browser will use cached version of images! Which is not what you just changed


Another Note: In general if you have auto-sync you don't need to reload if you change game.php file, you need normal reload if you change js, and force reload for images. If you changed state machine or database you likely need to restart the game.

Game Interface JS Stock

The BGA framework provides a few out of the box classes to deal with cards. The client side contains a class called Stock and it can be used for any dynamic html "pieces" management that uses common sprite images. On the server side we will use the Deck class which we discuss later.

If you open cards.jpg in an image viewer you will see that it is a "sprite" image - a 13x4 grid of images stitched together, which is a very efficient way to transport images. So we will use the Stock class to mark up these images and create "card" divs for us.

First, we need to add ebg/stock as a dependency in the hearts.js file:

define([
    "dojo","dojo/_base/declare",
    "ebg/core/gamegui",
    "ebg/counter",
    "ebg/stock"     /// <==== HERE
],

Then add this to the Javascript constructor, this will define size of our cards

            console.log('hearts constructor');
            this.cardwidth = 72;
            this.cardheight = 96;

The stock is initialized in the Javascript "setup" method like this:

    // TODO: Set up your game interface here, according to "gamedatas"

    // Player hand
    this.playerHand = new ebg.stock(); // new stock object for hand
    this.playerHand.create( this, $('myhand'), this.cardwidth, this.cardheight );

As parameters of the "create" method, we provided the width/height of an item (a card), and the container div "myhand" - which is an id of "div" element from our .tpl file representing a player hand.


Then, we must tell the stock what items it is going to display during its life: the 52 cards of a standard card game from a CSS sprite image named "cards.jpg" with all the cards arranged in 4 rows and 13 columns.

Here's how we tell stock what item types to display:

            this.playerHand.image_items_per_row = 13; // 13 images per row


            // Create cards types:
            for (var color = 1; color <= 4; color++) {
                for (var value = 2; value <= 14; value++) {
                    // Build card type id
                    var card_type_id = this.getCardUniqueId(color, value);
                    this.playerHand.addItemType(card_type_id, card_type_id, g_gamethemeurl + 'img/cards.jpg', card_type_id);
                }
            }

And add this function to the utilities section

        // Get card unique identifier based on its color and value
        getCardUniqueId : function(color, value) {
            return (color - 1) * 13 + (value - 2);
        },

Explanations:

  • At first, we tell the stock component that our CSS sprite contains 13 items per row. This way, it can find the correct image for each card type id.
  • Then for the 4x13 cards, we call the addItemType method that creates the type. The arguments are the type id, the weight of the card (for sorting purpose), the URL of our CSS sprite, and the position of our card image in the CSS sprite. It happens to be the same number in our case.

Note: we need to generate a unique ID for each type of card based on its color and value. For that we create a function getCardUniqueId. The type is the unique identifier of the TYPE of the card, e.g., the queen of spades encoded as an integer. If our deck had 2 standard card decks we would have had 2 queens of spades; they would share the same type and the same image but would have different ids. NOTE: It's unfortunate that they named this getCardUniqueId; it should have been getCardUniqueType, because it really isn't an id, but a TYPE of card. The type of the item should either be a reversible function of its properties (i.e., kind of suite * 13 + value) or just an enumerator described in material.inc.php. In this specific case it's a synthetic type id, which also the same as the number of the card in the sprite image (i.e., if you enumerate each image in sprite going left to right, then top to bottom).

Now let's add the 5 of Hearts to the player's hand just for fun (this code will go in setup method after types initialization):

// 2 = hearts, 5 is 5, and 42 is the card id, which normally would come from db
this.playerHand.addToStockWithId( this.getCardUniqueId( 2, 5 ), 42 );

This will add the card with id 42 and type 16 ( (2-1)*13+(5-2)=16 ).

Note that number 16 would not be something you can see in database, Deck database will have separate field for type and type_arg where type is suite and type_arg is number, so its not the same thing, but you can use same formula to convert. Number 42 on the other hand would be id field in database. But we get to database in the later section.

If you reload now you should see the 5 of hearts in "your hand".

Stock control can handle clicking on items and forms the selection. You can immediately react to selection or you can query it later; for example when user presses some other button.

Let's hook it up. Add this in the setup method in .js file, after this.playerHand is initialised:

    dojo.connect( this.playerHand, 'onChangeSelection', this, 'onPlayerHandSelectionChanged' );


Then find the Player's action comment section in .js file and add a handler after the comment:

        onPlayerHandSelectionChanged: function() {
            var items = this.playerHand.getSelectedItems();

            if (items.length > 0) {
                if (this.checkAction('actPlayCard', true)) {
                    // Can play a card

                    var card_id = items[0].id;
                    console.log("on playCard "+card_id);

                    this.playerHand.unselectAll();
                } else if (this.checkAction('actGiveCards')) {
                    // Can give cards => let the player select some cards
                } else {
                    this.playerHand.unselectAll();
                }
            }
        },

Note : If you already have an example handler onCardClick: function( card_id ), go ahead and remove that handler, it is a duplicate of what we are implementing in this tutorial and may cause issues later on.

The function name of the handler is 4th parameter of the dojo.connect function. Make sure you spell it correctly or there will be unpredictable effects.

Now if you reload, open the Javascript Console (F12), and then click on the card in My Hand, you should see:

 on playCard 42

printed on the console

Note : You need to be the active player to have rights to play a card and so log your message in the console

Game Database and Game Initialisation

Next step, you want to design a game database and setup a new game (on the server side). For that we need to a) modify the database schema to add our cards data b) add some global variables into the existing globals table.

To modify the schema, first exit your existing game(s). Open dbmodel.sql file and uncomment the card table creation.

CREATE TABLE IF NOT EXISTS `card` (
  `card_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `card_type` varchar(16) NOT NULL,
  `card_type_arg` int(11) NOT NULL,
  `card_location` varchar(16) NOT NULL,
  `card_location_arg` int(11) NOT NULL,
  PRIMARY KEY (`card_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

This is the "card" table which will be managed by the Deck php class.

In addition we want a little piece of information in the players table:

 -- add info about first player
 ALTER TABLE `player` ADD `player_first` BOOLEAN NOT NULL DEFAULT '0';

Not sure why they put this into the player table, as we could use a global db variable to hold first player as easily. But I am just following the existing code more-or-less.

Next we finally get into .game.php class, where the main logic and db interaction would be. Find php constructor which should be

 function __construct( )

This is first function in a file. Add this code to constructor.

        parent::__construct();
        $this->initGameStateLabels( array( 
                         "currentHandType" => 10, 
                         "trickColor" => 11, 
                         "alreadyPlayedHearts" => 12,
                          ) );

        $this->cards = $this->getNew( "module.common.deck" );
        $this->cards->init( "card" );

Here we are initializing three "Game State Variables" which are variables stored in the database. They are integers. It must start with values higher or equal to 10 since values lower than 10 are reserved. These values are stored by numeric ids in the database, but in the php we associate them with string labels for convenience of access.

The variables are:

  • "trickColor": numbers from 1 to 4 that map to card suit (not sure why it's called color; maybe it's a translation from French);
  • "alreadyPlayedHearts": a boolean flag (0 or 1) indicating whether somebody used hearts on the trick;
  • "currentHandType": stores the value to indicate who to give cards to during exchange.

The next 2 lines are creating $this->cards object and associating it with "card" table in the the database.

If we called db table 'foo' instead of 'card' the last statement would have been $this->cards->init( "foo" )


At this point I would start a new game and make sure it starts, then exit.

If you made a mistake in the .sql or php constructor the game won't start, and good luck debugging it. (That is why it's important to check once in a while to make sure it still starts while you remember what you have changed.)

Code Rev [2]

Now we can go to game initialization setupNewGame in game.php. This method is called only once when the game is created.

In your template project you should have code that deals with player table, just leave it as is. Start inserting the other code after "Start the game initialization" comment.

        // Init global values with their initial values

        // Note: hand types: 0 = give 3 cards to player on the left
        //                   1 = give 3 cards to player on the right
        //                   2 = give 3 cards to player opposite
        //                   3 = keep cards
        $this->setGameStateInitialValue( 'currentHandType', 0 );
        
        // Set current trick color to zero (= no trick color)
        $this->setGameStateInitialValue( 'trickColor', 0 );
        
        // Mark if we already played hearts during this hand
        $this->setGameStateInitialValue( 'alreadyPlayedHearts', 0 );

Here we initialize all the globals to 0.

Next is to create our cards in the database. We have one deck of cards so it's pretty simple.

        // Create cards
        $cards = array ();
        foreach ( $this->colors as $color_id => $color ) {
            // spade, heart, diamond, club
            for ($value = 2; $value <= 14; $value ++) {
                //  2, 3, 4, ... K, A
                $cards [] = array ('type' => $color_id,'type_arg' => $value,'nbr' => 1 );
            }
        }
        
        $this->cards->createCards( $cards, 'deck' );

This code that will create one of each card. But don't run it yet, because we missing $this->colors. So we have state of the game in the database, but there is some static game information which never changes. This information should be stored in material.inc.php and this way it can be accessed from all .php files (and .js if you send it via getAllDatas()). We will edit this file now by adding these lines

$this->colors = array(
    1 => array( 'name' => clienttranslate('spade'),
                'nametr' => $this->_('spade') ),
    2 => array( 'name' => clienttranslate('heart'),
                'nametr' => $this->_('heart') ),
    3 => array( 'name' => clienttranslate('club'),
                'nametr' => $this->_('club') ),
    4 => array( 'name' => clienttranslate('diamond'),
                'nametr' => $this->_('diamond') )
);

$this->values_label = array(
    2 =>'2',
    3 => '3',
    4 => '4',
    5 => '5',
    6 => '6',
    7 => '7',
    8 => '8',
    9 => '9',
    10 => '10',
    11 => clienttranslate('J'),
    12 => clienttranslate('Q'),
    13 => clienttranslate('K'),
    14 => clienttranslate('A')
);

Where $this->colors will define Suit labels and $this->values_label will define value labels. If you noticed, we have two of each label for suits. This is because sometimes we need translated values on the php side and sometimes we don't. In this case nametr will return a translated value in php, which is only useful when you throw exceptions to show the right strings. If you pass a value to the client via notification you should always use untranslated strings, and the client will translate it. 'clienttranslate' marks the value for translation but does not actually change it for php. For more about this wonderful translation stuff see Translations.

Full game model synchronisation

Now at any point in the game we need to make sure that database information can be reflected back in the UI, so we must fix the getAllDatas function to return all possible data we need to reconstruct the game. This is in the game.php file. The template for getAllDatas() already takes care of player info. Let's just add hand and tableau data before we return a result.

        // Cards in player hand
        $result['hand'] = $this->cards->getCardsInLocation( 'hand', $current_player_id );
        
        // Cards played on the table
        $result['cardsontable'] = $this->cards->getCardsInLocation( 'cardsontable' );

Now on the client side we should display this data, so in your .js file in the setup function (which is the receiver of getAllDatas) replace our hack of putting 5 of Hearts directly into the hand with:

            // Cards in player's hand
            for ( var i in this.gamedatas.hand) {
                var card = this.gamedatas.hand[i];
                var color = card.type;
                var value = card.type_arg;
                this.playerHand.addToStockWithId(this.getCardUniqueId(color, value), card.id);
            }

            // Cards played on table
            for (i in this.gamedatas.cardsontable) {
                var card = this.gamedatas.cardsontable[i];
                var color = card.type;
                var value = card.type_arg;
                var player_id = card.location_arg;
                this.playCardOnTable(player_id, color, value, card.id);
            }

This should show hand and tableau cards now, except we are missing the playCardOnTable function. So find the getCardUniqueId function which should be in the utilities section and add this after it:

        playCardOnTable : function(player_id, color, value, card_id) {
            // player_id => direction
            this.addTableCard(value, color, player_id, player_id);

            if (player_id != this.player_id) {
                // Some opponent played a card
                // Move card from player panel
                this.placeOnObject('cardontable_' + player_id, 'overall_player_board_' + player_id);
            } else {
                // You played a card. If it exists in your hand, move card from there and remove
                // corresponding item

                if ($('myhand_item_' + card_id)) {
                    this.placeOnObject('cardontable_' + player_id, 'myhand_item_' + card_id);
                    this.playerHand.removeFromStockById(card_id);
                }
            }

            // In any case: move it to its final destination
            this.slideToObject('cardontable_' + player_id, 'playertablecard_' + player_id).play();
        },

For this to work we also need to define the addTableCard

        addTableCard(value, color, card_player_id, playerTableId) {
            const x = value - 2;
            const y = color - 1;
            document.getElementById('playertablecard_' + playerTableId).insertAdjacentHTML('beforeend', `
                <div class="card cardontable" id="cardontable_${card_player_id}" style="background-position:-${x}00% -${y}00%"></div>
            `);
        },


What this does is basically create another card object, because if it is not our card it's not in our hand (Stock) so we have to create it out of thin air. The technique to do that is to implement a Javascript template object defined in the .tpl file with some parameters, which will basically create a "div" string (yes you could have used string concatenation but it would not be fancy). Now dojo.place places it (the div) on top of a placeholder. Now we have an object with an id of 'cardontable_' + player_id. Depending on who is playing it we either place it on the player miniboard or in hand (and remove it from hand stock). Then we animate the card move.

We also should fix our .css file now to add style for cardontable and REMOVE background for playertablecard which really is a placeholder div and not a card. (Don't miss the remove step; it will be all screwy if you do!)

.playertablecard {
    display: inline-block;
    position: relative;
    margin-top: 5px;
    width: 72px;
    height: 96px;
    /* we remove background-image here */
}

/*** cards on table ***/

.cardontable {
    position: absolute;
    width: 72px;
    height: 96px;
    background-image: url('img/cards.jpg'); 
}

Now to test that it actually works let's deal cards to players during game initialization:

Add this after createCards in setupNewGame function in the game.php file

        // Shuffle deck
        $this->cards->shuffle('deck');
        // Deal 13 cards to each players
        $players = $this->loadPlayersBasicInfos();
        foreach ( $players as $player_id => $player ) {
            $cards = $this->cards->pickCards(13, 'deck', $player_id);
        } 

Now when you start the game you should see 13 cards in your hand!

We just need to hook-up clicking on card and test if our playCardOnTable works.

Find onPlayerHandSelectionChanged function in the JS file, we should have logging there like console.log("on playCard "+card_id); So after that insert this (Note: this code is for testing we will replace it with server interaction after we test it.):

                    console.log("on playCard "+card_id);
                    // type is (color - 1) * 13 + (value - 2)
                    var type = items[0].type;
                    var color = Math.floor(type / 13) + 1;
                    var value = type % 13 + 2;
                    
                    this.playCardOnTable(this.player_id,color,value,card_id);

Now if you force reload (because we changed .css before) you should be able to click on card from your hand and see it moving, you can click on few cards this way. When you done enjoying the animation, press F5 to get your hand back.

Heartsla-sync.png

Code Rev [3]

State Machine

Now we need to create a game state machine. So the states are (excluding two more states we will add later to handle the exchange of cards at the beginning of the rounds):

  • Cards are dealt to all players (lets call it "newHand")
  • Player is selected who will start a new trick ("newTrick")
  • Player start or respond to played card ("playerTurn")
  • Game control is passed to next player or trick is ended ("nextPlayer")
  • End of hand processing (scoring and check for end of game) ("nextHand")


The state handling spread across 4 files, so you have to make sure all pieces are connected together. The state machine states.php defines all the states, and function handlers on php side in a form of string, and if any of these functions are not implemented it would be very hard to debug because it will break in random places.

So .states.php

$machinestates = array(

    // The initial state. Please do not modify.
    1 => array(
        "name" => "gameSetup",
        "description" => clienttranslate("Game setup"),
        "type" => "manager",
        "action" => "stGameSetup",
        "transitions" => array( "" => 2 )
    ),
    
    
    /// New hand
    2 => array(
        "name" => "newHand",
        "description" => "",
        "type" => "game",
        "action" => "stNewHand",
        "updateGameProgression" => true,   
        "transitions" => array( "" => 30 )
    ),    

    21 => array(       
        "name" => "giveCards",
        "description" => clienttranslate('Some players must choose 3 cards to give to ${direction}'),
        "descriptionmyturn" => clienttranslate('${you} must choose 3 cards to give to ${direction}'),
        "type" => "multipleactiveplayer",
        "action" => "stGiveCards",
        "args" => "argGiveCards",
        "possibleactions" => array( "actGiveCards" ),
        "transitions" => array( "giveCards" => 22, "skip" => 22 )        
    ), 
    
    22 => array(
        "name" => "takeCards",
        "description" => "",
        "type" => "game",
        "action" => "stTakeCards",
        "transitions" => array( "startHand" => 30, "skip" => 30  )
    ),        
      
    
    // Trick
    
    30 => array(
        "name" => "newTrick",
        "description" => "",
        "type" => "game",
        "action" => "stNewTrick",
        "transitions" => array( "" => 31 )
    ),       
    31 => array(
        "name" => "playerTurn",
        "description" => clienttranslate('${actplayer} must play a card'),
        "descriptionmyturn" => clienttranslate('${you} must play a card'),
        "type" => "activeplayer",
        "possibleactions" => array( "actPlayCard" ),
        "transitions" => array( "playCard" => 32 )
    ), 
    32 => array(
        "name" => "nextPlayer",
        "description" => "",
        "type" => "game",
        "action" => "stNextPlayer",
        "transitions" => array( "nextPlayer" => 31, "nextTrick" => 30, "endHand" => 40 )
    ), 
    
    
    // End of the hand (scoring, etc...)
    40 => array(
        "name" => "endHand",
        "description" => "",
        "type" => "game",
        "action" => "stEndHand",
        "transitions" => array( "nextHand" => 2, "endGame" => 99 )
    ),     
   
    // Final state.
    // Please do not modify.
    99 => array(
        "name" => "gameEnd",
        "description" => clienttranslate("End of game"),
        "type" => "manager",
        "action" => "stGameEnd",
        "args" => "argGameEnd"
    )

);

The full details on these fields are in Your_game_state_machine:_states.inc.php.

But basically we have Player states, in which a human player has to perform an "action" by pressing some button in the UI or selecting some game item, which will trigger js handler, which will do an ajax call to the server.

To make it run we have to define all the handler functions that we referenced in states, which are - one function for state arguments argGiveCards, 4 functions for robot states (where the game performs some action) and 1 function for player actions handling. In .game.php, find 'Game state arguments' section and paste this in:

    function argGiveCards() {
        return array ();
    }

This normally passes some parameters to states, but we don't need anything yet. It's good to have a placeholder there anyway, so we can fix it later. Important: even when it's a stub, this function must return an array not a scalar.

Let's do stubs for other functions, find the game state actions section in .game.php file and insert these

    function stNewHand() {
        // Take back all cards (from any location => null) to deck
        $this->cards->moveAllCardsInLocation(null, "deck");
        $this->cards->shuffle('deck');
        // Deal 13 cards to each players
        // Create deck, shuffle it and give 13 initial cards
        $players = $this->loadPlayersBasicInfos();
        foreach ( $players as $player_id => $player ) {
            $cards = $this->cards->pickCards(13, 'deck', $player_id);
            // Notify player about his cards
            $this->notifyPlayer($player_id, 'newHand', '', array ('cards' => $cards ));
        }
        $this->setGameStateValue('alreadyPlayedHearts', 0);
        $this->gamestate->nextState("");
    }

    function stNewTrick() {
        // New trick: active the player who wins the last trick, or the player who own the club-2 card
        // Reset trick color to 0 (= no color)
        $this->setGameStateInitialValue('trickColor', 0);
        $this->gamestate->nextState();
    }

    function stNextPlayer() {
        // Active next player OR end the trick and go to the next trick OR end the hand
        if ($this->cards->countCardInLocation('cardsontable') == 4) {
            // This is the end of the trick
            // Move all cards to "cardswon" of the given player
            $best_value_player_id = $this->activeNextPlayer(); // TODO figure out winner of trick
            $this->cards->moveAllCardsInLocation('cardsontable', 'cardswon', null, $best_value_player_id);
        
            if ($this->cards->countCardInLocation('hand') == 0) {
                // End of the hand
                $this->gamestate->nextState("endHand");
            } else {
                // End of the trick
                $this->gamestate->nextState("nextTrick");
            }
        } else {
            // Standard case (not the end of the trick)
            // => just active the next player
            $player_id = $this->activeNextPlayer();
            $this->giveExtraTime($player_id);
            $this->gamestate->nextState('nextPlayer');
        }
    }

    function stEndHand() {
        $this->gamestate->nextState("nextHand");
    }

Important: All state actions game or player must end with state transition (or thrown exception). Also make sure it's ONLY one state transition, if you accidentally fall through after state transition and do another one it will be a real mess and head scratching for long time.

Now find 'player actions' section and paste this code there

    function actPlayCard(int $card_id) {
        $player_id = $this->getActivePlayerId();
        throw new BgaUserException($this->_("Not implemented: ") . "$player_id plays $card_id");
    }

We won't implement it yet but throw an exception which we will see if interaction is working properly

Now the game should start but it would not be any different than before because we have to implement actual interactions. It's good to check if it's still working though (and if it was running before you have to exit because we changed state machine and normally it will break stuff). NOTE: When you play a card, you will not yet get the "Not implemented" message. That is added in the next section.

Client - Server interactions

Now to implement things for real we have hook UI actions to ajax calls, and process notifications sent by the server. So previously we hooked playCardOnTable right into js handler which caused client animation, in real game its a two step operation. When user clicks on game element js client sends an ajax call to server, server processes it and updates database, server sends notification in response, client hooks animations to server notification.

So in .js code replace onPlayerHandSelectionChanged with

        onPlayerHandSelectionChanged : function() {
            var items = this.playerHand.getSelectedItems();

            if (items.length > 0) {
                var action = 'actPlayCard';
                if (this.checkAction(action, true)) {
                    // Can play a card
                    var card_id = items[0].id;                    
                    this.bgaPerformAction(action, {
                        card_id : card_id,
                    });

                    this.playerHand.unselectAll();
                } else if (this.checkAction('actGiveCards')) {
                    // Can give cards => let the player select some cards
                } else {
                    this.playerHand.unselectAll();
                }
            }
        },


Now when you click on card you should get a server response: Not implemented...

Lets implement it, in .game.php

    function actPlayCard(int $card_id) {
        $player_id = $this->getActivePlayerId();
        $this->cards->moveCard($card_id, 'cardsontable', $player_id);
        // XXX check rules here
        $currentCard = $this->cards->getCard($card_id);
        // And notify
        $this->notifyAllPlayers('playCard', clienttranslate('${player_name} plays ${value_displayed} ${color_displayed}'), array (
                'i18n' => array ('color_displayed','value_displayed' ),'card_id' => $card_id,'player_id' => $player_id,
                'player_name' => $this->getActivePlayerName(),'value' => $currentCard ['type_arg'],
                'value_displayed' => $this->values_label [$currentCard ['type_arg']],'color' => $currentCard ['type'],
                'color_displayed' => $this->colors [$currentCard ['type']] ['name'] ));
        // Next player
        $this->gamestate->nextState('playCard');
    }

We get the card from client, we move it to the table (moveCard is hooked to database directly, its part of deck class), we notify all players and we change state. What we are missing here is bunch of checks (rule enforcements), we will add it later.

Interesting part about this notify is that we use i18n array for strings that needs to be translated by client, so they are sent as English text in notification, then client has to know which parameters needs translating.

On the client side .js we have to implement a notification handler to do the animation

        setupNotifications : function() {
            console.log('notifications subscriptions setup');

            const notifs = [
                ['newHand', 1],
                ['playCard', 100],
            ];
    
            notifs.forEach((notif) => {
                dojo.subscribe(notif[0], this, `notif_${notif[0]}`);
                this.notifqueue.setSynchronous(notif[0], notif[1]);
            });

        },

        notif_newHand : function(notif) {
            // We received a new full hand of 13 cards.
            this.playerHand.removeAll();

            for ( var i in notif.args.cards) {
                var card = notif.args.cards[i];
                var color = card.type;
                var value = card.type_arg;
                this.playerHand.addToStockWithId(this.getCardUniqueId(color, value), card.id);
            }
        },

        notif_playCard : function(notif) {
            // Play a card on the table
            this.playCardOnTable(notif.args.player_id, notif.args.color, notif.args.value, notif.args.card_id);
        },

Now it actually works through the server when you click on card - the move is recorded. If you test it now you will notice after trick is done all cards remains on the table, but if you press F5 they would disappear, this is because we updated database to pick-up the cards but did not send notification about it, so we need to send notification about it and have a handler for it

So in .game.php file add notification in stNextPlayer function after moveAllCardsInLocation call:

            // Notify
            // Note: we use 2 notifications here in order we can pause the display during the first notification
            //  before we move all cards to the winner (during the second)
            $players = $this->loadPlayersBasicInfos();
            $this->notifyAllPlayers( 'trickWin', clienttranslate('${player_name} wins the trick'), array(
                'player_id' => $best_value_player_id,
                'player_name' => $players[ $best_value_player_id ]['player_name']
            ) );            
            $this->notifyAllPlayers( 'giveAllCardsToPlayer','', array(
                'player_id' => $best_value_player_id
            ) );

And in .js file add 2 more notification handlers.

This is to subscribe in setupNotifications function

            const notifs = [
                ...
                ['trickWin', 1000],
                ['giveAllCardsToPlayer', 600],
            ];

And these are handlers

        notif_trickWin : function(notif) {
            // We do nothing here (just wait in order players can view the 4 cards played before they're gone.
        },
        notif_giveAllCardsToPlayer : function(notif) {
            // Move all cards on table to given table, then destroy them
            var winner_id = notif.args.player_id;
            for ( var player_id in this.gamedatas.players) {
                var anim = this.slideToObject('cardontable_' + player_id, 'overall_player_board_' + winner_id);
                dojo.connect(anim, 'onEnd', function(node) {
                    dojo.destroy(node);
                });
                anim.play();
            }
        },

So 'trickWin' notification does not do much except it will delay the processing of next notification by 1 second (1000 ms) and it will log the message (that happens independently of what handler does). Note: if on the other hand you don't want to log but want to do something else, send an empty message

Now after the trick you see all cards move towards the "player's stash".

Scoring and End of game handling

Now we should calculate scoring and for that we need to actually track who wins the trick. Trick is won by the player with highest card (no trump). We just need to remember what is trick suite. For which we will use state variable 'trickColor' which we already conveniently created.

In .game.php file find the playCard function and add this before notify functions

       $currentTrickColor = $this->getGameStateValue( 'trickColor' ) ;
       if( $currentTrickColor == 0 )
           $this->setGameStateValue( 'trickColor', $currentCard['type'] );

This will make sure we remember the first suit being played, now to use it modify the stNextPlayer function to fix our TODO comment

    function stNextPlayer() {
        // Active next player OR end the trick and go to the next trick OR end the hand
        if ($this->cards->countCardInLocation('cardsontable') == 4) {
            // This is the end of the trick
            $cards_on_table = $this->cards->getCardsInLocation('cardsontable');
            $best_value = 0;
            $best_value_player_id = null;
            $currentTrickColor = $this->getGameStateValue('trickColor');
            foreach ( $cards_on_table as $card ) {
                // Note: type = card color
                if ($card ['type'] == $currentTrickColor) {
                    if ($best_value_player_id === null || $card ['type_arg'] > $best_value) {
                        $best_value_player_id = $card ['location_arg']; // Note: location_arg = player who played this card on table
                        $best_value = $card ['type_arg']; // Note: type_arg = value of the card
                    }
                }
            }
            
            // Active this player => he's the one who starts the next trick
            $this->gamestate->changeActivePlayer( $best_value_player_id );
            
            // Move all cards to "cardswon" of the given player
            $this->cards->moveAllCardsInLocation('cardsontable', 'cardswon', null, $best_value_player_id);
        
            // Notify
            // ... same code here as before

The scoring rule in the studio example code is huge multi-page function, for this tutorial we will make simplier. Lets score -1 point per heart and call it a day. And game will end when somebody goes -100 or below.

As UI goes for scoring, the main thing to update is the scoring on the mini boards represented by stars, also we want to show that in the log. In addition scoring can be shown in Scoring Dialog using tableWindow notification, but it is a tutorial on its own and you can do it as homework (it is part of original heart game).

In .js file we need to add one more subscription and notification handler:

            const notifs = [
                ...
                ['newScores', 1],
            ];

in setupNotifications

and

       notif_newScores : function(notif) {
           // Update players' scores
           for ( var player_id in notif.args.newScores) {
               this.scoreCtrl[player_id].toValue(notif.args.newScores[player_id]);
           }
       },

somewhere after. this.scoreCtrl is pre-existing object that shows the scoring and this function will update score values per player from notification argument

so in .game.php our stEndHand function will look like

    function stEndHand() {
            // Count and score points, then end the game or go to the next hand.
        $players = $this->loadPlayersBasicInfos();
        // Gets all "hearts" + queen of spades

        $player_to_points = array ();
        foreach ( $players as $player_id => $player ) {
            $player_to_points [$player_id] = 0;
        }
        $cards = $this->cards->getCardsInLocation("cardswon");
        foreach ( $cards as $card ) {
            $player_id = $card ['location_arg'];
            // Note: 2 = heart
            if ($card ['type'] == 2) {
                $player_to_points [$player_id] ++;
            }
        }
        // Apply scores to player
        foreach ( $player_to_points as $player_id => $points ) {
            if ($points != 0) {
                $sql = "UPDATE player SET player_score=player_score-$points  WHERE player_id='$player_id'";
                $this->DbQuery($sql);
                $heart_number = $player_to_points [$player_id];
                $this->notifyAllPlayers("points", clienttranslate('${player_name} gets ${nbr} hearts and looses ${nbr} points'), array (
                        'player_id' => $player_id,'player_name' => $players [$player_id] ['player_name'],
                        'nbr' => $heart_number ));
            } else {
                // No point lost (just notify)
                $this->notifyAllPlayers("points", clienttranslate('${player_name} did not get any hearts'), array (
                        'player_id' => $player_id,'player_name' => $players [$player_id] ['player_name'] ));
            }
        }
        $newScores = $this->getCollectionFromDb("SELECT player_id, player_score FROM player", true );
        $this->notifyAllPlayers( "newScores", '', array( 'newScores' => $newScores ) );

        ///// Test if this is the end of the game
        foreach ( $newScores as $player_id => $score ) {
            if ($score <= -100) {
                // Trigger the end of the game !
                $this->gamestate->nextState("endGame");
                return;
            }
        }

        
        $this->gamestate->nextState("nextHand");
    }


So it should more less work now, including end of game condition. Try to play it!

Additional stuff

The following things were not implemented and can add them yourself by looking at the code of original hearts game:

  • Remove debug code from setupNewGame to deal cards, cards are now dealt in stNewHand state handler
  • Rule checking and rule enforcements in playCard function
  • Start scoring with 100 points each and end when <= 0
  • Fix scoring rules with Q of spades and 26 point reverse scoring
  • First player one with 2 club
  • Add progress handling
  • Add statistics
  • Add card exchange states
  • Add game option to start with 75 points instead of 100