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
(initial edit)
 
No edit summary
(2 intermediate revisions by the same user not shown)
Line 1: Line 1:
test
This tutorial will guide you through the basics of creating a simple game on BGA Studio, through the example of [https://ablagestapel.blogspot.com/p/road-to-300-spielregeln.html 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.
<nowiki><div id="rt300_mainboard"></nowiki>
    <nowiki><div id="rt300_spacer" class="rt300_zone"></div></nowiki>
    <nowiki><div id="rt300_dice"></nowiki>
    <nowiki></div></nowiki>
    <!-- BEGIN mainplboard -->
    <nowiki><div id="rt300_mainplboard" 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 class="rt300_playerheader"></nowiki>
                <nowiki><div class="rt300_nameholder"></nowiki>
                    <nowiki><div id="rt300_plname_{PLAYER_ID}" class="rt300_plname">{PLAYER_NAME}</div></nowiki>
                <nowiki></div></nowiki>
            <nowiki></div></nowiki>
            <nowiki><div id="rt300_metropad_{PLAYER_ID}" class="rt300_metropad" style="opacity: 1"></nowiki>
            <nowiki></div></nowiki>
        <nowiki></div></nowiki>
    <nowiki></div></nowiki>
      <!-- END mainplboard -->
    <nowiki><div id="rt300_opponents" class="rt300_zone"></nowiki>
          <!-- BEGIN opponentBoard -->
        <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 class="rt300_playerheader"></nowiki>
                    <nowiki><div class="rt300_nameholder"></nowiki>
                        <nowiki><div id="rt300_plname_{PLAYER_ID}" class="rt300_plname">{PLAYER_NAME}</div></nowiki>
                    <nowiki></div></nowiki>
                <nowiki></div></nowiki>
                <nowiki><div id="rt300_metropad_{PLAYER_ID}" class="rt300_metropad" style="opacity: 1"></nowiki>
                <nowiki></div></nowiki>
            <nowiki></div></nowiki>
        <nowiki></div></nowiki>
          <!-- END opponentBoard -->
    <nowiki></div></nowiki>
<nowiki></div></nowiki>
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 (ST_SETUP) and 99 (ST_GAME_END). One to play the round (ST_PERFORM_TURN) for any player, one to check the end game condition (ST_NEXT_ROUND), one to enable next round for players whose game is not over yet (ST_PLAYER_MULTIACTIVATE).
 
The ST_PERFORM_TURN 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 ST_NEXT_ROUND 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.
    ST_SETUP => [
        "name" => "gameSetup",
        "description" => "",
        "type" => "manager",
        "action" => "stGameSetup",
        "transitions" => ["" => ST_PLAYER_MULTIACTIVATE],
    ],
    ST_PERFORM_TURN => [
        "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" => ST_NEXT_ROUND],
    ],
    ST_NEXT_ROUND => [
        "name" => "nextRoundOrGameEnd",
        "description" => "",
        "type" => "game",
        "action" => "stNextRoundOrGameEnd",
        "updateGameProgression" => true,
        "transitions" => [
            "next" => ST_PLAYER_MULTIACTIVATE,
            "gameEnd" => ST_GAME_END,
        ],
    ],
    ST_PLAYER_MULTIACTIVATE => [
        "name" => "multiActivatePlayers",
        "description" => "",
        "type" => "game",
        "action" => "stMultiPlayerActivate",
        "transitions" => [
            "next" => ST_PERFORM_TURN,
        ],
    ],
    // Final state.
    // Please do not modify (and do not overload action/args methods).
    ST_GAME_END => [
        "name" => "gameEnd",
        "description" => clienttranslate("End of game"),
        "type" => "manager",
        "action" => "stGameEnd",
        "args" => "argGameEnd",
    ],
];
Don't forget to define constant variables in /modules/Constants.php for the above to work:
<?php
/****************/
define("ST_SETUP", 1);
/** CUSTOM **/
define("ST_PERFORM_TURN", 2);
define("ST_NEXT_ROUND", 3);
define("ST_PLAYER_MULTIACTIVATE", 4);
/****************/
define("ST_GAME_END", 99);
?>
Implement the 'stMultiPlayerActivate()' function in .game.php to manage which players can still play a round.
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");
    }
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) we check it in ST_PLAYER_MULTIACTIVATE state, by adding a new function to DBManage
    public static function getActivePlayers() {
        $sql = "SELECT player_id FROM player WHERE player_pass_count < 3";
        return self::getObjectListFromDB($sql);
    }

Revision as of 23:37, 25 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 (ST_SETUP) and 99 (ST_GAME_END). One to play the round (ST_PERFORM_TURN) for any player, one to check the end game condition (ST_NEXT_ROUND), one to enable next round for players whose game is not over yet (ST_PLAYER_MULTIACTIVATE).

The ST_PERFORM_TURN 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 ST_NEXT_ROUND 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.
    ST_SETUP => [
        "name" => "gameSetup",
        "description" => "",
        "type" => "manager",
        "action" => "stGameSetup",
        "transitions" => ["" => ST_PLAYER_MULTIACTIVATE],
    ],

    ST_PERFORM_TURN => [
        "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" => ST_NEXT_ROUND],
    ],

    ST_NEXT_ROUND => [
        "name" => "nextRoundOrGameEnd",
        "description" => "",
        "type" => "game",
        "action" => "stNextRoundOrGameEnd",
        "updateGameProgression" => true,
        "transitions" => [
            "next" => ST_PLAYER_MULTIACTIVATE,
            "gameEnd" => ST_GAME_END,
        ],
    ],

    ST_PLAYER_MULTIACTIVATE => [
        "name" => "multiActivatePlayers",
        "description" => "",
        "type" => "game",
        "action" => "stMultiPlayerActivate",
        "transitions" => [
            "next" => ST_PERFORM_TURN,
        ],
    ],

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

Don't forget to define constant variables in /modules/Constants.php for the above to work:

<?php

/****************/
define("ST_SETUP", 1);

/** CUSTOM **/
define("ST_PERFORM_TURN", 2);
define("ST_NEXT_ROUND", 3);
define("ST_PLAYER_MULTIACTIVATE", 4);

/****************/
define("ST_GAME_END", 99);

?>

Implement the 'stMultiPlayerActivate()' function in .game.php to manage which players can still play a round.

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");
    }

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) we check it in ST_PLAYER_MULTIACTIVATE state, by adding a new function to DBManage

    public static function getActivePlayers() {
        $sql = "SELECT player_id FROM player WHERE player_pass_count < 3";
        return self::getObjectListFromDB($sql);
    }