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
(Update part that related to older .tpl workflow)
 
(32 intermediate revisions by 3 users not shown)
Line 7: Line 7:
Before you read this tutorial, you must:
Before you read this tutorial, you must:
* Read the overall presentations of the BGA Framework ([[Studio|see here]]).
* Read the overall presentations of the BGA Framework ([[Studio|see here]]).
* 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
* Set up your 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]
Line 13: Line 12:


If you are stuck or 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]
== Hearts Rules ==
Hearts is a trick-taking card game for four players where the goal is to score the fewest points.
Players aim to avoid taking tricks with heart cards (1 point each) and the Queen of Spades (13 points).
Each round, 13 cards are dealt, players pass three cards, and the player with the 2 of Clubs starts the first trick.
Play continues clockwise, with players needing to follow suit if they can, and the highest card of the lead suit wins the trick.
Hearts cannot be played until they are "broken" by a player who can't follow suit and discards a heart, or by a player leading with a heart after they've been broken.


== 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 heartsYOURNAME 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
YOURNAME is your developer login name. You can also re-use the project you have created for the "First Steps" tutorial above.
YOURNAME is your developer login name (or shorter version of thereof). You can also re-use the project you have created for the "First Steps" tutorial above.


<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>
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. Also it will not match exactly with this tutorial for different reasons.
</i>
</i>


Line 45: Line 55:
Note: the game was re-written using new template, the old code is in "oldframework" branch. The new template is in main branch.
Note: the game was re-written using new template, the old code is in "oldframework" branch. The new template is in main branch.


The full game can be found in your FTP home folder, after getting read-only access: https://en.doc.boardgamearena.com/First_steps_with_BGA_Studio#Set_up_dev_environment_ide,_editor_and_File_Sync
The real hearts game (that you can play on BGA) can be found in your FTP home folder, after getting read-only access, go to https://studio.boardgamearena.com/projects, select Already Published and find Hearts to get access
(It may not match this tutorial as framework diverged since this game was created and it may not have been updated)


== Update game infos and box graphics ==
== Update game infos and box graphics ==
Line 84: Line 95:
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 [https://github.com/elaskavaia/bga-heartsla/blob/4b3a73eeb5acae961ade18473af119e8ce8d1a8f/img/cards.jpg hearts img/cards.jpg]  into img/ folder of your project.  
First copy a sprite with cards image from [https://x.boardgamearena.net/data/others/cards/FULLREZ_CARDS_ORIGINAL_NORMAL.jpg]  into img/cards.jpg folder of your project.  
<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]]. If you did not setup auto-sync of files, sync the graphics manually with remote folder (re-sync with your workspace).
Details about images can be found here: [[Game art: img directory]]. If you did not setup auto-sync of files, sync the graphics manually with remote folder (re-sync with your workspace).
Line 92: Line 102:


<pre>
<pre>
document.getElementById('game_play_area').insertAdjacentHTML('beforeend', `
 
        setup: function( gamedatas )
        {
            console.log( "Starting game setup" );
 
            document.getElementById('game_play_area').insertAdjacentHTML('beforeend', `
                 <div id="myhand_wrap" class="whiteblock">
                 <div id="myhand_wrap" class="whiteblock">
                     <b id="myhand_label">${_('My hand')}</b>
                     <b id="myhand_label">${_('My hand')}</b>
Line 100: Line 115:


             `);
             `);
            // ...
</pre>
</pre>




If you refresh you should see now white area with My Hand title (the image slightly obsolete, you may see more stuff there and different prompt).
If you refresh you should see now white area with My Hand title.




[[File:Heartsla-tpl2.png]]
[[File:Heartsla-tpl2.png]]


Now lets add a card into the hand, just so you can feel it. Edit the html string we inserted earlier and a playertablecard div inside a hand div
Now lets add a card into the hand, just so you can feel it. Edit the html snippet we inserted earlier buy adding a line representing a card
<pre>
<pre>
...
...
     <div id="myhand">
     <div id="myhand">
       <div class="playertablecard"></div>
       <div class="fakecard"></div>
     </div>
     </div>
...
...
</pre>
</pre>


Edit .css file
Edit .css file, add this code (.css file is empty now, only has comments, just tuck this at the end)
<pre>
<pre>
.playertablecard {
.fakecard {
     display: inline-block;
     display: inline-block;
     position: relative;
     position: relative;
     margin-top: 5px;
     margin-top: 5px;
     width: 72px;
    border-radius: 5%;
     height: 96px;
     width: 100px;
     height: 135px;
    background-size: calc(100px * 15);
     background-image: url('img/cards.jpg'); /* temp hack to see it */
     background-image: url('img/cards.jpg'); /* temp hack to see it */
}
}
Line 131: Line 149:
When you change existing graphics files 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.


You should see this:
You should see this (more less):


[[File:Heartsla-tpl3.png]]
[[File:Heartsla-tpl3.png]]
Note: If you don't see the card a) check it was synced to remote folder b) force reload page


Awesome! Now lets do the rest of layout.
Awesome! Now lets do the rest of layout.


Note: If you don't see the card a) check it was synced to remote folder b) force reload page


Let's complete the template string we created for the hand. You template should have this code, just leave it there
 
Let's complete the game template. You template should have this code, just leave it there
<pre>
<pre>


       // Example to add a div on the game area
       // Example to add a div on the game area
       document.getElementById("game_play_area").insertAdjacentHTML("beforeend",
       document.getElementById("game_play_area").insertAdjacentHTML("beforeend",
        `
                             <div id="player-tables"></div>
                             <div id="player-tables"></div>
                         `
                         `
Line 150: Line 170:
</pre>
</pre>


Then change the following template code to this
Then change the code following comment "// Setting up player boards" with this
<pre>
<pre>
       // Setting up player boards
       // Setting up player boards
       var numPlayers = Object.keys(gamedatas.players).length;
       const numPlayers = Object.keys(gamedatas.players).length;
       Object.values(gamedatas.players).forEach((player, index) => {
       Object.values(gamedatas.players).forEach((player, index) => {
         document.getElementById("player-tables").insertAdjacentHTML(
         document.getElementById("player-tables").insertAdjacentHTML(
           "beforeend",
           "beforeend",
          // we generate this html snippet for each player
           `
           `
     <div class="playertable whiteblock playertable_${DIRECTIONS[numPlayers][index]}">
     <div class="playertable whiteblock playertable_${DIRECTIONS[index]}">
         <div class="playertablename" style="color:#${player.color};"><span class="dealer_token" id="dealer_token_p${player.id}">🃏 </span>${player.name}</div>
         <div class="playertablename" style="color:#${player.color};">${player.name}</div>
         <div class="playertablecard" id="playertablecard_${player.id}"></div>
         <div id="tableau_${player.id}"></div>
        <div class="playertablename" id="hand_score_wrap_${player.id}"><span class="hand_score_label"></span> <span id="hand_score_${player.id}"></span></div>
     </div>
     </div>
                `
    `
         );
         );
       });
       });
Line 171: Line 191:
Oops! it won't load. This is to teach you how it will look like when you have syntax error in your js file. The game will hang loading at 10% or so. How to know what happened?
Oops! it won't load. This is to teach you how it will look like when you have syntax error in your js file. The game will hang loading at 10% or so. How to know what happened?
Open dev tools in browser (usually F12) and navigate to Console tab. You will see a stack trace of where error is. In our case
Open dev tools in browser (usually F12) and navigate to Console tab. You will see a stack trace of where error is. In our case
   heartslav.js:68 Uncaught (in promise) ReferenceError: DIRECTIONS is not defined
   HeartsFIXME.js:68 Uncaught (in promise) ReferenceError: DIRECTIONS is not defined
 


In real hearts game they use this direction array to map every player to direction (like North) but its not needed, we can just use player index.
Lets just replace DIRECTIONS[index] with index, i.e
  <div class="playertable whiteblock playertable_${index}">


For this new code to work, we need to add a constant at the beginning of the JS file, defining the expected position of each player (right after copyright comment):
<pre>
const DIRECTIONS = {
    3: ['S', 'W', 'E'],
    4: ['S', 'W', 'N', 'E'],
};
</pre>


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


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


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.
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.
Line 191: Line 208:


<pre>
<pre>
/** Table layout **/
:root {
  --h-card-width: 100px;
  --h-card-height: 135px;
  --h-tableau-width: 220px;
  --h-tableau-height: 180px;
}


#player-tables {
#player-tables {
    position: relative;
  position: relative;
    width: 710px;
  width: calc(var(--h-tableau-width) * 3.9);
    height: 340px;
  height: calc(var(--h-tableau-height) * 2.4);
}
}


.playertablename {
.playertablename {
    font-weight: bold;
  font-weight: bold;
}
}


.playertable {
.playertable {
    position: absolute;
  position: absolute;
    text-align: center;
  text-align: center;
    width: 180px;
  width: var(--h-tableau-width);
    height: 130px;
  height: var(--h-tableau-height);
}
}


.playertable_N {
.playertable_0 {
    left: 50%;
  top: 0px;
    top: 0px;
  left: 50%;
    margin-left: -90px; /* half of 180 */
  margin-left: calc(var(--h-tableau-width) / 2 * -1);
}
}
.playertable_S {
 
    left: 50%;
.playertable_1 {
    bottom: 0px;
  left: 0px;
    margin-left: -90px; /* half of 180 */
  top: 50%;
  margin-top: calc(var(--h-tableau-height) / 2 * -1);
}
}
.playertable_W {
.playertable_2 {
    left: 0px;
  right: 0px;
    top: 50%;
  top: 50%;
    margin-top: -55px; /* half of 130 */
  margin-top: calc(var(--h-tableau-height) / 2 * -1);
}
}
.playertable_E {
.playertable_3 {
    right: 0px;
  bottom: 0px;
    top: 50%;
  left: 50%;
    margin-top: -55px; /* half of 130 */
  margin-left: calc(var(--h-tableau-width) / 2 * -1);
}
}
</pre>
</pre>
Line 236: Line 259:
[[File:Heartsla-tpl5.png]]
[[File: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.
<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>
 
Here is some explanations about CSS (if you know everything about css already skip this):
* At top we defined some variables for sizes of cards and player "mats" (which we call tableau)
* We trying to layout mats in kind of diamond shape
* We define positions of our elements using top/bottom/left/right style property
* We used standard technique of centering the element which is use 50% for lets say "left", and then shift by half of size of object to actually center it (margin-left). You can remove margins to see how it look if we did not do that
 
 


<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>
<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 with BGA Cards ==


The BGA framework provides a 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 a class called [[Stock]] and it can be used for any dynamic html "pieces" management that uses
contains a component called [[BgaCards]] and it can be used for any dynamic html "pieces" management and animation.
common sprite images. On the server side we will use the [[Deck]] class which we discuss later.
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 .js file:
If you open cards.jpg in an image viewer you will see that it is a "sprite" image - a 15x4 grid of images stitched together,
which is a very efficient way to transport images. So we will use the card manager class to mark up these images and create
"card" divs for us and place them on the board.
 
First, we need to add dependencies in the .js file:
<pre>
<pre>
define([
define([
    "dojo","dojo/_base/declare",
  "dojo",
    "ebg/core/gamegui",
  "dojo/_base/declare",
    "ebg/counter",
  "ebg/core/gamegui",
    "ebg/stock"     /// <==== HERE, don't forget comma before
  "ebg/counter",
],
  getLibUrl("bga-animations", "1.x"), // the lib uses bga-animations so this is required!
  getLibUrl("bga-cards", "1.x"), // bga-cards itself
], function (dojo, declare, gamegui, counter, BgaAnimations, BgaCards) {
  return declare( // ...
</pre>
</pre>


Then add this to the Javascript constructor, this will define size of our cards
Now we will remove the fake card we added (in .js file) search and remove
<pre>
  <div class="fakecard"></div>
            console.log('hearts constructor');
 
            this.cardwidth = 72;
            this.cardheight = 96;
</pre>


The stock is initialized in the Javascript "setup" method, after we have created the "myhand" element",  like this:
Then we will add initialization code of bga cards and related component in setup method after the template code and before setupNotifications
<pre>
<pre>
    // TODO: Set up your game interface here, according to "gamedatas"
      // create the animation manager, and bind it to the `game.bgaAnimationsActive()` function
      this.animationManager = new BgaAnimations.Manager({
        animationsActive: () => this.bgaAnimationsActive(),
      });


    // Player hand
      const cardWidth = 100;
    this.playerHand = new ebg.stock(); // new stock object for hand
      const cardHeight = 135;
    this.playerHand.create( this, $('myhand'), this.cardwidth, this.cardheight );
</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 we have created before.
      // create the card manager
      this.cardsManager = new BgaCards.Manager({
        animationManager: this.animationManager,
        type: "ha-card", // the "type" of our cards in css
        getId: (card) => card.id,


        cardWidth: cardWidth,
        cardHeight: cardHeight,
        cardBorderRadius: "5%",
        setupFrontDiv: (card, div) => {
          div.dataset.type = card.type; // suit 1..4
          div.dataset.typeArg = card.type_arg; // value 2..14
          div.style.backgroundPositionX = `calc(100% / 14 * (${card.type_arg} - 2))`; // 14 is number of columns in stock image minus 1
          div.style.backgroundPositionY = `calc(100% / 3 * (${card.type} - 1))`; // 3 is number of rows in stock image minus 1
          this.addTooltipHtml(div.id, `tooltip of ${card.type}`);
        },
      });


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.
      // create the stock, in the game setup
      this.handStock = new BgaCards.HandStock(
        this.cardsManager,
        document.getElementById("myhand")
      );
          // TODO: fix handStock
      this.handStock.addCards([
        { id: 1, type: 2, type_arg: 4 }, // 4 of hearts
        { id: 2, type: 3, type_arg: 11 }, // Jack of clubs
      ]);
</pre>


Here's how we tell stock what item types to display (add after .create):
Also we need to add this .css (anywhere), that will map front face of the card to our image (1500% is because this image 15 times bigger than single card on X axis)
<pre>
<pre>
            this.playerHand.image_items_per_row = 13; // 13 images per row
.ha-card-front {
 
  background-size: 1500% auto;
 
  background-image: url("img/cards.jpg");
            // 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);
                }
            }
</pre>
</pre>


And add this function to the utilities section
<pre>
        // Get card unique identifier based on its color and value
        getCardUniqueId : function(color, value) {
            return (color - 1) * 13 + (value - 2);
        },
</pre>


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.
* First we created animation manager which will be used later
* 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.
* Then we define constant with width and height of our cards in pixes
* Then we create the cards manager. We tell it how to get unique id of each card (getId), and how to setup the div representing the front of the card (setupFrontDiv). In this function we set data attributes for type and type_arg which we will use later, and we set background position to show correct part of sprite image.
* Then we create a hand stock component which will represent player's hand. It is attached to div with id "myhand".
* Finally we add two cards into the hand stock just for testing.


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):
Now if you reload you should see two cards in your hand:


<pre>
[[File:Heartsla-tpl6.png|alt=Display two cards in player's hand]] 
// 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 );
</pre>


This will add the card with id 42 and type 16 ( (2-1)*13+(5-2)=16 ).
Now we will add the "stock" object that will control player tableau (add in setup function before setupNotification)
<code>


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.
      // map stocks


If you reload now you should see the 5 of hearts in "your hand".  
      this.tableauStocks = [];
      Object.values(gamedatas.players).forEach((player, index) => {
        // add player tableau stock
        this.tableauStocks[player.id] = new BgaCards.LineStock(
          this.cardsManager,
          document.getElementById(`tableau_${player.id}`)
        );


You may see some weird graphics glitches - its because we did not remove our hack with background from .css yet. You can go to .css file now and remove line with comment "temp hack to see it".
        // TODO: fix tableauStocks
 
        this.tableauStocks[player.id].addCards([
Stock control can handle clicking on items and forms the selection. You can immediately react to selection
          { id: index + 10, type: index + 1, type_arg: index + 2 },
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' );
</code>


[[File:Heartsla-tpl7.png]] 


Then find the Player's action comment section in .js file and add a handler after the comment:
Explanations:
* We go over each player and create component called LineStock to represent player tableau, it will hold a single card
* We assign this into tableauStocks map indexed by player id to use later
* Finally we add a fake card into that stock just to see something


<pre>
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.
    //// Player's action
 
    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;
Let's hook it up. Add this in the setup method in .js file, before // TODO: fix handStock:
          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();
        }
      }
    },
</pre>


''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.''
      this.handStock.setSelectionMode("single");
      this.handStock.onCardClick = (card) => {
        alert("boom!");
      };


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.
Reload the game and click on Card in your hand. You should get "boom".


Now if you reload, open the Javascript Console (F12), and then click on the card in My Hand, you should see:
We will stop for now with client because we need to code some server stuff.
  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 Initialization ==
== Game Database and Game Initialization ==
Line 377: Line 412:
For that we need to a) modify the 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
the existing globals table.
the existing globals table.
==== Database Schema ====
When you develop a game you need to figure out how you store your game pieces in database. This should be maximum 2 tables (like one for cards, items, tokens and meeples and one for counters).
In this game we will be using two tables, the default Deck table (supported by Deck component) and default "state variables" tables which called globals, to store some of integers.
In you never dealt with web servers - the database stores all information about your game and your php (server) code does not exists in memory between users actions.


To modify the schema, first exit your existing game(s). Open '''dbmodel.sql''' file and uncomment the card table creation.
To modify the schema, first exit your existing game(s). Open '''dbmodel.sql''' file and uncomment the card table creation.
Line 393: Line 433:
This is the "card" table which will be managed by the 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:
This is how we map the database to our game:
* card_id: unique id of each card, it will be auto-generated
* card_type: it will be suite of the card 1 to 4 (Spades,Hearts,Clubs,Diamonds).
* card_type_arg: will be "value" of the card, 2 to 14 (2 is 2,...,10 is 10, 11 is Jack, ...)
* card_location: will be location of the card, like "deck", "hand", "tableau" etc
* card_location_arg: will be additional argument for location - the player id if card is in player's hand or tableau.


  -- 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. It is not recommended to modify player table in general.


=== Game State Variables ===
Next we finally get into Game.php class (in modules/php subdir), where the main logic and db interaction would be. Find php constructor which should be  
Next we finally get into Game.php class (in modules/php subdir), 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. Add this code to constructor (replace existing initGameStateLabel if any).
This is first function in a file. Add this code to constructor (replace existing initGameStateLabel if any).
<pre>
<pre>
    public function __construct()
    {
         parent::__construct();
         parent::__construct();
         $this->initGameStateLabels( array(  
         $this->initGameStateLabels(
                        "currentHandType" => 10,
            [
                        "trickColor" => 11,  
                "trick_color" => 11,
                        "alreadyPlayedHearts" => 12,
            ]
                          ) );
        );


         $this->cards = $this->deckFactory->createDeck('card');
         $this->cards = $this->deckFactory->createDeck('card'); // card is the our database name
        // ...
</pre>
</pre>


If you see errors in IDE its because we also have to declared "cards" as class member, add   protected $cards; before the contructor.
If you see errors in IDE its because we also have to declared "cards" as class member, add ''public Deck $cards;'' before the contructor.
Also if you using IDE it will suggest to import Deck class, accept it. If you are using 'vi' just add this import where other imports (use in php) at the begging of the file after namespace declaration.
 
  use Bga\GameFramework\Components\Deck;


Here we are initializing three "Game State Variables" which are variables stored in the database. They are integers.
Here we are initializing three "Game State Variables" which are variables stored in the database. They are integers.
Line 423: Line 471:
The variables are:
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);
*"trick_color": 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.
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->createDeck( "foo" )</i>


Note: lower in the code there is a line that needs to be removed in order for the game to start.  Search for it and remove it now.  
Note: if you have some other leftovers from template like playerEnergy, leave it for now as is.
$this->setGameStateInitialValue("my_first_global_variable", 0);


Since we changed the db, we cannot re-use our existing game, we have to do express stop (from burger menu).
Since we changed the database schema, we cannot re-use our existing game, we have to do express stop (from burger menu).


Then start a new game and make sure it starts, then exit.
Then start a new game and make sure it starts, then exit.
Line 444: Line 488:
</i>
</i>


Code Rev [https://github.com/elaskavaia/bga-heartsla/tree/acd1926a6c09dc9afd0752cb6b78eef17c5dfe5c]
If you game won't even load and you want to stop it:
https://en.doc.boardgamearena.com/Tools_and_tips_of_BGA_Studio#Stopping_Hanging_Game
 


=== Game Setup ===
Now we can go to game initialization '''setupNewGame''' in Game.php. This method is called only once when the game 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 project 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 "// Init global values with their initial values" comment.
<pre>
<pre>
        // Init global values with their initial values
// Init global values with their initial values
 
// Set current trick color to zero (= no trick color)
$this->setGameStateInitialValue('trick_color', 0);
 


        // 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 );
</pre>
</pre>


Here we initialize 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 it's pretty simple.
Next is to create our cards in the database. We have one deck of cards so it's pretty simple.
Insert this after  // TODO: Setup the initial game situation here.
<pre>
<pre>
         // Create cards
         // Create cards
         $cards = [];
         $cards = [];
         foreach (self::$CARD_SUITS as $suit => $suit_info) {
         foreach ($this->card_types["suites"] as $suit => $suit_info) {
             // spade, heart, diamond, club
             // spade, heart, diamond, club
             foreach (self::$CARD_TYPES as $value => $info_value) {
             foreach ($this->card_types["types"] as $value => $info_value) {
                 //  2, 3, 4, ... K, A
                 //  2, 3, 4, ... K, A
                 $cards[] = ['type' => $suit, 'type_arg' => $value, 'nbr' => 1];
                 $cards[] = ['type' => $suit, 'type_arg' => $value, 'nbr' => 1];
             }
             }
         }
         }
         $this->cards->createCards( $cards, 'deck' );
         $this->cards->createCards($cards, 'deck');
</pre>
</pre>


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 ''card_types''.
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 .php and this way it can be accessed from all .php files (and .js if you send it via getAllDatas()).
This information should be stored in .php and this way it can be accessed from all .php files (and .js if you send it via getAllDatas()).
Line 488: Line 532:
Note: originally it was stored in material.inc.php file which is no longer part of default template, when you have a lot of material it makes sence to get it out of Game.php
Note: originally it was stored in material.inc.php file which is no longer part of default template, when you have a lot of material it makes sence to get it out of Game.php


We will edit  Game.php now by adding these lines in constructor
We will edit  Game.php now by adding these lines in constructor (if you already have ''self::$CARD_TYPES'', replace it)


<pre>
<pre>
         self::$CARD_SUITS = [
         $this->card_types = [
             1 => [
             "suites" => [
                'name' => clienttranslate('Spade'),
                1 => [
            ],
                    'name' => clienttranslate('Spade'),
            2 => [
                ],
                 'name' => clienttranslate('Heart'),
                2 => [
                    'name' => clienttranslate('Heart'),
                 ],
                3 => [
                    'name' => clienttranslate('Club'),
                ],
                4 => [
                    'name' => clienttranslate('Diamond'),
                ]
             ],
             ],
             3 => [
             "types" => [
                 'name' => clienttranslate('Club'),
                2 => ['name' => '2'],
            ],
                3 => ['name' => '3'],
            4 => [
                4 => ['name' => '4'],
                 'name' => clienttranslate('Diamond'),
                5 => ['name' => '5'],
                6 => ['name' => '6'],
                7 => ['name' => '7'],
                8 => ['name' => '8'],
                9 => ['name' => '9'],
                 10 => ['name' => '10'],
                11 => ['name' => clienttranslate('J')],
                12 => ['name' => clienttranslate('Q')],
                13 => ['name' => clienttranslate('K')],
                 14 => ['name' => clienttranslate('A')]
             ]
             ]
         ];
         ];


        self::$CARD_TYPES = [
            2 => ['name' => '2'],
            3 => ['name' => '3'],
            4 => ['name' => '4'],
            5 => ['name' => '5'],
            6 => ['name' => '6'],
            7 => ['name' => '7'],
            8 => ['name' => '8'],
            9 => ['name' => '9'],
            10 => ['name' => '10'],
            11 => ['name' => clienttranslate('J')],
            12 => ['name' => clienttranslate('Q')],
            13 => ['name' => clienttranslate('K')],
            14 => ['name' => clienttranslate('A')]
        ];
</pre>
</pre>
   
   
If you pass a value to the client via notification you should always use untranslated strings, and the client will translate it. Function 'clienttranslate' marks the value for translation but does not actually change it for php. For more about this wonderful translation stuff see [[Translations]].
If you pass a value to the client via notification you should always use untranslated strings, and the client will translate it. Function 'clienttranslate' marks the value for translation but does not actually change it for php. For more about this wonderful translation stuff see [[Translations]].


Can also declared these fields in the class
Can also have declared this in the class (replace $CARD_TYPES)
    private static array $CARD_SUITS;
public array $card_types;
     private static array $CARD_TYPES;
 
Reload to make it still works (no errors).
 
==== Dealing Cards ====
After we have initialized our deck, we want to deal 13 at random for each player. Add this after createCards in setupNewGame function in the Game.php file:
<pre>
// Shuffle deck
$this->cards->shuffle('deck');
// Deal 13 cards to each players
$players = $this->loadPlayersBasicInfos();
foreach ($players as $player_id => $player) {
     $this->cards->pickCards(13, 'deck', $player_id);
}
 
</pre>
In the next section we are going to learn how to show those cards to the right players, without exposing other player hands.


== Full game model synchronisation ==
==Full Game Model Synchronization==


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 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
to return all possible data we need to reconstruct the game. This is in the Game.php file.  
 
=== Player's Hand ===
The template for getAllDatas() already takes care of player info. Let's just
add hand and tableau data before we return a result.
add hand and tableau data before we return a result.


<pre>
<pre>
        // Cards in player hand
// Cards in player hand
        $result['hand'] = $this->cards->getCardsInLocation( 'hand', $current_player_id );
$result['hand'] = $this->cards->getCardsInLocation('hand', $current_player_id);
       
 
        // Cards played on the table
// Cards played on the table
        $result['cardsontable'] = $this->cards->getCardsInLocation( 'cardsontable' );
$result['cardsontable'] = $this->cards->getCardsInLocation('cardsontable');
</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 the setup function (which is the receiver of getAllDatas), find // TODO: fix handStock
and replace our hack of putting cards directly into the hand with:


<pre>
<pre>
            // Cards in player's hand
      // Cards in player's hand
            for ( var i in this.gamedatas.hand) {
      this.handStock.addCards(Array.from(Object.values(this.gamedatas.hand)));
                var card = this.gamedatas.hand[i];
}
                var color = card.type;
</pre>
                var value = card.type_arg;
So we added all cards from server to the hand stock. Have to do this ugly array convertion, hopefully they will fix addCards so we don't need to do this.
                this.playerHand.addToStockWithId(this.getCardUniqueId(color, value), card.id);
 
            }
At this point, you have to RESTART a game and each player should see their hand!
 
At any point if code does not work on clinet side, add command "debugger;" in the code. In browser press F12 to get dev tools, then reload.
You will hit breakpoint and can you in browser debugger. Don't forget to remove debugger; code after.
 
=== Cards on Table ===
Now lets fix out tableau, find comment in setup method of .js file  // TODO: fix tableau
and remove stock.addCards... from that loop. But right after this add


            // 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);
            }
</pre>


This should show hand and tableau cards now, except we are missing the '''playCardOnTable''' function. So find the '''getCardUniqueId''' function which
      // Cards played on table
should be in the utilities section and add this after it:
      for (i in this.gamedatas.cardsontable) {
        var card = this.gamedatas.cardsontable[i];
        var player_id = card.location_arg;
        this.tableauStocks[player_id].addCards([card]);
      }


<pre>
If you reload now you can see nothing on the table.
        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) {
Next, we will hook-up clicking on card and test if our animation.
                // 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)) {
Find the "boom" we put in the click handler. Replace with this
                    this.placeOnObject('cardontable_' + player_id, 'myhand_item_' + card_id);
      this.handStock.onCardClick = (card) => {
                    this.playerHand.removeFromStockById(card_id);
        this.tableauStocks[card.location_arg].addCards([card]);
                }
      };
            }


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


For this to work we also need to define the addTableCard
Now if you reload you should be able to click on card from your hand and see it moving,
<pre>
you can click on few cards this way. When you done enjoying the animation, press F5 to get your hand back.
        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>
            `);
        },
</pre>


[[File:Heartsla-sync.png]]


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.  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!)
==State Machine==


<pre>
Stop the game. We are about to work on the game logic.
.playertablecard {
    display: inline-block;
    position: relative;
    margin-top: 5px;
    width: 72px;
    height: 96px;
    /* we remove background-image here */
}


/*** cards on table ***/
You already read [http://www.slideshare.net/boardgamearena/bga-studio-focus-on-bga-game-state-machine Focus on BGA game state machine], so you know that this is the heart of your game logic.
Note: ignore all the source snippets in this presentation, as framework changed, just note the concepts.


.cardontable {
Here are the states we need to build (excluding two more states we will add later to handle the exchange of cards at the beginning of the rounds):
    position: absolute;
    width: 72px;
    height: 96px;
    background-image: url('img/cards.jpg');
}
</pre>


Now to test that it actually works let's deal cards to players during game initialization:
*Cards are dealt to all players (lets call it "NewHand")
*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) ("EndHand")


Add this after createCards in setupNewGame function in the Game.php file
<pre>
        // 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);
        }
</pre>


Now when you start the game you should see 13 cards in your hand!
Note: if you find states.inc.php file in top level directory - delete it now.


We just need to hook-up clicking on card and test if our playCardOnTable works.
=== State Templates ===
We will create just barebones state files first:


Find onPlayerHandSelectionChanged function in the JS file, we should have logging there like  console.log("on playCard "+card_id);
==== States/NewHand.php ====
So after that insert this (Note: this code is for testing we will replace it with server interaction after we test it.):
Let's create our first state - "NewHand". Create a new file under "module/php/States" and name it "'''NewHand.php'''".  
<pre>
<pre>
                    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);
</pre>
Now if you reload 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.


[[File:Heartsla-sync.png]]
<?php
declare(strict_types=1);
namespace Bga\Games\HeartsFIXME\States;


Code Rev [https://github.com/elaskavaia/bga-heartsla/tree/0ed80e254a6d0c29f267b308d1c2016db90fd4f6]
use Bga\Games\HeartsFIXME\Game;
use Bga\GameFramework\StateType;
use Bga\GameFramework\States\GameState;


== State Machine ==
class NewHand extends GameState
{
  public function __construct(protected Game $game)
  {
    parent::__construct(
      $game,
      id: 2, // the idea of the state
      type: StateType::GAME, // This type means that no player is active, and the game will automatically progress
      updateGameProgression: true, // entering this state can update the progress bar of the game
    );
  }


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):
  // The action we do when entering the state
  public function onEnteringState()
  {
    return PlayerTurn::class;
  }
}


* Cards are dealt to all players (lets call it "newHand")
</pre>
* Player is selected who will start a new trick ("newTrick")
You can read more about it here: [[State classes: State directory]]
* 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")


If you use IDE you see few errors:
* First there is no Bga\Games\HeartsFIXME\Game - that is because you games is not called HeartsFIXME, is something like HeartsFooBar - so you change this and namespace to your game name.
* Second it will compain abot NewTrick class - it does not exist yet


The state handling spread across multiple files, so you have to make sure all pieces are connected together.
Let's implement the other states:
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 (remove all template states and replace with these)


==== States/PlayerTurn.php ====
If file exists replace its content.
This action is different because it has an action a player must take. Read the comments in the code below to understand the syntax:
<pre>
<pre>
$machinestates = array(
<?php
    /// New hand
    2 => array(
        "name" => "newHand",
        "description" => "",
        "type" => "game",
        "action" => "stNewHand",
        "updateGameProgression" => true, 
        "transitions" => array( "" => 30 )
    ),   


    21 => array(      
declare(strict_types=1);
        "name" => "giveCards",
 
        "description" => clienttranslate('Some players must choose 3 cards to give to ${direction}'),
namespace Bga\Games\HeartsFIXME\States;
        "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 )
    ),


);
use Bga\Games\HeartsFIXME\Game;
</pre>
use Bga\GameFramework\StateType;
use Bga\GameFramework\States\PossibleAction;
use Bga\GameFramework\States\GameState;


The full details on these fields are in [[Your_game_state_machine:_states.inc.php]].
class PlayerTurn extends GameState
{
    public function __construct(protected Game $game)
    {
        parent::__construct(
            $game,
            id: 31,
            type: StateType::ACTIVE_PLAYER, // This state type means that one player is active and can do actions
            description: clienttranslate('${actplayer} must play a card'), // We tell OTHER players what they are waiting for
            descriptionMyTurn: clienttranslate('${you} must play a card'), // We tell the ACTIVE player what they must do
            // We suround the code with clienttranslate() so that the text is sent to the client for translation (this will enable the game to support other languages)
        );
    }


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.
    #[PossibleAction] // a PHP attribute that tells BGA "this method describes a possible action that the player could take", so that you can call that action from the front (the client)
    public function actPlayCard(int $cardId, int $activePlayerId)
    {
        // TODO: implement logic
        return NextPlayer::class; // after the action, we move to the next player
    }


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)
    public function zombie(int $playerId)
and 1 function for player actions handling.
    {
In Game.php, find 'Game state arguments' section and paste this in:
        // We must implement this so BGA can auto play in the case a player becomes a zombie, but for this tutorial we won't handle this case
<pre>
        throw new \BgaUserException('Not implemented: zombie for player ${player_id}', args: [
    function argGiveCards() {
            'player_id' => $playerId,
         return [];
         ]);
     }
     }
}
</pre>
</pre>
You would also notice the "zombie" method. This would allow BGA to auto-player for the player if they became inactive. This is mandatory, but we will implement this 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.
==== States/NextPlayer.php ====
Important: even when it's a stub, this function must return an array not a scalar.
This state have a couple of different options for what would be the next state:


Let's do stubs for other functions, find the game state actions section in Game.php file and insert these (replace all st* method defined in template)
* If not all players played a card in the current trick - we need to go to '''PlayerTurn''' (for the next player)
* If all players finished the trick but still have cards in their hand - we need to go to '''PlayerTurn'''
* If this is the last trick (no more cards in end) and it's finished, we need to go to '''EndHand'''
 
For now, let's return '''PlayerTurn''' class. We'll implement the logic later.
<pre>
<pre>
    function stNewHand() {
<?php
        // Take back all cards (from any location => null) to deck
declare(strict_types=1);
        $this->cards->moveAllCardsInLocation(null, "deck");
namespace Bga\Games\HeartsFIXME\States;
        $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->notify->player($player_id, 'newHand', '', array ('cards' => $cards ));
        }
        $this->setGameStateValue('alreadyPlayedHearts', 0);
        $this->gamestate->nextState("");
    }


    function stNewTrick() {
use Bga\GameFramework\StateType;
        // New trick: active the player who wins the last trick, or the player who own the club-2 card
use Bga\Games\HeartsFIXME\Game;
        // Reset trick color to 0 (= no color)
use Bga\GameFramework\States\GameState;
        $this->setGameStateInitialValue('trickColor', 0);
        $this->gamestate->nextState();
    }


    function stNextPlayer() {
class NextPlayer extends GameState
        // Active next player OR end the trick and go to the next trick OR end the hand
{
        if ($this->cards->countCardInLocation('cardsontable') == 4) {
  public function __construct(protected Game $game)
            // This is the end of the trick
  {
            // Move all cards to "cardswon" of the given player
    parent::__construct(
            $best_value_player_id = $this->activeNextPlayer(); // TODO figure out winner of trick
      $game,
            $this->cards->moveAllCardsInLocation('cardsontable', 'cardswon', null, $best_value_player_id);
      id: 32,
       
      type: StateType::GAME,
            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() {
  public function onEnteringState()
        $this->gamestate->nextState("nextHand");
  {
    }
    return PlayerTurn::class;
  }
}


</pre>
</pre>
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 (replace all existing act* functions)
==== States/EndHand.php ====
Here too we will have two options for transition, either we play another hand ('''NewHand''') or we finish the game (a reserved id for finishing the game is '''99''').
 
We will implement this logic later. For now let's return '''NewHand'''.
 
<pre>
<pre>
    function actPlayCard(int $card_id) {
<?php
        $player_id = $this->getActivePlayerId();
declare(strict_types=1);
        throw new \BgaUserException(clienttranslate('Not implemented: ${player_id} plays ${card_id}'), [
namespace Bga\Games\HeartsFIXME\States;
                    "player_id" => "$player_id",
 
                    "card_id" => $card_id,
use Bga\GameFramework\StateType;
                ]);
use Bga\Games\HeartsFIXME\Game;
use Bga\GameFramework\States\GameState;
 
class EndHand extends GameState
{
  public function __construct(protected Game $game)
  {
    parent::__construct(
      $game,
      id: 40,
      type: StateType::GAME,
      description: "",
    );
  }
 
  public function onEnteringState()
  {
    // TODO: implement logic
    return NewHand::class;
  }
}
 


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


If your game is running you have to exit it. When we change state machine in the middle of the game usually it won't be good.
Check again that no HeartsFIXME left in the code, if yes replace with game name.


Now the game should start but it would not be any different than before because we have to implement actual interactions.
Remove EndScore.php - don't need it.
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.
Don't start the game yet, it won't load, we have to clean up bunch of template code in the .js


Since we added bunch of different states we need to remove some more templace code, in .js file find onUpdateActionButtons, and remove all functional code, leaving just this
=== Test Your Game is not broken ===
<pre>
We changed state related logic, so we need to restart the game. If the game starts without error we are good. We won't be able to test the interactions yet because we need to implement the client side.
 
Since we added bunch of different states we need to remove some more templace code, in .js file find onUpdateActionButtons, and remove all functional code, leaving just this<pre>
     onUpdateActionButtons: function (stateName, args) {
     onUpdateActionButtons: function (stateName, args) {
       console.log("onUpdateActionButtons: " + stateName, args);
       console.log("onUpdateActionButtons: " + stateName, args);
Line 858: Line 854:
</pre>
</pre>


== Client - Server interactions ==
=== State Logic ===
Now if you RESTART the game, it should not crash and you see 13 cards in your hand
 
==== New Hand ====
We need to:
 
# Move all cards to the deck
# Shuffle the cards
# Deal the cards to the players
 
 
Here's the code insert infro NewHand.php state:
<pre>
    // The action we do when entering the state
    public function onEnteringState()
    {
        $game = $this->game;
        // Take back all cards (from any location => null) to deck
        $game->cards->moveAllCardsInLocation(null, "deck");
        $game->cards->shuffle('deck');
        // Deal 13 cards to each players
        // Create deck, shuffle it and give 13 initial cards
        $players = $game->loadPlayersBasicInfos();
        foreach ($players as $player_id => $player) {
            $cards = $game->cards->pickCards(13, 'deck', $player_id);
            // Notify player about his cards
            $this->notify->player($player_id, 'newHand', '', array('cards' => $cards));
        }
 
        // reset trick color
        $this->game->setGameStateInitialValue('trick_color', 0);
 
        // FIXME: first player one with 2 of clubs
        $first_player = (int) $this->game->getActivePlayerId();
        $this->game->gamestate->changeActivePlayer($first_player);
        return PlayerTurn::class;
    }
</pre>
 
 
==== Next Player ====
Here we can handle the logic of what is the next state we need to move to:<pre>
public function onEnteringState()
  {
    $game = $this->game;
    // Active next player OR end the trick and go to the next trick OR end the hand
    if ($game->cards->countCardInLocation('cardsontable') == 4) {
      // This is the end of the trick
      // Select the winner
      $best_value_player_id = $game->activeNextPlayer(); // TODO figure out winner of trick
 
      // Move all cards to "cardswon" of the given player
      $game->cards->moveAllCardsInLocation('cardsontable', 'cardswon', null, $best_value_player_id);
 
      if ($game->cards->countCardInLocation('hand') == 0) {
        // End of the hand
        return EndHand::class;
      } else {
        // End of the trick
        // Reset trick suite to 0
        $this->game->setGameStateInitialValue('trick_color', 0);
        return PlayerTurn::class;
      }
    } else {
      // Standard case (not the end of the trick)
      // => just active the next player
      $player_id = $game->activeNextPlayer();
      $game->giveExtraTime($player_id);
      return PlayerTurn::class;
    }
  }
</pre>'''Important''': All state actions game or player must return the next state transition (or thrown exception).
 
==== Player Turn ====
We will not implement this yet, but we can throw an exception to check that the interaction is working properly.
<pre>
#[PossibleAction] // a PHP attribute that tells BGA "this method describes a possible action that the player could take", so that you can call that action from the front (the client)
  public function actPlayCard(int $cardId, int $activePlayerId)
  {
    throw new \BgaUserException('Not implemented: ${player_id} played card ${card_id}', args: [
      'player_id' => $activePlayerId,
      'card_id' => $cardId,
    ]);
    return NextPlayer::class; // after the action, we move to the next player
  }
</pre>
 
==Client - Server Interactions==


Now to implement things for real we have hook UI actions to ajax calls, and process notifications sent by the server.
Now to implement things for real we have hook UI actions to ajax calls, and process notifications sent by the server.
Line 865: Line 948:
notification in response, client hooks animations to server notification.
notification in response, client hooks animations to server notification.


Eventhough we can it ajax call the API now called "bgaPerformAction" and this is what you should be using.
So in .js code replace find out handStock.onCardClick and replace  the handler to
      this.handStock.onCardClick = (card) => {
        this.onCardClick(card);
      };


So in .js code replace onPlayerHandSelectionChanged with
Then find onClickCard that exists in template and replace with  
<pre>
<pre>
        onPlayerHandSelectionChanged : function() {
    onCardClick: function (card) {
             var items = this.playerHand.getSelectedItems();
      console.log("onCardClick", card);
      if (!card) return; // hmm
      switch (this.gamedatas.gamestate.name) {
        case "PlayerTurn":
          // Can play a card
          this.bgaPerformAction("actPlayCard", {
             cardId: card.id, // this corresponds to the argument name in php, so it needs to be exactly the same
          });
          break;
        case "GiveCards":
          // Can give cards TODO
          break;
        default: {
          this.handStock.unselectAll();
          break;
        }
      }
    },


            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();
                }
            }
        },
</pre>
</pre>




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


Lets implement it, in Game.php
Lets implement it, in '''PlayerTurn.php'''
<pre>


    function actPlayCard(int $card_id) {
We need to:
        $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->notify->all('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' => self::$CARD_TYPES [$currentCard ['type_arg']] ['name'],
                'color' => $currentCard ['type'],
                'color_displayed' => self::$CARD_SUITS [$currentCard ['type']] ['name'] ));
        // Next player
        $this->gamestate->nextState('playCard');
    }


# Move the card
# Notify all player on the the move
<pre>
#[PossibleAction]
  public function actPlayCard(int $cardId, int $activePlayerId)
  {
    $game = $this->game;
    $game->cards->moveCard($cardId, 'cardsontable', $activePlayerId);
    // TODO: check rules here
    $currentCard = $game->cards->getCard($cardId);
    // And notify
        $game->notify->all('playCard', clienttranslate('${player_name} plays ${value_displayed} ${color_displayed}'), [
            'i18n' => array('color_displayed', 'value_displayed'),
            'card' => $currentCard,
            'player_id' => $activePlayerId,
            'player_name' => $game->getActivePlayerName(),
            'value_displayed' => $game->card_types['types'][$currentCard['type_arg']]['name'],
            'color_displayed' => $game->card_types['suites'][$currentCard['type']]['name']
        ]
        );
    return NextPlayer::class;
  }
</pre>
</pre>


Line 922: Line 1,015:
they are 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. Below the '''setupNotification''' method (which you don't need to touch)
after <code>// TODO: from this point and below, you can write your game notifications handling methods</code>
 
you can put the following code:


<pre>
<pre>
        setupNotifications : function() {
    notif_newHand: function (args) {
            console.log('notifications subscriptions setup');
      // We received a new full hand of 13 cards.
    // table of notif type to delay in milliseconds
      this.handStock.removeAll();
            const notifs = [
      this.handStock.addCards(Array.from(Object.values(args.hand)));
                ['newHand', 1],
    },
                ['playCard', 100],
 
            ];
     notif_playCard: function (args) {
      
      // Play a card on the table
            notifs.forEach((notif) => {
      this.tableauStocks[args.player_id].addCards([args.card]);
                dojo.subscribe(notif[0], this, `notif_${notif[0]}`);
    },
                this.notifqueue.setSynchronous(notif[0], notif[1]);
</pre>
            });


        },
BGA will automatically bind the event to the '''notif_{eventName} handler''' which will receive the "args" you passed from php.


        notif_newHand : function(notif) {
Refresh the page and try to play a card from the correct player. The card should move to the played area. When you refresh - you should still see the card there.
            // We received a new full hand of 13 cards.
Swicth to next player using the arrows near player name and play next card.
            this.playerHand.removeAll();
Just before last card save the game state in "Save 1" slot (buttons in the bottom). These saves game states and you can reload it using "Load 1" later.
It is very handy.
Finish playing the trick. 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.


            for ( var i in notif.args.cards) {
So in '''NextPlayer.php''' file add notification after // Move all cards to "cardswon" of the given player:
                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) {
<pre>
            // Play a card on the table
            this.playCardOnTable(notif.args.player_id, notif.args.color, notif.args.value, notif.args.card_id);
        },
</pre>


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:
            // Move all win cards to cardswon location
            $moved_cards = $game->cards->getCardsInLocation('cardsontable'); // remember for notification what we moved
            $game->cards->moveAllCardsInLocation('cardsontable', 'cardswon', null, $best_value_player_id);


<pre>
            // Notify
             // 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 = $this->loadPlayersBasicInfos();
             $players = $game->loadPlayersBasicInfos();
             $this->notify->all( 'trickWin', clienttranslate('${player_name} wins the trick'), array(
             $game->notify->all('trickWin', clienttranslate('${player_name} wins the trick'), array(
                'player_id' => $best_value_player_id,
                'player_name' => $players[$best_value_player_id]['player_name'],
 
            ));
 
            $game->notify->all('giveAllCardsToPlayer', '', array(
                 'player_id' => $best_value_player_id,
                 'player_id' => $best_value_player_id,
                 'player_name' => $players[ $best_value_player_id ]['player_name']
                 'cards' => $game->cards->getCards(array_keys($moved_cards))
            ) );           
             ));
            $this->notify->all( 'giveAllCardsToPlayer','', array(
                'player_id' => $best_value_player_id
             ) );
</pre>
</pre>


And in .js file add 2 more notification handlers.
You notice that we passes player_name in notification because its used in the message but its pretty redundant,
as we should be able to figure out player_name by player_id.
 
There is a way to fix it.


This is to subscribe in setupNotifications function
in constructor of Game.php uncomment the decorator and remove second part that related to cards, so you will end up with
        /* notification decorator */
 
        $this->notify->addDecorator(function(string $message, array $args) {
            if (isset($args['player_id']) && !isset($args['player_name']) && str_contains($message, '${player_name}')) {
                $args['player_name'] = $this->getPlayerNameById($args['player_id']);
            }
   
            return $args;
        });
Now we can remove player_name as notification argument in NextPlayer.php (and other states) and $players variable because its not used anymore.
 
            $game->notify->all('trickWin', clienttranslate('${player_name} wins the trick'), array(
                'player_id' => $best_value_player_id,
            ));
 
Now lets add these handlers in the .js file to handle our notifications:
<pre>
<pre>
            const notifs = [
    notif_trickWin: async function () {
                ...
      // We do nothing here (just wait in order players can view the 4 cards played before they're gone)
                ['trickWin', 1000],
    },
                ['giveAllCardsToPlayer', 600],
    notif_giveAllCardsToPlayer: async function (args) {
            ];
      // Move all cards on table to given table, then destroy them
      const winner_id = args.player_id;
 
      await this.tableauStocks[winner_id].addCards(
        Array.from(Object.values(args.cards))
      );
      // TODO: cards has to dissapear after
    },
</pre>
</pre>


And these are handlers
Ok we notice that cards that was won bunched up in ugly column and stay on tableau, but they should dissaper after trick is taken.
 
Now lets fix the ugly stock. We can make tableau a bit bigger to fit 4 cards or we should make cards overlap, later makes more sense since making tableau too big will be ugly.
 
I could not figure out how to do overlap in LineStock,  AI thinks that there is attribute cardOverlap that I can set when creatingt stock, but it does not work on LineStock (as on 1.7),
so lets just add css for this in .css file
 
<pre>
<pre>
        notif_trickWin : function(notif) {
.playertable .ha-card ~ .ha-card {
            // We do nothing here (just wait in order players can view the 4 cards played before they're gone.
    margin-left: calc(var(--h-card-width) * -0.8);
        },
}
        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();
            }
        },
</pre>
</pre>


So 'trickWin' notification does not do much except it will delay the processing of next notification by 1 second (1000 ms)
This uses tilda operator that target the sibling, which is essentially all cards except first.
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 want to do something else, send an empty message</i>
If you want to test that it works you can reload you test state using Load 1 button to see the finishing of a trick.
 
 
Now reload to test the trick taking - it is pretty now .
 
Final touch, we need card to dissapear into the void. The void we have to create first.
We need to add another node in the dom for that void stock, on server we called location "cardswon" so lets use same name, change the tableau template in .js file to this
    <div class="playertable whiteblock playertable_${index}">
        <div class="playertablename" style="color:#${player.color};">${player.name}</div>
        <div id="cardswon_${player.id}" class="cardswon"></div>
        <div id="tableau_${player.id}" class="tableau"></div>
    </div>
 
We added cardswon (and class for tableau just in case we need it later).
 
Now in setup method of .js file we need to create stock for this location, in the loop where we adding tableau stock and the end of loop add this code:
        // add void stock
        new BgaCards.VoidStock(
          this.cardsManager,
          document.getElementById(`cardswon_${player.id}`),
          {
            autoPlace: (card) =>
              card.location === "cardswon" && card.location_arg == player.id,
          }
        );
 
If you notice we did not assign this to any variable, this is because we won't need to refer to it, we will use autoPlace feature, where cardManager will know where to place it based on the location from server.
Finally we just have to modify notification handler to add this animation,  this is final version (in .js file)
 
    notif_giveAllCardsToPlayer: async function (args) {
      // Move all cards on table to given table, then destroy them
      const winner_id = args.player_id;
 
      const cards = Array.from(Object.values(args.cards));
      await this.tableauStocks[winner_id].addCards(cards);
      await this.cardsManager.placeCards(cards); // auto-placement
    },
 
So the function is async means it will return Promise. We are doing it so we can wait other animations to complete.
First we adding cards to player tableau, waiting for animation, then adding to our void stock where they are dissapear.


Now after the trick you see all cards move towards the "player's stash".
Now after the trick you see all cards move towards the "player's stash".
The animation is not ideal, so lets at void stock settings to see if can improve it: https://x.boardgamearena.net/data/game-libs/bga-cards/1.0.7/docs/classes/stocks_void-stock.VoidStock.html
Ok, well I could not figure it out, but now you know where docs for these components are.
We will do our CSS hack, in .css add:
.cardswon > .ha-card {
  position: absolute;
  top: 0 !important;
}
==Zombie turn==


== Scoring and End of game handling ==
We will implement a zombie function now because a) we have to do it at some point
b) playing 13 cards from 4 players manually to test this game is super annoying - but we can actually re-use this feature to "auto-play"
 
In '''PlayerTurn.php''' file, replace the zombie function with this code:
<pre>
    public function zombie(int $playerId)
    {
        $game = $this->game;
        // Auto-play a random card from player's hand
        $cards_in_hand = $game->cards->getCardsInLocation('hand', $playerId);
        if (count($cards_in_hand) > 0) {
            $card_to_play = $cards_in_hand[array_rand($cards_in_hand)];
            $game->cards->moveCard($card_to_play['id'], 'cardsontable', $playerId);
            // Notify
            $game->notify->all(
                'playCard',
                clienttranslate('${player_name} auto plays ${value_displayed} ${color_displayed}'),
                [
                    'i18n' => array('color_displayed', 'value_displayed'),
                    'card' => $card_to_play,
                    'player_id' => $playerId,
                    'value_displayed' => $game->card_types['types'][$card_to_play['type_arg']]['name'],
                    'color_displayed' => $game->card_types['suites'][$card_to_play['type']]['name']
                ]
            );
        }
        return NextPlayer::class;
    }
</pre>
 
Now, watch this! Click Debug symbol on top bar (bug) and select function "playAutomatically" (this is actually function in your php file! it starts with debug_),
and select number of moves, i.e. 4.
If your zombie function works correctly you will see player play automatically. To play whole hand it will be 52 moves (13*4).
 
==Scoring and End of game handling==


Now we should calculate scoring and for that we need to actually track who wins the trick.
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.
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.
For which we will use state variable 'trick_color' which we already conveniently created.
 
In '''PlayerTurn.php''' state, add this before notification
<pre>
$currenttrick_color = $game->getGameStateValue('trick_color');
if ($currenttrick_color == 0) $game->setGameStateValue('trick_color', $currentCard['type']);


In Game.php file find the actPlayCard function and add this before notify functions
</pre>
        $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
This will make sure we remember the first suit being played, now we will use it. Modify the '''NextPlayer.php''' state to fix our TODO comment in onEnteringState
<pre>
<pre>
    function stNextPlayer() {
         // Active next player OR end the trick and go to the next trick OR end the hand
         // Active next player OR end the trick and go to the next trick OR end the hand
         if ($this->cards->countCardInLocation('cardsontable') == 4) {
         if ($game->cards->countCardInLocation('cardsontable') == 4) {
             // This is the end of the trick
             // This is the end of the trick
             $cards_on_table = $this->cards->getCardsInLocation('cardsontable');
             $cards_on_table = $game->cards->getCardsInLocation('cardsontable');
             $best_value = 0;
             $best_value = 0;
             $best_value_player_id = null;
             $best_value_player_id = $this->game->getActivePlayerId(); // fallback
             $currentTrickColor = $this->getGameStateValue('trickColor');
             $currenttrick_color = $game->getGameStateValue('trick_color');
             foreach ( $cards_on_table as $card ) {
             foreach ($cards_on_table as $card) {
                // Note: type = card color
                 if ($card['type'] == $currenttrick_color) {   // type is card suite
                 if ($card ['type'] == $currentTrickColor) {
                     if ($best_value_player_id === null || $card['type_arg'] > $best_value) {
                     if ($best_value_player_id === null || $card ['type_arg'] > $best_value) {
                         $best_value_player_id = $card['location_arg']; // location_arg is player who played this card on table
                         $best_value_player_id = $card ['location_arg']; // Note: location_arg = player who played this card on table
                         $best_value = $card['type_arg']; // type_arg is value of the card (2 to 14)
                         $best_value = $card ['type_arg']; // Note: type_arg = value of the card
                     }
                     }
                 }
                 }
             }
             }
           
 
             // Active this player => he's the one who starts the next trick
             // Active this player => he's the one who starts the next trick
             $this->gamestate->changeActivePlayer( $best_value_player_id );
             $this->gamestate->changeActivePlayer($best_value_player_id);
           
 
             // Move all cards to "cardswon" of the given player
             // Move all win cards to cardswon location
             $this->cards->moveAllCardsInLocation('cardsontable', 'cardswon', null, $best_value_player_id);
            $win_location = 'cardswon';
       
             $game->cards->moveAllCardsInLocation('cardsontable', $win_location, null, $best_value_player_id);
            // Notify
             // ... notification is the same as before
             // ... same code here as before
</pre>
</pre>


Line 1,058: Line 1,249:
Lets score -1 point per heart and call it a day. And game will end when somebody goes -100 or below.
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
As UI goes for scoring, the main thing to update is:
we want to show that in the log.
 
In addition scoring can be shown in [[Game_interface_logic:_yourgamename.js#Scoring_dialogs|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).
* The scoring on the mini boards represented by stars
* Show that in the log.  


so in Game.php our stEndHand function will look like
For a real game, you might consider showing the scoring in a [[Game_interface_logic:_yourgamename.js#Scoring_dialogs|Scoring Dialog]] using tableWindow notification, but this is out of scope of this tutorial. You can do that as homework.
 
In '''EndHand.php''':


<pre>
<pre>
    function stEndHand() {
use Bga\GameFramework\NotificationMessage; // add this to the top of the file, together with the other "use" statements
            // Count and score points, then end the game or go to the next hand.
 
...
 
public function onEnteringState()
{
  $game = $this->game;
  // Count and score points, then end the game or go to the next hand.
  $players = $game->loadPlayersBasicInfos();
  // Gets all "hearts" + queen of spades
 
  $player_to_points = array();
  foreach ($players as $player_id => $player) {
    $player_to_points[$player_id] = 0;
  }
 
  $cards = $game->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) {
      $game->playerScore->inc(
        $player_id,
        -$points,
        new NotificationMessage(
          clienttranslate('${player_name} gets ${absInc} hearts and looses ${absInc} points'),
        )
      );
    }
  }
 
  ///// Test if this is the end of the game
  if ($game->playerScore->getMin() <= -100) {
    // Trigger the end of the game !
    return 99; // end game
  }
 
 
  return NewHand::class;
}
</pre>
 
The game should work now. Try to play it!
==Clean Up==
We left some code that comes from template and our first code, we should remove it now.
 
* In .js file remove debugger; statements if any
* Remove debug code from setupNewGame to deal cards, cards are now dealt in stNewHand state handler
        // Shuffle deck
        $this->cards->shuffle('deck');
        // Deal 13 cards to each players
         $players = $this->loadPlayersBasicInfos();
         $players = $this->loadPlayersBasicInfos();
         // Gets all "hearts" + queen of spades
        foreach ($players as $player_id => $player) {
            $this->cards->pickCards(13, 'deck', $player_id);
        }
* Find and remove $playerEnergy variable and it's uses from Game.php (was part of template)
 
==Rule Enforcements==
Now we have a working game, but there is no rule enforcement.
You can implement these rules in the '''actPlayCard''' function of '''PlayerTurn.php'''
<pre>
 
    public function actPlayCard(int $cardId, int $activePlayerId)
    {
        $game = $this->game;
        $card = $game->cards->getCard($cardId);
        if (!$card) {
            throw new \BgaSystemException("Invalid move");
        }
         // Rule checks


         $player_to_points = array ();
         // Check that player has this card in hand
         foreach ( $players as $player_id => $player ) {
         if ($card['location'] != "hand") {
             $player_to_points [$player_id] = 0;
             throw new \BgaUserException(
                clienttranslate('You do not have this card in your hand')
            );
         }
         }
         $cards = $this->cards->getCardsInLocation("cardswon");
         $currenttrick_color = $game->getGameStateValue('trick_color');
        foreach ( $cards as $card ) {
        // Check that player follows suit if possible
            $player_id = $card ['location_arg'];
        if ($currenttrick_color != 0) {
             // Note: 2 = heart
            $has_suit = false;
             if ($card ['type'] == 2) {
            $hand_cards = $game->cards->getCardsInLocation('hand', $activePlayerId);
                 $player_to_points [$player_id] ++;
            foreach ($hand_cards as $hand_card) {
                if ($hand_card['type'] == $currenttrick_color) {
                    $has_suit = true;
                    break;
                }
             }
             if ($has_suit && $card['type'] != $currenttrick_color) {
                 throw new \BgaUserException(
                    clienttranslate('You must follow suit')
                );
             }
             }
         }
         }
         // Apply scores to player
 
         foreach ( $player_to_points as $player_id => $points ) {
 
            if ($points != 0) {
</pre>
                $this->playerScore->inc($player_id, -$points, new NotificationMessage(clienttranslate('${player_name} gets ${absInc} hearts and looses ${absInc} points'))";
You can try to play wrong card now and you will see error!
But you noticed we broke the zombie mode as you cannot play random card anymore and now uThe user cannot play ANY card, there are only some cards they can play but we don't show this information which is annoying.
To fix this we can add a helper function that will return a list of playable cards for a given player.
Add these functions in '''Game.php''' file:
<pre>
    function getPlayableCards($player_id): array
    {
         // Get all data needed to check playable cards at the moment
         $currentTrickColor = $this->getGameStateValue('trick_color');
        $broken_heart = $this->brokenHeart();
        $total_played = $this->cards->countCardInLocation('cardswon') + $this->cards->countCardInLocation('cardsontable');
        $hand = $this->cards->getPlayerHand($player_id);
 
        $playable_card_ids = [];
        $all_ids = array_keys($hand);
 
 
        if ($this->cards->getCardsInLocation('cardsontable', $player_id)) return []; // Already played a card
 
        // Check whether the first card of the hand has been played or not
        // if ($total_played == 0) {
        //    // No cards have been played yet, find and return the starter card only
        //    foreach ($hand as $card) if ($card['type'] == 3 && $card['type_arg'] == 2) return [$card['id']]; // 2 of clubs
        //    return [];
        // } else
        if (!$currentTrickColor) { // First card of the trick
            if ($broken_heart) return $all_ids; // Broken Heart or no limitation, can play any card
            else {
                // Exclude Heart as Heart hasn't been broken yet
                foreach ($hand as $card) if ($card['type'] != 2) $playable_card_ids[] = $card['id'];
                if (!$playable_card_ids) return $all_ids; // All Heart cards!
                else return $playable_card_ids;
             }
             }
        } else {
            // Must follow the lead suit if possible
            $same_suit = false;
            foreach ($hand as $card)
                if ($card['type'] == $currentTrickColor) {
                    $same_suit = true;
                    break;
                }
            if ($same_suit) return $this->getObjectListFromDB("SELECT card_id FROM card WHERE card_type = $currentTrickColor AND card_location = 'hand' AND card_location_arg = $player_id", true); // Has at least 1 card of the same suit
            else return $all_ids;
         }
         }
    }
    function brokenHeart(): bool
    {
        // Check Heart in the played card piles
        return (bool)$this->getUniqueValueFromDB("SELECT count(*) FROM card WHERE card_location = 'cardswon' AND card_type = 2");
    }
    function tableHeart(): bool
    {
        // Check Heart in the current trick
        return (bool)$this->getUniqueValueFromDB("SELECT count(*) FROM card WHERE card_location = 'cardsontable' AND card_type = 2");
    }
</pre>
Now we can use this function in the zombie function of '''PlayerTurn.php''' to pick a random playable card:
<pre>
    public function zombie(int $playerId)
    {
        $playable_cards = $this->game->getPlayableCards($playerId);
        $zombieChoice = $this->getRandomZombieChoice($playable_cards); // random choice over possible moves
        return $this->actPlayCard((int)$zombieChoice, $playerId);
    }
</pre>


         ///// Test if this is the end of the game
And we can significantly simplify our rule check at actPlayCard
         if ($this->playerScore->getMin() <= -100) {
<pre>
             // Trigger the end of the game !
         $game = $this->game;
            $this->gamestate->nextState("endGame");
         $currentCard = $game->cards->getCard($cardId);
             return;
        if (!$currentCard) {
             throw new \BgaSystemException("Invalid move");
        }
        // Rule checks
        $playable_cards = $game->getPlayableCards($activePlayerId);
        if (!in_array($cardId, $playable_cards)) {
             throw new \BgaUserException(clienttranslate("You cannot play this card now"));
         }
         }


       
        $this->gamestate->nextState("nextHand");
    }
</pre>
</pre>
Finally we can send this to the client via state args so the user can see what moves are valid:
In '''PlayerTurn.php''' add this method:
<pre>
    public function getArgs(int $activePlayerId): array
    {
        // Send playable card ids of the active player privately
        return [
            '_private' => [
                $activePlayerId => [
                    'playableCards' => $this->game->getPlayableCards($activePlayerId)
                ],
            ],
        ];
    }   
</pre>
On the client side in the .js file replace the onEnteringState method with this code:
<pre>
    onEnteringState: function (stateName, args) {
      console.log("Entering state: " + stateName, args);
      switch (stateName) {
        case "PlayerTurn":
          if (this.isCurrentPlayerActive()) {
            // Check playable cards received from argPlayerTurn() in php
            const playableCardIds = args.args._private.playableCards.map((x) =>
              parseInt(x)
            );
            const allCards = this.handStock.getCards();
            const playableCards = allCards.filter(
              (card) => playableCardIds.includes(parseInt(card.id)) // never know if we get int or string, this method cares
            );
            this.handStock.setSelectionMode("single", playableCards);
          }
          break;
      }
    },
</pre> 
And ALSO remove the this.handStock.setSelectionMode("single") line from the setup method. If you leave it there it won't work as this is async function that collide with our other async
functions that we are using in the onEnteringState
==Fix first player with 2 of clubs==
Find the comment
// FIXME: first player one with 2 of clubs
and replace with this code:
// first player one with 2 of clubs
$first_player = $this->game->getUniqueValueFromDb("SELECT card_location_arg FROM card WHERE card_location = 'hand' AND card_type = 3 AND card_type_arg = 2");// 2 of clubs
Then we can uncomment our code for getPlayableCards in Game.php:
        //Check whether the first card of the hand has been played or not
        $total_played = $this->cards->countCardInLocation('cardswon') + $this->cards->countCardInLocation('cardsontable');
        if ($total_played == 0) {
            // No cards have been played yet, find and return the starter card only
            foreach ($hand as $card) if ($card['type'] == 3 && $card['type_arg'] == 2) return [$card['id']]; // 2 of clubs
            return $all_ids; // should not happen
        } else
==Spectator support==
A spectator is not a real player but they can watch the game. Most games will require special spectator support, it's one of the steps in the alpha testing checklist.
In this game it's pretty simple, we just hide the hand control in the client
In the .js file in the setup function add this code (after DOM is created):
      // Hide hand zone from spectators
      if (this.isSpectator)
        document.getElementById("myhand_wrap").style.display = "none";
Click Test Spectator at the end of player's panels to test this.
==Improve UI==
We need to fix a few things in the UI still.
===Center Player Areas===
First let's fix the player tables - to make them centered.
In the .css file find #player-tables and change it to this:
#player-tables {
  position: relative;
  width: calc(var(--h-tableau-width) * 3.8);
  height: calc(var(--h-tableau-height) * 2.4);
  margin: auto; // that is a cheap way to make it centered
}
===Better Card Play Animations===
When another player plays a card it kind of just appears on the tableau, we want to make it look like it's coming from the player hand.
We don't actually have any sort of UI location to have a player hand - but we can either put it on the mini player panel or add it to the bottom of the player areas.
Let's try to put this on the mini player panels.
First we need to add a node in the DOM on the player panel and maybe add an icon to represent the hand.
We have access to some BGA icons and font awesome icons https://fontawesome.com/v4/icons, so we can pick one from there:
In the .js file in the template for player tableau and add this at the end of the forEach body:
<pre>
        document.getElementById(`player_panel_content_${player.color}`).innerHTML =
        `<div id="otherhand_${player.id}" class="otherhand"><i class="fa fa-window-restore"></i></div>`;
</pre>
In the .js file replace the notif handler for play with this:
<pre>
    notif_playCard: async function (args) {
      // Play a card on the table
      const playerId = args.player_id;
      let settings = {};
      if (playerId != this.player_id) {
        settings = {
          fromElement: $(`otherhand_${playerId}`),
          toPlaceholder: "grow",
        };
      }
      await this.tableauStocks[playerId].addCard(args.card, settings);
    },
</pre>
What we did here is added a settings parameter for card placement - for cases where it's not our own card to move it from the "hand" area on the mini player board.
Reload and test (use the autoPlay feature to see the animation when the "other" player plays the card).
Now we can also replace the void stock we create with animation to the same "otherhand" area:
<pre>
    notif_giveAllCardsToPlayer: async function (args) {
      // Move all cards from notification to dedicated player area and fade out
      const playerId = args.player_id;
      const cards = Array.from(Object.values(args.cards));
      await this.tableauStocks[playerId].addCards(cards);
      await this.tableauStocks[playerId].removeCards(cards, {
        fadeOut: true,
        slideTo: $(`otherhand_${playerId}`),
      });
    },
</pre>
And in this case we don't really need VoidStock anymore, we can remove it
Delete this
<pre>
        // add void stock
        new BgaCards.VoidStock(
          this.cardsManager,
          document.getElementById(`cardswon_${playerId}`),
          {
            fadeOut: true, // not working
            toPlaceholder: "shrink", // not working
            autoPlace: (card) =>
              card.location === "cardswon" && card.location_arg == playerId,
          }
        );
</pre>
Also can delete related css and DOM element cardswon.
We can also add this in .css to make this symbol centered:
.otherhand {
  position: relative;
  margin: auto;
  text-align: center;
}


===Card Sorting===
It would be nice to sort the cards in hand by suit and value.
You can add sorting
      // create the stock, in the game setup
      this.handStock = new BgaCards.HandStock(
        this.cardsManager,
        document.getElementById("myhand"),
        {
            sort: BgaCards.sort('type', 'type_arg'), // sort by suite then by value
        }
      );


So it should more less work now, including end of game condition. Try to play it!
but you will notice its not sorted right.
Its because of type mismatch. The server sends us strings and bga-cards expects integers.
We have to change this on client or server. I was already doing some ugly convertion on client, so lets just make it official.
We will add 2 functions in utility section that will do the convertions for us:
    ///////////////////////////////////////////////////
    //// Utility methods


== Additional stuff ==
    remapToBgaCardList: function (cards) {
      if (!cards) return [];
      if (cards.type) {
        // actually one card
        return [this.remapToBgaCard(cards)];
      } else if (Array.isArray(cards)) {
        return cards.map((card) => this.remapToBgaCard(card));
      } else {
        return Object.values(cards).map((card) => this.remapToBgaCard(card));
      }
    },
    remapToBgaCard: function (card) {
      // proper casts
      return {
        id: parseInt(card.id),
        type: parseInt(card.type),
        type_arg: parseInt(card.type_arg),
        location: card.location,
        location_arg: parseInt(card.location_arg),
      };
    },
Then in setup change addCards to this
    this.handStock.addCards(this.remapToBgaCardList(this.gamedatas.hand));
and
      // Cards played on table
      for (i in this.gamedatas.cardsontable) {
        var card = this.gamedatas.cardsontable[i];
        var player_id = card.location_arg;
        this.tableauStocks[player_id].addCard(this.remapToBgaCard(card));
      }


The following things were not implemented and can add them yourself by looking at the code of original hearts game:
In notif_newHand replace addCards like this:
    this.handStock.addCards(this.remapToBgaCardList(args.hand));


* Remove debug code from setupNewGame to deal cards, cards are now dealt in stNewHand state handler
In notif_giveAllCardsToPlayer
* Rule checking and rule enforcements in actPlayCard function
      const cards = this.remapToBgaCardList(args.cards);
* Start scoring with 100 points each and end when <= 0
In notif_playCard
* Fix scoring rules with Q of spades and 26 point reverse scoring
      await this.tableauStocks[playerId].addCard(this.remapToBgaCard(args.card), settings);
* First player one with 2 club
 
* Add progress handling
Now sorting should work!
* Add statistics
 
* Add card exchange states
===Tooltips===
* Add game option to start with 75 points instead of 100
We can add tooltips to cards to show their name.
In the .js file find where created silly tooltip with addTooltipHtml and replace with this:
          this.addTooltipHtml(div.id,
            _(this.gamedatas.card_types.types[card.type_arg].name)+ " " +
            _(this.gamedatas.card_types.suites[card.type].name)
          );
Now what is this.gamedatas.card_types? Well that is our "material" of the game which is in our case variable in php, we have to send it to client for this to work.
Since it never changes we send it in getAllDatas method, add this at the end before return:
            $result['card_types'] = $this->card_types;
 
Of course this is very basic tooltips and not even needed in this game, but in real game your want tooltips everywhere!!!
Lets add tooltip to our fake hand symbol also (the "otherhand") (in setup method in .js somewhere in forEach loop over players)
        // add tooltips to player hand symbol
        this.addTooltipHtml(
          `otherhand_${playerId}`,
          _("Placeholder for player's hand")
        );
 
==Game progresstion==
In this game it should be easy, we just need to know if somebody close to -100 points!
Find getGameProgression in Game.php and replace with this:
    public function getGameProgression()
    {
        $min = $this->playerScore->getMin();
        return -1 * $min; // we get close to -100 we get close to 100% game completion
    }
 
 
==Additional stuff==
 
The following things were not implemented and you can add them yourself by looking at the code of the original hearts game:
 
*Mark player who started the hand and add log about what is starting Suite of the trick
*Start scoring with 100 points each and end when <= 0
*Fix scoring rules with Q of spades and 26 point reverse scoring
*Add statistics
*Add card exchange states
*Add game option to start with 75 points instead of 100


== After the tutorial ==
==After the tutorial==
You might want to check another tutorial, or start working on your first real project !
You might want to check another tutorial, or start working on your first real project!


[[Create a game in BGA Studio: Complete Walkthrough]]
[[Create a game in BGA Studio: Complete Walkthrough]]
[[Category:Studio]]
[[Category:Studio]]

Latest revision as of 03:33, 4 December 2025


Game File Reference



Useful Components

Official

  • Deck: a PHP component to manage cards (deck, hands, picking cards, moving cards, shuffle deck, ...).
  • PlayerCounter and TableCounter: PHP components to manage counters.
  • 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).
  • bga-animations : a JS component for animations.
  • bga-cards : a JS component for cards.
  • bga-dice : a JS component for dice.
  • bga-autofit : a JS component to make text fit on a fixed size div.
  • bga-score-sheet : a JS component to help you display an animated score sheet at the end of the game.

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).
  • 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


Hearts Rules

Hearts is a trick-taking card game for four players where the goal is to score the fewest points. Players aim to avoid taking tricks with heart cards (1 point each) and the Queen of Spades (13 points). Each round, 13 cards are dealt, players pass three cards, and the player with the 2 of Clubs starts the first trick. Play continues clockwise, with players needing to follow suit if they can, and the highest card of the lead suit wins the trick. Hearts cannot be played until they are "broken" by a player who can't follow suit and discards a heart, or by a player leading with a heart after they've been broken.

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 (or shorter version of thereof). You can also re-use the project you have created for the "First Steps" tutorial above.

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. Also it will not match exactly with this tutorial for different reasons.


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 .js file (for example replace "Player zone content goes here" to "Hello"), 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).


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.

Note: the game was re-written using new template, the old code is in "oldframework" branch. The new template is in main branch.

The real hearts game (that you can play on BGA) can be found in your FTP home folder, after getting read-only access, go to https://studio.boardgamearena.com/projects, select Already Published and find Hearts to get access (It may not match this tutorial as framework diverged since this game was created and it may not have been updated)

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.

 // Game publisher
   'publisher' => 'Public Domain',
 // Board Game Geek ID of the publisher
   'publisher_bgg_id' => 171,
 // 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 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.

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 [1] into img/cards.jpg folder of your project.

Details about images can be found here: Game art: img directory. If you did not setup auto-sync of files, sync the graphics manually with remote folder (re-sync with your workspace).

Edit .js to add some divs to represent player table and hand area, at the beginning of the setup function


        setup: function( gamedatas )
        {
            console.log( "Starting game setup" );

            document.getElementById('game_play_area').insertAdjacentHTML('beforeend', `
                <div id="myhand_wrap" class="whiteblock">
                    <b id="myhand_label">${_('My hand')}</b>
                    <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 the html snippet we inserted earlier buy adding a line representing a card

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

Edit .css file, add this code (.css file is empty now, only has comments, just tuck this at the end)

.fakecard {
    display: inline-block;
    position: relative;
    margin-top: 5px;
    border-radius: 5%;
    width: 100px;
    height: 135px;
    background-size: calc(100px * 15);
    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 (more less):

Heartsla-tpl3.png


Note: If you don't see the card a) check it was synced to remote folder b) force reload page

Awesome! Now lets do the rest of layout.


Let's complete the game template. You template should have this code, just leave it there


      // Example to add a div on the game area
      document.getElementById("game_play_area").insertAdjacentHTML("beforeend",
                            <div id="player-tables"></div>
                        `
      );

Then change the code following comment "// Setting up player boards" with this

      // Setting up player boards
      const numPlayers = Object.keys(gamedatas.players).length;
      Object.values(gamedatas.players).forEach((player, index) => {
        document.getElementById("player-tables").insertAdjacentHTML(
          "beforeend",
          // we generate this html snippet for each player
          `
    <div class="playertable whiteblock playertable_${DIRECTIONS[index]}">
        <div class="playertablename" style="color:#${player.color};">${player.name}</div>
        <div id="tableau_${player.id}"></div>
    </div>
    `
        );
      });

What we did is we added a template for every players at the table. Now try to reload you game. Oops! it won't load. This is to teach you how it will look like when you have syntax error in your js file. The game will hang loading at 10% or so. How to know what happened? Open dev tools in browser (usually F12) and navigate to Console tab. You will see a stack trace of where error is. In our case

 HeartsFIXME.js:68 Uncaught (in promise) ReferenceError: DIRECTIONS is not defined


In real hearts game they use this direction array to map every player to direction (like North) but its not needed, we can just use player index. Lets just replace DIRECTIONS[index] with index, i.e


Reload. If everything went well you should see this:

Display player space of all players

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

:root {
  --h-card-width: 100px;
  --h-card-height: 135px;
  --h-tableau-width: 220px;
  --h-tableau-height: 180px;
}

#player-tables {
  position: relative;
  width: calc(var(--h-tableau-width) * 3.9);
  height: calc(var(--h-tableau-height) * 2.4);
}

.playertablename {
  font-weight: bold;
}

.playertable {
  position: absolute;
  text-align: center;
  width: var(--h-tableau-width);
  height: var(--h-tableau-height);
}

.playertable_0 {
  top: 0px;
  left: 50%;
  margin-left: calc(var(--h-tableau-width) / 2 * -1);
}

.playertable_1 {
  left: 0px;
  top: 50%;
  margin-top: calc(var(--h-tableau-height) / 2 * -1);
}
.playertable_2 {
  right: 0px;
  top: 50%;
  margin-top: calc(var(--h-tableau-height) / 2 * -1);
}
.playertable_3 {
  bottom: 0px;
  left: 50%;
  margin-left: calc(var(--h-tableau-width) / 2 * -1);
}


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

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

Here is some explanations about CSS (if you know everything about css already skip this):

  • At top we defined some variables for sizes of cards and player "mats" (which we call tableau)
  • We trying to layout mats in kind of diamond shape
  • We define positions of our elements using top/bottom/left/right style property
  • We used standard technique of centering the element which is use 50% for lets say "left", and then shift by half of size of object to actually center it (margin-left). You can remove margins to see how it look if we did not do that



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 with BGA Cards

The BGA framework provides a few out of the box classes to deal with cards. The client side contains a component called BgaCards and it can be used for any dynamic html "pieces" management and animation. 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 15x4 grid of images stitched together, which is a very efficient way to transport images. So we will use the card manager class to mark up these images and create "card" divs for us and place them on the board.

First, we need to add dependencies in the .js file:

define([
  "dojo",
  "dojo/_base/declare",
  "ebg/core/gamegui",
  "ebg/counter",
  getLibUrl("bga-animations", "1.x"), // the lib uses bga-animations so this is required!
  getLibUrl("bga-cards", "1.x"), // bga-cards itself
], function (dojo, declare, gamegui, counter, BgaAnimations, BgaCards) {
  return declare( // ...
 

Now we will remove the fake card we added (in .js file) search and remove


Then we will add initialization code of bga cards and related component in setup method after the template code and before setupNotifications

      // create the animation manager, and bind it to the `game.bgaAnimationsActive()` function
      this.animationManager = new BgaAnimations.Manager({
        animationsActive: () => this.bgaAnimationsActive(),
      });

      const cardWidth = 100;
      const cardHeight = 135;

      // create the card manager
      this.cardsManager = new BgaCards.Manager({
        animationManager: this.animationManager,
        type: "ha-card", // the "type" of our cards in css
        getId: (card) => card.id,

        cardWidth: cardWidth,
        cardHeight: cardHeight,
        cardBorderRadius: "5%",
        setupFrontDiv: (card, div) => {
          div.dataset.type = card.type; // suit 1..4
          div.dataset.typeArg = card.type_arg; // value 2..14
          div.style.backgroundPositionX = `calc(100% / 14 * (${card.type_arg} - 2))`; // 14 is number of columns in stock image minus 1
          div.style.backgroundPositionY = `calc(100% / 3 * (${card.type} - 1))`; // 3 is number of rows in stock image minus 1
          this.addTooltipHtml(div.id, `tooltip of ${card.type}`);
        },
      });

      // create the stock, in the game setup
      this.handStock = new BgaCards.HandStock(
        this.cardsManager,
        document.getElementById("myhand")
      );
          // TODO: fix handStock
      this.handStock.addCards([
        { id: 1, type: 2, type_arg: 4 }, // 4 of hearts
        { id: 2, type: 3, type_arg: 11 }, // Jack of clubs
      ]); 

Also we need to add this .css (anywhere), that will map front face of the card to our image (1500% is because this image 15 times bigger than single card on X axis)

.ha-card-front {
  background-size: 1500% auto;
  background-image: url("img/cards.jpg");
}


Explanations:

  • First we created animation manager which will be used later
  • Then we define constant with width and height of our cards in pixes
  • Then we create the cards manager. We tell it how to get unique id of each card (getId), and how to setup the div representing the front of the card (setupFrontDiv). In this function we set data attributes for type and type_arg which we will use later, and we set background position to show correct part of sprite image.
  • Then we create a hand stock component which will represent player's hand. It is attached to div with id "myhand".
  • Finally we add two cards into the hand stock just for testing.


Now if you reload you should see two cards in your hand:

Display two cards in player's hand

Now we will add the "stock" object that will control player tableau (add in setup function before setupNotification)

     // map stocks
     this.tableauStocks = [];
     Object.values(gamedatas.players).forEach((player, index) => {
       // add player tableau stock
       this.tableauStocks[player.id] = new BgaCards.LineStock(
         this.cardsManager,
         document.getElementById(`tableau_${player.id}`)
       );
       // TODO: fix tableauStocks
       this.tableauStocks[player.id].addCards([
         { id: index + 10, type: index + 1, type_arg: index + 2 },
       ]);
     });


Heartsla-tpl7.png

Explanations:

  • We go over each player and create component called LineStock to represent player tableau, it will hold a single card
  • We assign this into tableauStocks map indexed by player id to use later
  • Finally we add a fake card into that stock just to see something

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, before // TODO: fix handStock:

     this.handStock.setSelectionMode("single");
     this.handStock.onCardClick = (card) => {
       alert("boom!");
     };

Reload the game and click on Card in your hand. You should get "boom".

We will stop for now with client because we need to code some server stuff.


Game Database and Game Initialization

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.

Database Schema

When you develop a game you need to figure out how you store your game pieces in database. This should be maximum 2 tables (like one for cards, items, tokens and meeples and one for counters). In this game we will be using two tables, the default Deck table (supported by Deck component) and default "state variables" tables which called globals, to store some of integers. In you never dealt with web servers - the database stores all information about your game and your php (server) code does not exists in memory between users actions.

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.

This is how we map the database to our game:

  • card_id: unique id of each card, it will be auto-generated
  • card_type: it will be suite of the card 1 to 4 (Spades,Hearts,Clubs,Diamonds).
  • card_type_arg: will be "value" of the card, 2 to 14 (2 is 2,...,10 is 10, 11 is Jack, ...)
  • card_location: will be location of the card, like "deck", "hand", "tableau" etc
  • card_location_arg: will be additional argument for location - the player id if card is in player's hand or tableau.


Game State Variables

Next we finally get into Game.php class (in modules/php subdir), 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 (replace existing initGameStateLabel if any).

    public function __construct()
    {
        parent::__construct();
        $this->initGameStateLabels(
            [
                "trick_color" => 11,
            ]
        );

        $this->cards = $this->deckFactory->createDeck('card'); // card is the our database name
        // ...

If you see errors in IDE its because we also have to declared "cards" as class member, add public Deck $cards; before the contructor. Also if you using IDE it will suggest to import Deck class, accept it. If you are using 'vi' just add this import where other imports (use in php) at the begging of the file after namespace declaration.

 use Bga\GameFramework\Components\Deck;

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:

  • "trick_color": numbers from 1 to 4 that map to card suit (not sure why it's called color; maybe it's a translation from French);

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->createDeck( "foo" )

Note: if you have some other leftovers from template like playerEnergy, leave it for now as is.

Since we changed the database schema, we cannot re-use our existing game, we have to do express stop (from burger menu).

Then 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.)

If you game won't even load and you want to stop it: https://en.doc.boardgamearena.com/Tools_and_tips_of_BGA_Studio#Stopping_Hanging_Game


Game Setup

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 "// Init global values with their initial values" comment.

// Init global values with their initial values

// Set current trick color to zero (= no trick color)
$this->setGameStateInitialValue('trick_color', 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. Insert this after // TODO: Setup the initial game situation here.

        // Create cards
        $cards = [];
        foreach ($this->card_types["suites"] as $suit => $suit_info) {
            // spade, heart, diamond, club
            foreach ($this->card_types["types"] as $value => $info_value) {
                //  2, 3, 4, ... K, A
                $cards[] = ['type' => $suit, '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 card_types. 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 .php and this way it can be accessed from all .php files (and .js if you send it via getAllDatas()).

Note: originally it was stored in material.inc.php file which is no longer part of default template, when you have a lot of material it makes sence to get it out of Game.php

We will edit Game.php now by adding these lines in constructor (if you already have self::$CARD_TYPES, replace it)

        $this->card_types = [
            "suites" => [
                1 => [
                    'name' => clienttranslate('Spade'),
                ],
                2 => [
                    'name' => clienttranslate('Heart'),
                ],
                3 => [
                    'name' => clienttranslate('Club'),
                ],
                4 => [
                    'name' => clienttranslate('Diamond'),
                ]
            ],
            "types" => [
                2 => ['name' => '2'],
                3 => ['name' => '3'],
                4 => ['name' => '4'],
                5 => ['name' => '5'],
                6 => ['name' => '6'],
                7 => ['name' => '7'],
                8 => ['name' => '8'],
                9 => ['name' => '9'],
                10 => ['name' => '10'],
                11 => ['name' => clienttranslate('J')],
                12 => ['name' => clienttranslate('Q')],
                13 => ['name' => clienttranslate('K')],
                14 => ['name' => clienttranslate('A')]
            ]
        ];

If you pass a value to the client via notification you should always use untranslated strings, and the client will translate it. Function 'clienttranslate' marks the value for translation but does not actually change it for php. For more about this wonderful translation stuff see Translations.

Can also have declared this in the class (replace $CARD_TYPES)

public array $card_types;

Reload to make it still works (no errors).

Dealing Cards

After we have initialized our deck, we want to deal 13 at random for each player. 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) {
    $this->cards->pickCards(13, 'deck', $player_id);
}

In the next section we are going to learn how to show those cards to the right players, without exposing other player hands.

Full Game Model Synchronization

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.

Player's Hand

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), find // TODO: fix handStock and replace our hack of putting cards directly into the hand with:

      // Cards in player's hand
      this.handStock.addCards(Array.from(Object.values(this.gamedatas.hand)));
}

So we added all cards from server to the hand stock. Have to do this ugly array convertion, hopefully they will fix addCards so we don't need to do this.

At this point, you have to RESTART a game and each player should see their hand!

At any point if code does not work on clinet side, add command "debugger;" in the code. In browser press F12 to get dev tools, then reload. You will hit breakpoint and can you in browser debugger. Don't forget to remove debugger; code after.

Cards on Table

Now lets fix out tableau, find comment in setup method of .js file // TODO: fix tableau and remove stock.addCards... from that loop. But right after this add


     // Cards played on table
     for (i in this.gamedatas.cardsontable) {
       var card = this.gamedatas.cardsontable[i];
       var player_id = card.location_arg;
       this.tableauStocks[player_id].addCards([card]);
     }

If you reload now you can see nothing on the table.

Next, we will hook-up clicking on card and test if our animation.

Find the "boom" we put in the click handler. Replace with this

     this.handStock.onCardClick = (card) => {
        this.tableauStocks[card.location_arg].addCards([card]);
     };


Now if you reload 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


State Machine

Stop the game. We are about to work on the game logic.

You already read Focus on BGA game state machine, so you know that this is the heart of your game logic. Note: ignore all the source snippets in this presentation, as framework changed, just note the concepts.

Here are the states we need to build (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 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) ("EndHand")


Note: if you find states.inc.php file in top level directory - delete it now.

State Templates

We will create just barebones state files first:

States/NewHand.php

Let's create our first state - "NewHand". Create a new file under "module/php/States" and name it "NewHand.php".


<?php
declare(strict_types=1);
namespace Bga\Games\HeartsFIXME\States;

use Bga\Games\HeartsFIXME\Game;
use Bga\GameFramework\StateType;
use Bga\GameFramework\States\GameState;

class NewHand extends GameState
{
  public function __construct(protected Game $game)
  {
    parent::__construct(
      $game,
      id: 2, // the idea of the state
      type: StateType::GAME, // This type means that no player is active, and the game will automatically progress
      updateGameProgression: true, // entering this state can update the progress bar of the game
    );
  }

  // The action we do when entering the state
  public function onEnteringState()
  {
    return PlayerTurn::class;
  }
}

You can read more about it here: State classes: State directory

If you use IDE you see few errors:

  • First there is no Bga\Games\HeartsFIXME\Game - that is because you games is not called HeartsFIXME, is something like HeartsFooBar - so you change this and namespace to your game name.
  • Second it will compain abot NewTrick class - it does not exist yet

Let's implement the other states:


States/PlayerTurn.php

If file exists replace its content. This action is different because it has an action a player must take. Read the comments in the code below to understand the syntax:

<?php

declare(strict_types=1);

namespace Bga\Games\HeartsFIXME\States;

use Bga\Games\HeartsFIXME\Game;
use Bga\GameFramework\StateType;
use Bga\GameFramework\States\PossibleAction;
use Bga\GameFramework\States\GameState;

class PlayerTurn extends GameState
{
    public function __construct(protected Game $game)
    {
        parent::__construct(
            $game,
            id: 31,
            type: StateType::ACTIVE_PLAYER, // This state type means that one player is active and can do actions
            description: clienttranslate('${actplayer} must play a card'), // We tell OTHER players what they are waiting for
            descriptionMyTurn: clienttranslate('${you} must play a card'), // We tell the ACTIVE player what they must do
            // We suround the code with clienttranslate() so that the text is sent to the client for translation (this will enable the game to support other languages)
        );
    }

    #[PossibleAction] // a PHP attribute that tells BGA "this method describes a possible action that the player could take", so that you can call that action from the front (the client)
    public function actPlayCard(int $cardId, int $activePlayerId)
    {
        // TODO: implement logic
        return NextPlayer::class; // after the action, we move to the next player
    }

    public function zombie(int $playerId)
    {
        // We must implement this so BGA can auto play in the case a player becomes a zombie, but for this tutorial we won't handle this case
        throw new \BgaUserException('Not implemented: zombie for player ${player_id}', args: [
            'player_id' => $playerId,
        ]);
    }
}


You would also notice the "zombie" method. This would allow BGA to auto-player for the player if they became inactive. This is mandatory, but we will implement this later.

States/NextPlayer.php

This state have a couple of different options for what would be the next state:

  • If not all players played a card in the current trick - we need to go to PlayerTurn (for the next player)
  • If all players finished the trick but still have cards in their hand - we need to go to PlayerTurn
  • If this is the last trick (no more cards in end) and it's finished, we need to go to EndHand

For now, let's return PlayerTurn class. We'll implement the logic later.

<?php
declare(strict_types=1);
namespace Bga\Games\HeartsFIXME\States;

use Bga\GameFramework\StateType;
use Bga\Games\HeartsFIXME\Game;
use Bga\GameFramework\States\GameState;

class NextPlayer extends GameState
{
  public function __construct(protected Game $game)
  {
    parent::__construct(
      $game,
      id: 32,
      type: StateType::GAME,
    );
  }

  public function onEnteringState()
  {
    return PlayerTurn::class;
  }
}

States/EndHand.php

Here too we will have two options for transition, either we play another hand (NewHand) or we finish the game (a reserved id for finishing the game is 99).

We will implement this logic later. For now let's return NewHand.

<?php
declare(strict_types=1);
namespace Bga\Games\HeartsFIXME\States;

use Bga\GameFramework\StateType;
use Bga\Games\HeartsFIXME\Game;
use Bga\GameFramework\States\GameState;

class EndHand extends GameState
{
  public function __construct(protected Game $game)
  {
    parent::__construct(
      $game,
      id: 40,
      type: StateType::GAME,
      description: "",
    );
  }

  public function onEnteringState()
  {
    // TODO: implement logic
    return NewHand::class;
  }
}


Check again that no HeartsFIXME left in the code, if yes replace with game name.

Remove EndScore.php - don't need it.

Don't start the game yet, it won't load, we have to clean up bunch of template code in the .js

Test Your Game is not broken

We changed state related logic, so we need to restart the game. If the game starts without error we are good. We won't be able to test the interactions yet because we need to implement the client side.

Since we added bunch of different states we need to remove some more templace code, in .js file find onUpdateActionButtons, and remove all functional code, leaving just this
    onUpdateActionButtons: function (stateName, args) {
      console.log("onUpdateActionButtons: " + stateName, args);

      if (this.isCurrentPlayerActive()) {
        switch (stateName) {
          case "playerTurn":
            break;
        }
      }
    },

State Logic

Now if you RESTART the game, it should not crash and you see 13 cards in your hand

New Hand

We need to:

  1. Move all cards to the deck
  2. Shuffle the cards
  3. Deal the cards to the players


Here's the code insert infro NewHand.php state:

    // The action we do when entering the state
    public function onEnteringState()
    {
        $game = $this->game;
        // Take back all cards (from any location => null) to deck
        $game->cards->moveAllCardsInLocation(null, "deck");
        $game->cards->shuffle('deck');
        // Deal 13 cards to each players
        // Create deck, shuffle it and give 13 initial cards
        $players = $game->loadPlayersBasicInfos();
        foreach ($players as $player_id => $player) {
            $cards = $game->cards->pickCards(13, 'deck', $player_id);
            // Notify player about his cards
            $this->notify->player($player_id, 'newHand', '', array('cards' => $cards));
        }

        // reset trick color
        $this->game->setGameStateInitialValue('trick_color', 0);

        // FIXME: first player one with 2 of clubs
        $first_player = (int) $this->game->getActivePlayerId();
        $this->game->gamestate->changeActivePlayer($first_player);
        return PlayerTurn::class;
    }


Next Player

Here we can handle the logic of what is the next state we need to move to:
public function onEnteringState()
  {
    $game = $this->game;
    // Active next player OR end the trick and go to the next trick OR end the hand
    if ($game->cards->countCardInLocation('cardsontable') == 4) {
      // This is the end of the trick
      // Select the winner
      $best_value_player_id = $game->activeNextPlayer(); // TODO figure out winner of trick

      // Move all cards to "cardswon" of the given player
      $game->cards->moveAllCardsInLocation('cardsontable', 'cardswon', null, $best_value_player_id);

      if ($game->cards->countCardInLocation('hand') == 0) {
        // End of the hand
        return EndHand::class;
      } else {
        // End of the trick
        // Reset trick suite to 0 
        $this->game->setGameStateInitialValue('trick_color', 0);
        return PlayerTurn::class;
      }
    } else {
      // Standard case (not the end of the trick)
      // => just active the next player
      $player_id = $game->activeNextPlayer();
      $game->giveExtraTime($player_id);
      return PlayerTurn::class;
    }
  }
Important: All state actions game or player must return the next state transition (or thrown exception).

Player Turn

We will not implement this yet, but we can throw an exception to check that the interaction is working properly.

#[PossibleAction] // a PHP attribute that tells BGA "this method describes a possible action that the player could take", so that you can call that action from the front (the client)
  public function actPlayCard(int $cardId, int $activePlayerId)
  {
    throw new \BgaUserException('Not implemented: ${player_id} played card ${card_id}', args: [
      'player_id' => $activePlayerId,
      'card_id' => $cardId,
    ]);
    return NextPlayer::class; // after the action, we move to the next player
  }

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 find out handStock.onCardClick and replace the handler to

     this.handStock.onCardClick = (card) => {
        this.onCardClick(card);
     };

Then find onClickCard that exists in template and replace with

    onCardClick: function (card) {
      console.log("onCardClick", card);
      if (!card) return; // hmm
      switch (this.gamedatas.gamestate.name) {
        case "PlayerTurn":
          // Can play a card
          this.bgaPerformAction("actPlayCard", {
            cardId: card.id, // this corresponds to the argument name in php, so it needs to be exactly the same
          });
          break;
        case "GiveCards":
          // Can give cards TODO
          break;
        default: {
          this.handStock.unselectAll();
          break;
        }
      }
    },



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

Lets implement it, in PlayerTurn.php

We need to:

  1. Move the card
  2. Notify all player on the the move
#[PossibleAction]
  public function actPlayCard(int $cardId, int $activePlayerId)
  {
    $game = $this->game;
    $game->cards->moveCard($cardId, 'cardsontable', $activePlayerId);
    // TODO: check rules here
    $currentCard = $game->cards->getCard($cardId);
    // And notify
        $game->notify->all('playCard', clienttranslate('${player_name} plays ${value_displayed} ${color_displayed}'), [
            'i18n' => array('color_displayed', 'value_displayed'),
            'card' => $currentCard,
            'player_id' => $activePlayerId,
            'player_name' => $game->getActivePlayerName(),
            'value_displayed' => $game->card_types['types'][$currentCard['type_arg']]['name'],
            'color_displayed' => $game->card_types['suites'][$currentCard['type']]['name']
        ]
        );
    return NextPlayer::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 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. Below the setupNotification method (which you don't need to touch) after // TODO: from this point and below, you can write your game notifications handling methods

you can put the following code:

    notif_newHand: function (args) {
      // We received a new full hand of 13 cards.
      this.handStock.removeAll();
      this.handStock.addCards(Array.from(Object.values(args.hand)));
    },

    notif_playCard: function (args) {
      // Play a card on the table
      this.tableauStocks[args.player_id].addCards([args.card]);
    },

BGA will automatically bind the event to the notif_{eventName} handler which will receive the "args" you passed from php.

Refresh the page and try to play a card from the correct player. The card should move to the played area. When you refresh - you should still see the card there. Swicth to next player using the arrows near player name and play next card. Just before last card save the game state in "Save 1" slot (buttons in the bottom). These saves game states and you can reload it using "Load 1" later. It is very handy. Finish playing the trick. 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 in NextPlayer.php file add notification after // Move all cards to "cardswon" of the given player:



            // Move all win cards to cardswon location
            $moved_cards = $game->cards->getCardsInLocation('cardsontable'); // remember for notification what we moved
            $game->cards->moveAllCardsInLocation('cardsontable', 'cardswon', null, $best_value_player_id);

            // 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 = $game->loadPlayersBasicInfos();
            $game->notify->all('trickWin', clienttranslate('${player_name} wins the trick'), array(
                'player_id' => $best_value_player_id,
                'player_name' => $players[$best_value_player_id]['player_name'],

            ));

            $game->notify->all('giveAllCardsToPlayer', '', array(
                'player_id' => $best_value_player_id,
                'cards' => $game->cards->getCards(array_keys($moved_cards))
            ));

You notice that we passes player_name in notification because its used in the message but its pretty redundant, as we should be able to figure out player_name by player_id.

There is a way to fix it.

in constructor of Game.php uncomment the decorator and remove second part that related to cards, so you will end up with

       /* notification decorator */
       $this->notify->addDecorator(function(string $message, array $args) {
           if (isset($args['player_id']) && !isset($args['player_name']) && str_contains($message, '${player_name}')) {
               $args['player_name'] = $this->getPlayerNameById($args['player_id']);
           }
   
           return $args;
       });

Now we can remove player_name as notification argument in NextPlayer.php (and other states) and $players variable because its not used anymore.

           $game->notify->all('trickWin', clienttranslate('${player_name} wins the trick'), array(
               'player_id' => $best_value_player_id,
           ));

Now lets add these handlers in the .js file to handle our notifications:

    notif_trickWin: async function () {
      // We do nothing here (just wait in order players can view the 4 cards played before they're gone)
    },
     notif_giveAllCardsToPlayer: async function (args) {
      // Move all cards on table to given table, then destroy them
      const winner_id = args.player_id;

      await this.tableauStocks[winner_id].addCards(
        Array.from(Object.values(args.cards))
      );
      // TODO: cards has to dissapear after
    },

Ok we notice that cards that was won bunched up in ugly column and stay on tableau, but they should dissaper after trick is taken.

Now lets fix the ugly stock. We can make tableau a bit bigger to fit 4 cards or we should make cards overlap, later makes more sense since making tableau too big will be ugly.

I could not figure out how to do overlap in LineStock, AI thinks that there is attribute cardOverlap that I can set when creatingt stock, but it does not work on LineStock (as on 1.7), so lets just add css for this in .css file

.playertable .ha-card ~ .ha-card {
    margin-left: calc(var(--h-card-width) * -0.8);
}

This uses tilda operator that target the sibling, which is essentially all cards except first.

If you want to test that it works you can reload you test state using Load 1 button to see the finishing of a trick.


Now reload to test the trick taking - it is pretty now .

Final touch, we need card to dissapear into the void. The void we have to create first. We need to add another node in the dom for that void stock, on server we called location "cardswon" so lets use same name, change the tableau template in .js file to this

${player.name}

We added cardswon (and class for tableau just in case we need it later).

Now in setup method of .js file we need to create stock for this location, in the loop where we adding tableau stock and the end of loop add this code:

       // add void stock
       new BgaCards.VoidStock(
         this.cardsManager,
         document.getElementById(`cardswon_${player.id}`),
         {
           autoPlace: (card) =>
             card.location === "cardswon" && card.location_arg == player.id,
         }
       );

If you notice we did not assign this to any variable, this is because we won't need to refer to it, we will use autoPlace feature, where cardManager will know where to place it based on the location from server. Finally we just have to modify notification handler to add this animation, this is final version (in .js file)

   notif_giveAllCardsToPlayer: async function (args) {
     // Move all cards on table to given table, then destroy them
     const winner_id = args.player_id;
     const cards = Array.from(Object.values(args.cards));
     await this.tableauStocks[winner_id].addCards(cards);
     await this.cardsManager.placeCards(cards); // auto-placement
   },

So the function is async means it will return Promise. We are doing it so we can wait other animations to complete. First we adding cards to player tableau, waiting for animation, then adding to our void stock where they are dissapear.

Now after the trick you see all cards move towards the "player's stash". The animation is not ideal, so lets at void stock settings to see if can improve it: https://x.boardgamearena.net/data/game-libs/bga-cards/1.0.7/docs/classes/stocks_void-stock.VoidStock.html Ok, well I could not figure it out, but now you know where docs for these components are. We will do our CSS hack, in .css add:

.cardswon > .ha-card {
  position: absolute;
  top: 0 !important;
} 

Zombie turn

We will implement a zombie function now because a) we have to do it at some point b) playing 13 cards from 4 players manually to test this game is super annoying - but we can actually re-use this feature to "auto-play"

In PlayerTurn.php file, replace the zombie function with this code:

    public function zombie(int $playerId)
    {
        $game = $this->game;
        // Auto-play a random card from player's hand
        $cards_in_hand = $game->cards->getCardsInLocation('hand', $playerId);
        if (count($cards_in_hand) > 0) {
            $card_to_play = $cards_in_hand[array_rand($cards_in_hand)];
            $game->cards->moveCard($card_to_play['id'], 'cardsontable', $playerId);
            // Notify
            $game->notify->all(
                'playCard',
                clienttranslate('${player_name} auto plays ${value_displayed} ${color_displayed}'),
                [
                    'i18n' => array('color_displayed', 'value_displayed'),
                    'card' => $card_to_play,
                    'player_id' => $playerId,
                    'value_displayed' => $game->card_types['types'][$card_to_play['type_arg']]['name'],
                    'color_displayed' => $game->card_types['suites'][$card_to_play['type']]['name']
                ]
            );
        }
        return NextPlayer::class;
    }

Now, watch this! Click Debug symbol on top bar (bug) and select function "playAutomatically" (this is actually function in your php file! it starts with debug_), and select number of moves, i.e. 4. If your zombie function works correctly you will see player play automatically. To play whole hand it will be 52 moves (13*4).

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 'trick_color' which we already conveniently created.

In PlayerTurn.php state, add this before notification

$currenttrick_color = $game->getGameStateValue('trick_color');
if ($currenttrick_color == 0) $game->setGameStateValue('trick_color', $currentCard['type']);

This will make sure we remember the first suit being played, now we will use it. Modify the NextPlayer.php state to fix our TODO comment in onEnteringState

        // Active next player OR end the trick and go to the next trick OR end the hand
        if ($game->cards->countCardInLocation('cardsontable') == 4) {
            // This is the end of the trick
            $cards_on_table = $game->cards->getCardsInLocation('cardsontable');
            $best_value = 0;
            $best_value_player_id = $this->game->getActivePlayerId(); // fallback 
            $currenttrick_color = $game->getGameStateValue('trick_color');
            foreach ($cards_on_table as $card) {
                if ($card['type'] == $currenttrick_color) {   // type is card suite
                    if ($best_value_player_id === null || $card['type_arg'] > $best_value) {
                        $best_value_player_id = $card['location_arg']; // location_arg is player who played this card on table
                        $best_value = $card['type_arg']; // type_arg is value of the card (2 to 14)
                    }
                }
            }

            // Active this player => he's the one who starts the next trick
            $this->gamestate->changeActivePlayer($best_value_player_id);

            // Move all win cards to cardswon location
            $win_location = 'cardswon';
            $game->cards->moveAllCardsInLocation('cardsontable', $win_location, null, $best_value_player_id);
            // ... notification is the same 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
  • Show that in the log.

For a real game, you might consider showing the scoring in a Scoring Dialog using tableWindow notification, but this is out of scope of this tutorial. You can do that as homework.

In EndHand.php:

use Bga\GameFramework\NotificationMessage; // add this to the top of the file, together with the other "use" statements

...

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

  $player_to_points = array();
  foreach ($players as $player_id => $player) {
    $player_to_points[$player_id] = 0;
  }

  $cards = $game->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) {
      $game->playerScore->inc(
        $player_id,
        -$points,
        new NotificationMessage(
          clienttranslate('${player_name} gets ${absInc} hearts and looses ${absInc} points'),
        )
      );
    }
  }

  ///// Test if this is the end of the game
  if ($game->playerScore->getMin() <= -100) {
    // Trigger the end of the game !
    return 99; // end game
  }


  return NewHand::class;
}

The game should work now. Try to play it!

Clean Up

We left some code that comes from template and our first code, we should remove it now.

  • In .js file remove debugger; statements if any
  • Remove debug code from setupNewGame to deal cards, cards are now dealt in stNewHand state handler
       // Shuffle deck
       $this->cards->shuffle('deck');
       // Deal 13 cards to each players
       $players = $this->loadPlayersBasicInfos();
       foreach ($players as $player_id => $player) {
           $this->cards->pickCards(13, 'deck', $player_id);
       }
  • Find and remove $playerEnergy variable and it's uses from Game.php (was part of template)

Rule Enforcements

Now we have a working game, but there is no rule enforcement. You can implement these rules in the actPlayCard function of PlayerTurn.php

  
    public function actPlayCard(int $cardId, int $activePlayerId)
    {
        $game = $this->game;
        $card = $game->cards->getCard($cardId);
        if (!$card) {
            throw new \BgaSystemException("Invalid move");
        }
        // Rule checks

        // Check that player has this card in hand
        if ($card['location'] != "hand") {
            throw new \BgaUserException(
                clienttranslate('You do not have this card in your hand')
            );
        }
        $currenttrick_color = $game->getGameStateValue('trick_color');
        // Check that player follows suit if possible
        if ($currenttrick_color != 0) {
            $has_suit = false;
            $hand_cards = $game->cards->getCardsInLocation('hand', $activePlayerId);
            foreach ($hand_cards as $hand_card) {
                if ($hand_card['type'] == $currenttrick_color) {
                    $has_suit = true;
                    break;
                }
            }
            if ($has_suit && $card['type'] != $currenttrick_color) {
                throw new \BgaUserException(
                    clienttranslate('You must follow suit')
                );
            }
        }


You can try to play wrong card now and you will see error! But you noticed we broke the zombie mode as you cannot play random card anymore and now uThe user cannot play ANY card, there are only some cards they can play but we don't show this information which is annoying. To fix this we can add a helper function that will return a list of playable cards for a given player. Add these functions in Game.php file:

    function getPlayableCards($player_id): array
    {
        // Get all data needed to check playable cards at the moment
        $currentTrickColor = $this->getGameStateValue('trick_color');
        $broken_heart = $this->brokenHeart();
        $total_played = $this->cards->countCardInLocation('cardswon') + $this->cards->countCardInLocation('cardsontable');
        $hand = $this->cards->getPlayerHand($player_id);

        $playable_card_ids = [];
        $all_ids = array_keys($hand);


        if ($this->cards->getCardsInLocation('cardsontable', $player_id)) return []; // Already played a card

        // Check whether the first card of the hand has been played or not
        // if ($total_played == 0) {
        //     // No cards have been played yet, find and return the starter card only
        //     foreach ($hand as $card) if ($card['type'] == 3 && $card['type_arg'] == 2) return [$card['id']]; // 2 of clubs
        //     return [];
        // } else
        if (!$currentTrickColor) { // First card of the trick
            if ($broken_heart) return $all_ids; // Broken Heart or no limitation, can play any card
            else {
                // Exclude Heart as Heart hasn't been broken yet
                foreach ($hand as $card) if ($card['type'] != 2) $playable_card_ids[] = $card['id'];
                if (!$playable_card_ids) return $all_ids; // All Heart cards!
                else return $playable_card_ids;
            }
        } else {
            // Must follow the lead suit if possible
            $same_suit = false;
            foreach ($hand as $card)
                if ($card['type'] == $currentTrickColor) {
                    $same_suit = true;
                    break;
                }
            if ($same_suit) return $this->getObjectListFromDB("SELECT card_id FROM card WHERE card_type = $currentTrickColor AND card_location = 'hand' AND card_location_arg = $player_id", true); // Has at least 1 card of the same suit

            else return $all_ids;
        }
    }

    function brokenHeart(): bool
    {
        // Check Heart in the played card piles
        return (bool)$this->getUniqueValueFromDB("SELECT count(*) FROM card WHERE card_location = 'cardswon' AND card_type = 2");
    }

    function tableHeart(): bool
    {
        // Check Heart in the current trick
        return (bool)$this->getUniqueValueFromDB("SELECT count(*) FROM card WHERE card_location = 'cardsontable' AND card_type = 2");
    }

Now we can use this function in the zombie function of PlayerTurn.php to pick a random playable card:

    public function zombie(int $playerId)
    {
        $playable_cards = $this->game->getPlayableCards($playerId);
        $zombieChoice = $this->getRandomZombieChoice($playable_cards); // random choice over possible moves
        return $this->actPlayCard((int)$zombieChoice, $playerId);
    }

And we can significantly simplify our rule check at actPlayCard

        $game = $this->game;
        $currentCard = $game->cards->getCard($cardId);
        if (!$currentCard) {
            throw new \BgaSystemException("Invalid move");
        }
        // Rule checks
        $playable_cards = $game->getPlayableCards($activePlayerId);
        if (!in_array($cardId, $playable_cards)) {
            throw new \BgaUserException(clienttranslate("You cannot play this card now"));
        }

Finally we can send this to the client via state args so the user can see what moves are valid: In PlayerTurn.php add this method:

    public function getArgs(int $activePlayerId): array
    {
        // Send playable card ids of the active player privately
        return [
            '_private' => [
                $activePlayerId => [
                    'playableCards' => $this->game->getPlayableCards($activePlayerId)
                ],
            ],
        ];
    }    

On the client side in the .js file replace the onEnteringState method with this code:

    onEnteringState: function (stateName, args) {
      console.log("Entering state: " + stateName, args);
      switch (stateName) {
        case "PlayerTurn":
          if (this.isCurrentPlayerActive()) {
            // Check playable cards received from argPlayerTurn() in php

            const playableCardIds = args.args._private.playableCards.map((x) =>
              parseInt(x)
            ); 

            const allCards = this.handStock.getCards();
            const playableCards = allCards.filter(
              (card) => playableCardIds.includes(parseInt(card.id)) // never know if we get int or string, this method cares
            );
            this.handStock.setSelectionMode("single", playableCards);
          }
          break;
      }
    },

And ALSO remove the this.handStock.setSelectionMode("single") line from the setup method. If you leave it there it won't work as this is async function that collide with our other async functions that we are using in the onEnteringState

Fix first player with 2 of clubs

Find the comment

// FIXME: first player one with 2 of clubs

and replace with this code:

// first player one with 2 of clubs
$first_player = $this->game->getUniqueValueFromDb("SELECT card_location_arg FROM card WHERE card_location = 'hand' AND card_type = 3 AND card_type_arg = 2");// 2 of clubs
Then we can uncomment our code for getPlayableCards in Game.php:
       //Check whether the first card of the hand has been played or not
       $total_played = $this->cards->countCardInLocation('cardswon') + $this->cards->countCardInLocation('cardsontable');
       if ($total_played == 0) {
           // No cards have been played yet, find and return the starter card only
           foreach ($hand as $card) if ($card['type'] == 3 && $card['type_arg'] == 2) return [$card['id']]; // 2 of clubs
           return $all_ids; // should not happen
       } else

Spectator support

A spectator is not a real player but they can watch the game. Most games will require special spectator support, it's one of the steps in the alpha testing checklist. In this game it's pretty simple, we just hide the hand control in the client

In the .js file in the setup function add this code (after DOM is created):

     // Hide hand zone from spectators
     if (this.isSpectator)
       document.getElementById("myhand_wrap").style.display = "none";

Click Test Spectator at the end of player's panels to test this.

Improve UI

We need to fix a few things in the UI still.

Center Player Areas

First let's fix the player tables - to make them centered. In the .css file find #player-tables and change it to this:

#player-tables {
 position: relative;
 width: calc(var(--h-tableau-width) * 3.8);
 height: calc(var(--h-tableau-height) * 2.4);
 margin: auto; // that is a cheap way to make it centered
}

Better Card Play Animations

When another player plays a card it kind of just appears on the tableau, we want to make it look like it's coming from the player hand. We don't actually have any sort of UI location to have a player hand - but we can either put it on the mini player panel or add it to the bottom of the player areas. Let's try to put this on the mini player panels. First we need to add a node in the DOM on the player panel and maybe add an icon to represent the hand. We have access to some BGA icons and font awesome icons https://fontawesome.com/v4/icons, so we can pick one from there:

In the .js file in the template for player tableau and add this at the end of the forEach body:

         document.getElementById(`player_panel_content_${player.color}`).innerHTML = 
         `<div id="otherhand_${player.id}" class="otherhand"><i class="fa fa-window-restore"></i></div>`;

In the .js file replace the notif handler for play with this:

    notif_playCard: async function (args) {
      // Play a card on the table
      const playerId = args.player_id;
      let settings = {};
      if (playerId != this.player_id) {
        settings = {
          fromElement: $(`otherhand_${playerId}`),
          toPlaceholder: "grow",
        };
      }
      await this.tableauStocks[playerId].addCard(args.card, settings);
    },

What we did here is added a settings parameter for card placement - for cases where it's not our own card to move it from the "hand" area on the mini player board. Reload and test (use the autoPlay feature to see the animation when the "other" player plays the card).

Now we can also replace the void stock we create with animation to the same "otherhand" area:

    notif_giveAllCardsToPlayer: async function (args) {
      // Move all cards from notification to dedicated player area and fade out
      const playerId = args.player_id;

      const cards = Array.from(Object.values(args.cards));
      await this.tableauStocks[playerId].addCards(cards);
      await this.tableauStocks[playerId].removeCards(cards, {
        fadeOut: true,
        slideTo: $(`otherhand_${playerId}`),
      });
    },

And in this case we don't really need VoidStock anymore, we can remove it Delete this

        // add void stock
        new BgaCards.VoidStock(
          this.cardsManager,
          document.getElementById(`cardswon_${playerId}`),
          {
            fadeOut: true, // not working
            toPlaceholder: "shrink", // not working
            autoPlace: (card) =>
              card.location === "cardswon" && card.location_arg == playerId,
          }
        );

Also can delete related css and DOM element cardswon.


We can also add this in .css to make this symbol centered:

.otherhand {
  position: relative;
  margin: auto;
  text-align: center;
}

Card Sorting

It would be nice to sort the cards in hand by suit and value. You can add sorting

     // create the stock, in the game setup
     this.handStock = new BgaCards.HandStock(
       this.cardsManager,
       document.getElementById("myhand"),
       {
            sort: BgaCards.sort('type', 'type_arg'), // sort by suite then by value
       }
     ); 

but you will notice its not sorted right. Its because of type mismatch. The server sends us strings and bga-cards expects integers. We have to change this on client or server. I was already doing some ugly convertion on client, so lets just make it official. We will add 2 functions in utility section that will do the convertions for us:

   ///////////////////////////////////////////////////
   //// Utility methods
   remapToBgaCardList: function (cards) {
     if (!cards) return [];
     if (cards.type) {
       // actually one card
       return [this.remapToBgaCard(cards)];
     } else if (Array.isArray(cards)) {
       return cards.map((card) => this.remapToBgaCard(card));
     } else {
       return Object.values(cards).map((card) => this.remapToBgaCard(card));
     }
   },
   remapToBgaCard: function (card) {
     // proper casts
     return {
       id: parseInt(card.id),
       type: parseInt(card.type),
       type_arg: parseInt(card.type_arg),
       location: card.location,
       location_arg: parseInt(card.location_arg),
     };
   },

Then in setup change addCards to this

   this.handStock.addCards(this.remapToBgaCardList(this.gamedatas.hand));

and

     // Cards played on table
     for (i in this.gamedatas.cardsontable) {
       var card = this.gamedatas.cardsontable[i];
       var player_id = card.location_arg;
       this.tableauStocks[player_id].addCard(this.remapToBgaCard(card));
     }

In notif_newHand replace addCards like this:

   this.handStock.addCards(this.remapToBgaCardList(args.hand));

In notif_giveAllCardsToPlayer

     const cards = this.remapToBgaCardList(args.cards);

In notif_playCard

     await this.tableauStocks[playerId].addCard(this.remapToBgaCard(args.card), settings);

Now sorting should work!

Tooltips

We can add tooltips to cards to show their name. In the .js file find where created silly tooltip with addTooltipHtml and replace with this:

         this.addTooltipHtml(div.id, 
            _(this.gamedatas.card_types.types[card.type_arg].name)+ " " +
            _(this.gamedatas.card_types.suites[card.type].name) 
         );

Now what is this.gamedatas.card_types? Well that is our "material" of the game which is in our case variable in php, we have to send it to client for this to work. Since it never changes we send it in getAllDatas method, add this at the end before return:

           $result['card_types'] = $this->card_types;

Of course this is very basic tooltips and not even needed in this game, but in real game your want tooltips everywhere!!! Lets add tooltip to our fake hand symbol also (the "otherhand") (in setup method in .js somewhere in forEach loop over players)

       // add tooltips to player hand symbol
       this.addTooltipHtml(
         `otherhand_${playerId}`,
         _("Placeholder for player's hand")
       );

Game progresstion

In this game it should be easy, we just need to know if somebody close to -100 points! Find getGameProgression in Game.php and replace with this:

   public function getGameProgression()
   {
       $min = $this->playerScore->getMin();
       return -1 * $min; // we get close to -100 we get close to 100% game completion
   }


Additional stuff

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

  • Mark player who started the hand and add log about what is starting Suite of the trick
  • Start scoring with 100 points each and end when <= 0
  • Fix scoring rules with Q of spades and 26 point reverse scoring
  • Add statistics
  • Add card exchange states
  • Add game option to start with 75 points instead of 100

After the tutorial

You might want to check another tutorial, or start working on your first real project!

Create a game in BGA Studio: Complete Walkthrough