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

Tutorial roadtothreehoundred: Difference between revisions

From Board Game Arena
Jump to navigation Jump to search
No edit summary
No edit summary
 
(2 intermediate revisions by the same user not shown)
Line 25: Line 25:
         <nowiki></div></nowiki>
         <nowiki></div></nowiki>
     <nowiki></div></nowiki>
     <nowiki></div></nowiki>
          <!-- END mainplboard -->
            <!-- END mainplboard -->
   
   
     <nowiki><div id="rt300_opponents" class="rt300_zone"></nowiki>
     <nowiki><div id="rt300_opponents" class="rt300_zone"></nowiki>
              <!-- BEGIN opponentBoard -->
                <!-- BEGIN opponentBoard -->
         <nowiki><div id="rt300_mainplboard_{PLAYER_ID}" class="rt300_zone" style="--player-color: #{PLAYER_COLOR}"></nowiki>
         <nowiki><div id="rt300_mainplboard_{PLAYER_ID}" class="rt300_zone" style="--player-color: #{PLAYER_COLOR}"></nowiki>
             <nowiki><div id="rt300_playerboard_{PLAYER_ID}" class="rt300_playerboard" style="order: 0"></nowiki>
             <nowiki><div id="rt300_playerboard_{PLAYER_ID}" class="rt300_playerboard" style="order: 0"></nowiki>
Line 41: Line 41:
             <nowiki></div></nowiki>
             <nowiki></div></nowiki>
         <nowiki></div></nowiki>
         <nowiki></div></nowiki>
              <!-- END opponentBoard -->
                <!-- END opponentBoard -->
     <nowiki></div></nowiki>
     <nowiki></div></nowiki>
   
   
Line 475: Line 475:
   
   
         }
         }
iii) getAllDatas() should also get this new field, so we replace "players" code by this:
/***
        $result = array();
   
        $current_player_id = self::getCurrentPlayerId();    // !! We must only return informations visible by this player !!
   
        // Get information about players
        // Note: you can retrieve some extra field you added for "player" table in "dbmodel.sql" if you need it.
        $sql = "SELECT player_id id, player_score score FROM player ";
        $result['players'] = self::getCollectionFromDb( $sql );
***/
        $result = array();
        // Get information about players
        // Note: you can retrieve some extra field you added for "player" table in "dbmodel.sql" if you need it.
        $result["players"] = DBManager::getAllPlayerDatas();
iv) we set the pass count on .js file too
            ///////////////////////////////////////////////////////////////////////
            // Extend the player panel
            // First the wrapper
            // Generate from template
            var panelWrapperItem = this.format_block("jstpl_panelItemWrapper", {
                plId: player_id,
            });
            // Add it to the player board
            dojo.place(panelWrapperItem, "player_board_" + player_id, "last");
v) pass count will show in page thru HTML template. In the script section of .tpl file
    var jstpl_panelItemWrapper = '<nowiki><div class="rt300_grid_wrapper" id="panelItem_wrapper_${plId}"> <div id="rt300_pass_count_${plId}" class="rt300_pass_count">          </div></nowiki> <nowiki></div></nowiki>';
Finally in DBManage we add all database related functions for the states coding added in this section:
Finally in DBManage we add all database related functions for the states coding added in this section:
  public static function getActivePlayers() {
  public static function getActivePlayers() {
Line 508: Line 538:
         $this->gamestate->nextState("next");
         $this->gamestate->nextState("next");
     }
     }
== Dice ==
Possible player actions will depend on the result of dice roll. So we need to define this object and its logic before we implement player actions.
Possible player actions will depend on the result of dice roll. So we need to define this object and its logic before we implement player actions.


Line 620: Line 652:
             <nowiki></div></nowiki>
             <nowiki></div></nowiki>
         <nowiki></div></nowiki>
         <nowiki></div></nowiki>
So we have one class for each of the 3 dice, one classe for each die side, and for each side as many classes as dots on the side.
So we have one class for each of the 3 dice, one classe for each die side, and for each side as many classes as dots on the side. We'll see how all this is used later on. For now, in .css we define class for dice, dot and side, and for selected dice:
.dice {
    display: flex;
    width: 100px;
    height: 100px;
    transform-style: preserve-3d;
    transition: transform 1s;
}
.dot {
    position: absolute;
    width: 20px;
    height: 20px;
    margin: -10px 5px 5px -10px;
    border-radius: 20px;
    background-color: #46a12a;
    box-shadow: inset 2px 2px #46a12a;
}
.side {
    position: absolute;
    background-color: #fff;
    border-radius: 5px;
    width: 100px;
    height: 100px;
    border: 1px solid #e5e5e5;
    text-align: center;
    line-height: 2em;
}
.dice.used > div.side {
    background-color: darkgray;
    border: 1px solid darkgray;
}
In .js we set basic Dice functions:
/** Game class */
var Dice = /** @class */ (function (_super) {
    __extends(Dice, _super);
    function Dice() {
        return _super.call(this) || this;
    }
    Dice.setDice = function (diceOne, diceTwo, diceThree) {
        this.diceOne = parseInt(diceOne);
        this.diceTwo = parseInt(diceTwo);
        this.diceThree = parseInt(diceThree);
        var elDiceOne = $("dice1");
        var elDiceTwo = $("dice2");
        var elDiceThree = $("dice3");
        console.log(diceOne + " " + diceTwo + " " + diceThree);
        Dice.reset();
    };
    Dice.reset = function () {
        var elDiceOne = $("dice1");
        var elDiceTwo = $("dice2");
        var elDiceThree = $("dice3");
        dojo.removeClass(elDiceOne, "used");
        dojo.removeClass(elDiceTwo, "used");
        dojo.removeClass(elDiceThree, "used");
    };
    return Dice;
}(RootReference));
/// <reference path="base/GameBasics.ts"/>
In game.php.initGameStateLabels():
            [
            "dice_value_one" => 11,
            "dice_value_two" => 12,
            "dice_value_three" => 13,
            ]
Now let's get the dice rolling at game set-up. For that we implement dice functions on server side in module\Dice.php:
<?php
class Dice extends APP_GameClass {
    private static function instance() {
        return roadtothreehoundredjngm::$instance;
    }
    public static function rollDice($withNotification = false) {
        // Roll them
        $diceOne = bga_rand(1, 6);
        $diceTwo = bga_rand(1, 6);
        $diceThree = bga_rand(1, 6);
        // Store
        self::instance()->setGameStateValue("dice_value_one", $diceOne);
        self::instance()->setGameStateValue("dice_value_two", $diceTwo);
        self::instance()->setGameStateValue("dice_value_three", $diceThree);
        // Notify
        if ($withNotification) {
            NotificationManager::diceRolled($diceOne, $diceTwo, $diceThree);
        }
    }
    public static function getDice() {
        return [
            "diceOne" => self::instance()->getGameStateValue("dice_value_one"),
            "diceTwo" => self::instance()->getGameStateValue("dice_value_two"),
            "diceThree" => self::instance()->getGameStateValue("dice_value_three"),
        ];
    }
}
?>
This will only work if Dice.php is added to material.inc.php:
require_once "modules/Dice.php";
We'll need to define a public variable in game.php to send things back and forward with Dice.php:
    public static $instance;
Then in construct():
        self::$instance = $this;
   
        self::initGameStateLabels( array(
            //    "my_first_global_variable" => 10,
            //    "my_second_global_variable" => 11,
            //      ...
            //    "my_first_game_variant" => 100,
            //    "my_second_game_variant" => 101,
            //      ...
           
                "dice_value_one" => 11,
                "dice_value_two" => 12,
                "dice_value_three" => 13,
               
            ) );
In setupNewGame:
        /************ Start the game initialization *****/
        // Init global values with their initial values
        Dice::rollDice(true);
and in getAllDatas()
        $result["dice"] = Dice::getDice();
Let's add notification to players for every time we roll dice. In modules/Dice.php, add to rollDice()
        // Notify
        if ($withNotification) {
            NotificationManager::diceRolled($diceOne, $diceTwo, $diceThree);
        }
As you might have guessed, we'll manage all server notifications from /modules/NotificationManager.php
 
Add this php file to material.inc.php
require_once "modules/NotificationManager.php";
NotificationManager.php
<?php
class NotificationManager extends APP_GameClass {
    private static function instance() {
        return roadtothreehoundredjngm::$instance;
    }
    public static function diceRolled($diceOne, $diceTwo, $diceThree) {
        self::instance()->notifyAllPlayers("notifDiceRolled", clienttranslate("Dice get rolled with ${diceOne}, ${diceTwo}, ${diceThree}."), [
            "diceOne" => $diceOne,
            "diceTwo" => $diceTwo,
            "diceThree" => $diceThree,
        ]);
    }
}
?>
In .js counterpart we already have setupNotifications() to catch notifications coming from server, we simply need to add this notification to the list:
        var notifs = array(
            //Add notifications here
            ["DiceRolled"],
        );
Now we implement this function in javascript:
    GameBody.prototype.notifDiceRolled = function (notif) {
        Dice.setDice(notif.args.diceOne, notif.args.diceTwo, notif.args.diceThree);
    };
Run the game and check that the results of the roll now show in the game log.
 
== Player actions ==

Latest revision as of 03:14, 28 March 2024

This tutorial will guide you through the basics of creating a simple game on BGA Studio, through the example of Road To Three Hundred

<<INSERT CONTENT>>

Set up the board

Edit .tpl to add divs in the HTML for the main board, which is divided in three: area for the dice (we will code details later), area for this player, and area for the other player.

<div id="rt300_mainboard">
    <div id="rt300_spacer" class="rt300_zone"></div>

    <div id="rt300_dice">

    </div>

    <div id="rt300_mainplboard" class="rt300_zone" style="--player-color: #{PLAYER_COLOR}">
        <div id="rt300_playerboard_{PLAYER_ID}" class="rt300_playerboard" style="order: 0">
            <div class="rt300_playerheader">
                <div class="rt300_nameholder">
                    <div id="rt300_plname_{PLAYER_ID}" class="rt300_plname">{PLAYER_NAME}</div>
                </div>
            </div>

            <div id="rt300_metropad_{PLAYER_ID}" class="rt300_metropad" style="opacity: 1">
            </div>
        </div>
    </div>

    <div id="rt300_opponents" class="rt300_zone">
        <div id="rt300_mainplboard_{PLAYER_ID}" class="rt300_zone" style="--player-color: #{PLAYER_COLOR}">
            <div id="rt300_playerboard_{PLAYER_ID}" class="rt300_playerboard" style="order: 0">
                <div class="rt300_playerheader">
                    <div class="rt300_nameholder">
                        <div id="rt300_plname_{PLAYER_ID}" class="rt300_plname">{PLAYER_NAME}</div>
                    </div>
                </div>

                <div id="rt300_metropad_{PLAYER_ID}" class="rt300_metropad" style="opacity: 1">
                </div>
            </div>
        </div>
    </div>

</div>

Edit .css to set the div sizes and positions and upload "field.png" in the 'img' folder of your SFTP access to show the image of the board as background for both this player and the other player (smaller).

You can find the board image here : <<UPLOAD FILE>>

#rt300_mainboard {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
    gap: 1em;
}


#rt300_mainplboard {
    --padsize: 60vh;
    --padratio: 1;
}

.rt300_playerboard {
    border-left: 1px solid gray;
    border-top: 1px solid gray;
    filter: drop-shadow(2px 4px 6px black);
    position: relative;
}

.rt300_zone {
    align-items: center;
    display: flex;
    flex-direction: column;
    justify-content: space-evenly;
    padding: 1vh;
}

#rt300_opponents {
    --padsize: 19vh;
    --padratio: 1;
    display: flex;
    justify-content: space-evenly;
    width: 20vh;
}


@media only screen and (max-width: 560px) {
    #rt300_spacer {
        display: none;
    }
}

.rt300_playerheader {
    background-color: #46a12a;
    height: calc(var(--padsize) * var(--padratio) * 0.05);
    position: relative;
    width: calc(var(--padsize) * var(--padratio));
}
#rt300_dice {
    display: flex;
    flex-direction: column;
    justify-content: space-evenly;
    align-items: center;
    filter: drop-shadow(2px 4px 6px black);
}


.rt300_nameholder {
    align-items: center;
    display: flex;
    font-family: Indie Flower, sans-serif;
    font-weight: bolder;
    height: 38%;
    justify-content: center;
    left: 33%;
    position: absolute;
    top: 29%;
    width: 31%;
}

.rt300_plname {
    font-size: calc(var(--padsize) * var(--padratio) * 0.04);
    height: fit-content;

    color: var(--player-color);
}

.rt300_metropad {
    background-image: url(img/field.png);
    background-size: contain;
    background-repeat: no-repeat;
    height: calc(var(--padsize) * var(--padratio) * 1.252);
    position: relative;
    width: calc(var(--padsize) * var(--padratio));
}

You can run the game to check the boards are being shown.

Set up the backend of your game

Edit dbmodel.sql to create a table for board squares. We need coordinates for each square and a field to store their status.

CREATE TABLE `board` (
  `player_id` int(11) NOT NULL,
  `col` int(11) NOT NULL,
  `row` int(11) NOT NULL,
  `status` int(11) NOT NULL
);
ALTER TABLE `board` ADD PRIMARY KEY(`player_id`, `col`, `row`);

Edit .game.php->setupNewGame() to insert the empty squares (6x6) with coordinates into the database.

        $sql1 = "INSERT INTO board (player_id, row, col, status) VALUES ";
        $values = [];
        foreach ($players as $player_id => $player) {
            for ($r = 1; $r <= 6; $r++) {
                for ($c = 1; $c <= 6; $c++) {
                    $values[] = "('" . $player_id . "', '$r', '$c', 0) ";
                }
            }
        }
        $sql1 .= implode($values, ",");
        self::DbQuery($sql1);

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

        $result["board"] = [];
        foreach ($result["players"] as $playerInfo) {
            $result["board"][$playerInfo["id"]] = DBManager::getBoardForPlayer($playerInfo["id"]);
        }

We define the DBManager class in a separate module modules\DBManager.php to keep a clean code.

<?php
class DBManager extends APP_GameClass {
    public static function getBoardForPlayer($player_id, $useUndoTable = false) {
        $sql = "SELECT col, row, status FROM board WHERE player_id = $player_id ORDER BY col, row; ";
        if ($useUndoTable) {
            $sql = "SELECT col, row, status FROM board_undo WHERE player_id = $player_id ORDER BY col, row; ";
        }
        return self::getObjectListFromDB($sql);
    }
}
?>

We will add more database management related functions to this file later on.

With class extends, we construct the game in the GameBody variable definition. Now we add construction for playerBoards[]:

var GameBody = /** @class */ (function (_super) {
    __extends(GameBody, _super);
    function GameBody() {
        var _this = 
        //console.log('GameBody constructor');
        _super.call(this) || this;

        _this.playerBoards = [];

Likewise, we set up game data in GameBody,prototype.setup funtion. Now set up the player boards:

    GameBody.prototype.setup = function (gamedatas) {
        ///////////////////////////////////////////////////////////////////////////////////////////
        // Debug
        console.log("Starting game setup");
        ///////////////////////////////////////////////////////////////////////////////////////////
        // Setting up player boards
        for (var player_id in gamedatas.players) {
            var player = gamedatas.players[player_id];

            var playerBoard = new PlayerBoard(player, gamedatas.board[player_id]);
            this.playerBoards[player_id] = playerBoard;

As we did with DBManager, we define the PlayerBoard class in a separate module modules\PlayerBoard.php to keep a clean code.

<?php
class PlayerBoard extends APP_GameClass {
    public int $playerId;
    public int $playerPassed;
    public $board = [];
    public $useUndoTable;

    function __construct($playerId) {
        // Store values
        $this->playerId = $playerId;

        // Get playerboard
        $boardInfo = DBManager::getBoardForPlayer($playerId);
        $this->board = [];
        foreach ($boardInfo as $element) {
            $cell = new PlayerBoardCell($this->playerId, $element["col"], $element["row"], $element["status"]);
            $this->board[] = $cell;
        }
    }

}

?>

and in PlayerBoardCell.php:

<?php
class PlayerBoardCell extends APP_GameClass {
    public int $col;
    public int $row;
    public int $status;
    public int $playerId;

    function __construct($playerId, $col, $row, $status) {
        $this->playerId = intval($playerId);
        $this->col = intval($col);
        $this->row = intval($row);
        $this->status = intval($status);
    }
}
?>

To make sure functions in modules are reachable from the game, add a call in material.inc.php:

require_once "modules/PlayerBoardCell.php";
require_once "modules/PlayerBoard.php";
require_once "modules/DBManager.php";

Run the game now, simply to test that nothing goes south (you will no changes with respects to the previous checkpoint).

Set up the front-end of your game

Instead of using the .js file available in the blank game template, we will use new class that extends bga core game class with more functionality:

REPLACE TEMPLATE .JS FILE BY THIS: <<LINK TO roadtothreehoundredjngm - INITIAL>>

The data you returned in $result["board"][] in .game.php->getAllDatas() needs to be made available in your .js. so that the squares layer that will be used to get click events and to display used squares is set up. This means we need to define PlayerBoard and PlayerBoiardCell classes and functions also in .js file:

var PlayerBoard = /** @class */ (function (_super) {

    __extends(PlayerBoard, _super);
    function PlayerBoard(playerInfo, boardInfo) {
        var _this = _super.call(this) || this;
        _this.clientStateArgs = {};
        // Store values
        _this.playerId = playerInfo.id;
        _this.board = [];
        boardInfo.forEach(function (element) {
            _this.board.push(new PlayerBoardCell(_this.playerId, element.col, element.row, element.status));
        });
        return _this;
    }
    return PlayerBoard;
}(RootReference));
/// <reference path="base/RootReference.ts"/>

var PlayerBoardCell = /** @class */ (function (_super) {
    __extends(PlayerBoardCell, _super);
    function PlayerBoardCell(playerId, col, row, status) {
        var _this = _super.call(this) || this;
        _this.col = parseInt(col);
        _this.row = parseInt(row);
        _this.status = parseInt(status);
        if (col >= 1 && col <= 6 && row >= 1 && row <= 6) {
            _this.element = $("".concat(playerId, "_").concat(col, "_").concat(row));

/*** THIS GIVES ERROR: Cannot read properties of null (reading 'dataset')
            // NEEDS TO BE DEBUGGED
            // ?? WHAT ARE THE IMPLICATIONS OF MISSING THIS CODE ??
            //_this.element.dataset.status = _this.status.toString();
***/
        }
        return _this;
    }
    return PlayerBoardCell;
}(RootReference));
/// <reference path="RootReference.ts"/>

Run the game now, simply to test that nothing goes south (you will no changes with respects to the previous checkpoint).

Manage states and events

Define your game states in states.inc.php. We will use 3 states in addition of the predefined states 1 and 99. One to play the round for any player, one to check the end game condition, one to enable next round for players whose game is not over yet.

The player round state requires an action from any of the active players, so its type is 'multipleactiveplayer'.

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

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

$machinestates = [
    // The initial state. Please do not modify.
    1 => [
        "name" => "gameSetup",
        "description" => "",
        "type" => "manager",
        "action" => "stGameSetup",
        "transitions" => ["" => 4//'ST_PLAYER_MULTIACTIVATE'
        ],
    ],

    2 => [
        "name" => "playerTurn",
        "description" => clienttranslate("Waiting for other players"),
        "descriptionmyturn" => clienttranslate('${you} must select a cell to cross or pass the turn if non available'),
        "type" => "multipleactiveplayer",
        "possibleactions" => ["confirmTurn", "undoTurn", "passTurn", "passWholeGame"],
        "args" => "argPlayerTurn",
        "transitions" => ["next" => 3],
    ],

    3 => [
        "name" => "nextRoundOrGameEnd",
        "description" => "",
        "type" => "game",
        "action" => "stNextRoundOrGameEnd",
        "updateGameProgression" => true,
        "transitions" => [
            "next" => 4,
            "gameEnd" => 99,
        ],
    ],
    4 => [
        "name" => "multiActivatePlayers",
        "description" => clienttranslate("multiActivatePlayers state"),
        "type" => "game",
        "action" => "stMultiPlayerActivate",
        "transitions" => [
            "next" => 2,
        ],
    ],

    // Final state.
    // Please do not modify (and do not overload action/args methods).
    99 => [
        "name" => "gameEnd",
        "description" => clienttranslate("End of game"),
        "type" => "manager",
        "action" => "stGameEnd",
        "args" => "argGameEnd",
    ],
];

There are four actions player can do - "confirmTurn", "undoTurn", "passTurn", "passWholeGame". We define stub functions into .action.php and .game.php files now, since we have not implemented states yet.

i) action file

  public function passTurn() {
      self::setAjaxMode();

      // Then, call the appropriate method in your game logic, like "playCard" or "myAction"

      self::ajaxResponse();
  }

  public function passWholeGame() {
      self::setAjaxMode();

      // Then, call the appropriate method in your game logic, like "playCard" or "myAction"

      self::ajaxResponse();
  }

  public function confirmTurn() {
      self::setAjaxMode();

      // Retrieve arguments
      // Note: these arguments correspond to what has been sent through the javascript "ajaxcall" method

      // Then, call the appropriate method in your game logic, like "playCard" or "myAction"

      self::ajaxResponse();
  }

  public function undoTurn() {
    self::setAjaxMode();

    // Then, call the appropriate method in your game logic, like "playCard" or "myAction"
    
    self::ajaxResponse();
  }

ii) game file

    function passTurn() {
        // Check that this is the player's turn and that it is a "possible action" at this game state (see states.inc.php)
        self::checkAction("passTurn");

        // Get player and infos
        $player_id = self::getCurrentPlayerId();
        $playerInfo = DBManager::getPlayerDatasForPlayer($player_id);

        // Deactive player and if that player was the last active, change state
        $this->gamestate->setPlayerNonMultiactive($player_id, "next");
    }

    function passWholeGame() {
        // Check that this is the player's turn and that it is a "possible action" at this game state (see states.inc.php)
        self::checkAction("passWholeGame");

        // Get player and infos
        $player_id = self::getCurrentPlayerId();
        $playerInfo = DBManager::getPlayerDatasForPlayer($player_id);

        // Deactive player and if that player was the last active, change state
        $this->gamestate->setPlayerNonMultiactive($player_id, "next");
    }

    function confirmTurn($crossedCellCoordinate, $movedCellCoordinate) {

        // Check that this is the player's turn and that it is a "possible action" at this game state (see states.inc.php)
        self::checkAction("confirmTurn");

        // Get player and infos
        $player_id = self::getCurrentPlayerId();
        $playerInfo = DBManager::getPlayerDatasForPlayer($player_id);

        // Deactive player and if that player was the last active, change state
        $this->gamestate->setPlayerNonMultiactive($player_id, "next");
    }

    function undoTurn() {
        // Check that this is the player's turn and that it is a "possible action" at this game state (see states.inc.php)
        if (self::checkAction("undoTurn", false)) {
            Utils::userException(self::_("You did not make a turn to undo"));
        }

        // Get player and info
        $player_id = self::getCurrentPlayerId();
        $playerInfo = DBManager::getPlayerDatasForPlayer($player_id);

        // Re-Activate player
        $this->gamestate->setPlayersMultiactive([$player_id], "Ingore_ME", false);
    }

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

    function argPlayerTurn() {
        $players = DBManager::getAllPlayerDatas();

        $private = [
            "_private" => [],
        ];
        foreach ($players as $playerInfo) {
            $private["_private"][$playerInfo["id"]] = DBManager::getBoardForPlayer($playerInfo["id"], true);
        }
        return $private;
    }

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

    function stMultiPlayerActivate() {
        $this->gamestate->nextState("next");
    }
    function stNextRoundOrGameEnd() {
        $this->gamestate->nextState("next");
    }

Now as for getActivePlayers(): the rules dictate that a player is not allowed to play anymore after 3 consecutive round passes. We need to introduce a player field to keep track of that count, so

i) we add it to player table in the DB model

ALTER TABLE `player` ADD `player_pass_count` INT NOT NULL DEFAULT '0' AFTER `player_state`;

ii) we initialise it in game.php, adding it to the populate query in setupNewGame()

// Create players
        // Note: if you added some extra field on "player" table in the database (dbmodel.sql), you can initialize it there.
    
        //$sql = "INSERT INTO player (player_id, player_color, player_canal, player_name, player_avatar) VALUES ";
        $sql = "INSERT INTO player (player_id, player_color, player_canal, player_name, player_avatar, player_pass_count) VALUES ";

        $values = [];
        foreach ($players as $player_id => $player) {
            $color = array_shift($default_colors);
            $values[] = "('" . $player_id . "','$color','" . $player["player_canal"] . "','" . addslashes($player["player_name"]) . "','" . addslashes($player["player_avatar"]) . "', 0)";
            //$values[] = "('" . $player_id . "','$color','" . $player["player_canal"] . "','" . addslashes($player["player_name"]) . "','" . addslashes($player["player_avatar"]) . "')";

        }

iii) getAllDatas() should also get this new field, so we replace "players" code by this:

/***
        $result = array();
    
        $current_player_id = self::getCurrentPlayerId();    // !! We must only return informations visible by this player !!
    

        // Get information about players
        // Note: you can retrieve some extra field you added for "player" table in "dbmodel.sql" if you need it.
        $sql = "SELECT player_id id, player_score score FROM player ";
        $result['players'] = self::getCollectionFromDb( $sql );

***/
        $result = array();

        // Get information about players
        // Note: you can retrieve some extra field you added for "player" table in "dbmodel.sql" if you need it.
        $result["players"] = DBManager::getAllPlayerDatas();

iv) we set the pass count on .js file too

            ///////////////////////////////////////////////////////////////////////
            // Extend the player panel
            // First the wrapper
            // Generate from template
            var panelWrapperItem = this.format_block("jstpl_panelItemWrapper", {
                plId: player_id,
            });
            // Add it to the player board
            dojo.place(panelWrapperItem, "player_board_" + player_id, "last");

v) pass count will show in page thru HTML template. In the script section of .tpl file

    var jstpl_panelItemWrapper = '<div class="rt300_grid_wrapper" id="panelItem_wrapper_${plId}"> <div id="rt300_pass_count_${plId}" class="rt300_pass_count">          </div> </div>';

Finally in DBManage we add all database related functions for the states coding added in this section:

public static function getActivePlayers() {
        $sql = "SELECT player_id FROM player WHERE player_pass_count < 3";
        return self::getObjectListFromDB($sql);
    }
    
    public static function getAllPlayerDatas() {
        $sql = "SELECT player_id id, player_score score, player_pass_count AS pass_count FROM player ";
        return self::getCollectionFromDb($sql);
    }
    public static function getPlayerDatasForPlayer($player_id) {
        $sql = "SELECT player_id id, player_score score, player_pass_count AS pass_count FROM player WHERE player_id = $player_id;";
        return self::getObjectFromDB($sql);
    }

Now the game should start and you should see "Waiting for other players" on the top status bar, even if you switch players.

This is because we have not set player turn yet. Let us know implement function stMultiPlayerActivate():

    function stMultiPlayerActivate() {
        // Get all players which are still in the game
        $players = DBManager::getActivePlayers();
        $players = array_map(fn($value): string => $value["player_id"], $players);

        // Give them extra time
        foreach ($players as $player_id) {
            self::giveExtraTime($player_id);
            self::giveExtraTime($player_id);
        }

        // All those players are active
        $this->gamestate->setPlayersMultiactive($players, "INGORE_ME", true);

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

Dice

Possible player actions will depend on the result of dice roll. So we need to define this object and its logic before we implement player actions.

We'll follow same implementation steps as with boards above: tpl, css, js, game files

In tpl, add this in "rt300_dice" area:

        <div id="dice1" class="dice dice-two">
            <div id="dice-two-side-one" class="side one">
                <div class="dot one-1"></div>
            </div>
            <div id="dice-two-side-two" class="side two">
                <div class="dot two-1"></div>
                <div class="dot two-2"></div>
            </div>
            <div id="dice-two-side-three" class="side three">
                <div class="dot three-1"></div>
                <div class="dot three-2"></div>
                <div class="dot three-3"></div>
            </div>
            <div id="dice-two-side-four" class="side four">
                <div class="dot four-1"></div>
                <div class="dot four-2"></div>
                <div class="dot four-3"></div>
                <div class="dot four-4"></div>
            </div>
            <div id="dice-two-side-five" class="side five">
                <div class="dot five-1"></div>
                <div class="dot five-2"></div>
                <div class="dot five-3"></div>
                <div class="dot five-4"></div>
                <div class="dot five-5"></div>
            </div>
            <div id="dice-two-side-six" class="side six">
                <div class="dot six-1"></div>
                <div class="dot six-2"></div>
                <div class="dot six-3"></div>
                <div class="dot six-4"></div>
                <div class="dot six-5"></div>
                <div class="dot six-6"></div>
            </div>
        </div>

        <div id="dice2" class="dice dice-two">
            <div id="dice-two-side-one" class="side one">
                <div class="dot one-1"></div>
            </div>
            <div id="dice-two-side-two" class="side two">
                <div class="dot two-1"></div>
                <div class="dot two-2"></div>
            </div>
            <div id="dice-two-side-three" class="side three">
                <div class="dot three-1"></div>
                <div class="dot three-2"></div>
                <div class="dot three-3"></div>
            </div>
            <div id="dice-two-side-four" class="side four">
                <div class="dot four-1"></div>
                <div class="dot four-2"></div>
                <div class="dot four-3"></div>
                <div class="dot four-4"></div>
            </div>
            <div id="dice-two-side-five" class="side five">
                <div class="dot five-1"></div>
                <div class="dot five-2"></div>
                <div class="dot five-3"></div>
                <div class="dot five-4"></div>
                <div class="dot five-5"></div>
            </div>
            <div id="dice-two-side-six" class="side six">
                <div class="dot six-1"></div>
                <div class="dot six-2"></div>
                <div class="dot six-3"></div>
                <div class="dot six-4"></div>
                <div class="dot six-5"></div>
                <div class="dot six-6"></div>
            </div>
        </div>

        <div id="dice3" class="dice dice-three">
            <div id="dice-two-side-one" class="side one">
                <div class="dot one-1"></div>
            </div>
            <div id="dice-two-side-two" class="side two">
                <div class="dot two-1"></div>
                <div class="dot two-2"></div>
            </div>
            <div id="dice-two-side-three" class="side three">
                <div class="dot three-1"></div>
                <div class="dot three-2"></div>
                <div class="dot three-3"></div>
            </div>
            <div id="dice-two-side-four" class="side four">
                <div class="dot four-1"></div>
                <div class="dot four-2"></div>
                <div class="dot four-3"></div>
                <div class="dot four-4"></div>
            </div>
            <div id="dice-two-side-five" class="side five">
                <div class="dot five-1"></div>
                <div class="dot five-2"></div>
                <div class="dot five-3"></div>
                <div class="dot five-4"></div>
                <div class="dot five-5"></div>
            </div>
            <div id="dice-two-side-six" class="side six">
                <div class="dot six-1"></div>
                <div class="dot six-2"></div>
                <div class="dot six-3"></div>
                <div class="dot six-4"></div>
                <div class="dot six-5"></div>
                <div class="dot six-6"></div>
            </div>
        </div>

So we have one class for each of the 3 dice, one classe for each die side, and for each side as many classes as dots on the side. We'll see how all this is used later on. For now, in .css we define class for dice, dot and side, and for selected dice:

.dice {
    display: flex;
    width: 100px;
    height: 100px;
    transform-style: preserve-3d;
    transition: transform 1s;
}

.dot {
    position: absolute;
    width: 20px;
    height: 20px;
    margin: -10px 5px 5px -10px;
    border-radius: 20px;
    background-color: #46a12a;
    box-shadow: inset 2px 2px #46a12a;
}

.side {
    position: absolute;
    background-color: #fff;
    border-radius: 5px;
    width: 100px;
    height: 100px;
    border: 1px solid #e5e5e5;
    text-align: center;
    line-height: 2em;
}

.dice.used > div.side {
    background-color: darkgray;
    border: 1px solid darkgray;
}

In .js we set basic Dice functions:

/** Game class */
var Dice = /** @class */ (function (_super) {

    __extends(Dice, _super);
    function Dice() {
        return _super.call(this) || this;
    }
    Dice.setDice = function (diceOne, diceTwo, diceThree) {
        this.diceOne = parseInt(diceOne);
        this.diceTwo = parseInt(diceTwo);
        this.diceThree = parseInt(diceThree);
        var elDiceOne = $("dice1");
        var elDiceTwo = $("dice2");
        var elDiceThree = $("dice3");
        console.log(diceOne + " " + diceTwo + " " + diceThree);
        Dice.reset();
    };

    Dice.reset = function () {
        var elDiceOne = $("dice1");
        var elDiceTwo = $("dice2");
        var elDiceThree = $("dice3");
        dojo.removeClass(elDiceOne, "used");
        dojo.removeClass(elDiceTwo, "used");
        dojo.removeClass(elDiceThree, "used");
    };
    return Dice;

}(RootReference));
/// <reference path="base/GameBasics.ts"/>

In game.php.initGameStateLabels():

            [
            "dice_value_one" => 11,
            "dice_value_two" => 12,
            "dice_value_three" => 13,
            ]

Now let's get the dice rolling at game set-up. For that we implement dice functions on server side in module\Dice.php:

<?php
class Dice extends APP_GameClass {
    private static function instance() {
        return roadtothreehoundredjngm::$instance;
    }

    public static function rollDice($withNotification = false) {
        // Roll them
        $diceOne = bga_rand(1, 6);
        $diceTwo = bga_rand(1, 6);
        $diceThree = bga_rand(1, 6);

        // Store
        self::instance()->setGameStateValue("dice_value_one", $diceOne);
        self::instance()->setGameStateValue("dice_value_two", $diceTwo);
        self::instance()->setGameStateValue("dice_value_three", $diceThree);

        // Notify
        if ($withNotification) {
            NotificationManager::diceRolled($diceOne, $diceTwo, $diceThree);
        }
    }

    public static function getDice() {
        return [
            "diceOne" => self::instance()->getGameStateValue("dice_value_one"),
            "diceTwo" => self::instance()->getGameStateValue("dice_value_two"),
            "diceThree" => self::instance()->getGameStateValue("dice_value_three"),
        ];
    }
}

?>

This will only work if Dice.php is added to material.inc.php:

require_once "modules/Dice.php";

We'll need to define a public variable in game.php to send things back and forward with Dice.php:

    public static $instance;

Then in construct():

        self::$instance = $this;
    
        self::initGameStateLabels( array( 
            //    "my_first_global_variable" => 10,
            //    "my_second_global_variable" => 11,
            //      ...
            //    "my_first_game_variant" => 100,
            //    "my_second_game_variant" => 101,
            //      ...
            
                "dice_value_one" => 11,
                "dice_value_two" => 12,
                "dice_value_three" => 13,
                
            ) ); 

In setupNewGame:

        /************ Start the game initialization *****/

        // Init global values with their initial values

        Dice::rollDice(true);

and in getAllDatas()

        $result["dice"] = Dice::getDice();

Let's add notification to players for every time we roll dice. In modules/Dice.php, add to rollDice()

        // Notify
        if ($withNotification) {
            NotificationManager::diceRolled($diceOne, $diceTwo, $diceThree);
        }

As you might have guessed, we'll manage all server notifications from /modules/NotificationManager.php

Add this php file to material.inc.php

require_once "modules/NotificationManager.php";

NotificationManager.php

<?php
class NotificationManager extends APP_GameClass {
    private static function instance() {
        return roadtothreehoundredjngm::$instance;
    }

    public static function diceRolled($diceOne, $diceTwo, $diceThree) {
        self::instance()->notifyAllPlayers("notifDiceRolled", clienttranslate("Dice get rolled with ${diceOne}, ${diceTwo}, ${diceThree}."), [
            "diceOne" => $diceOne,
            "diceTwo" => $diceTwo,
            "diceThree" => $diceThree,
        ]);
    }
}
?>

In .js counterpart we already have setupNotifications() to catch notifications coming from server, we simply need to add this notification to the list:

        var notifs = array(
            //Add notifications here
            ["DiceRolled"],
        );

Now we implement this function in javascript:

    GameBody.prototype.notifDiceRolled = function (notif) {
        Dice.setDice(notif.args.diceOne, notif.args.diceTwo, notif.args.diceThree);
    };

Run the game and check that the results of the roll now show in the game log.

Player actions