Wednesday, December 20, 2023

Enable Sub-Game Selection

 

Overview

I'm continuing to learn how to develop a game within Board Game Arena. In this section I'm working on the next step in the game I'm building out which involves the dealer picking a specific game to play. Ultimately, each game can only be selected once, but this is just going to focus on making the selection possible and ensuring that I can tell that the selection was made.

I realize I haven't named the game I'm working to implement and as I kept typing things like "the game I'm building out" it started to seem silly. The game is Guillotine. A game that my brother taught me years ago and now I play with a group of friends regularly. Here's a link to the rules if you're interested.

For some context, you might want to read the previous post about getting a hand of cards to show.

Create a state for selecting a game

After a hand is dealt the dealer must select the game to play. These directly translate to states to be implemented. So we need to create a new state for handling game selection and update the state for dealing a new hand to transition to the newly created one.
2 => [
  "name" => "newHand",
  "description" => "",
  "type" => "game",
  "action" => "stNewHand",
  "updateGameProgression" => true,
  "transitions" => ["selectGame" => 10]
],
 
10 => [
  "name" => "gameSelection",
  "description" => clienttranslate('${actplayer} must select the game to play'),
  "descriptionmyturn" => clienttranslate('${you} must select the game to play'),
  "type" => "activeplayer",
  "args" => "argSelectGame",
  "possibleactions" => ["gameSelection"],
  "transitions" => []
],

Since the only thing we want to happen is for the game to transition from "newHand" to "gameSelection" we need to invoke the transition in the "newHand" action. So at the end of the stNewHand function we do just that with the following that references the name of the transition, "selectGame".
$this->gamestate->nextState("selectGame");

Some things to note about the new state (see documentation for complete details):
  • "type" => "activeplayer" indicates that a single player is active and must do something to continue the game.
  • "description" and "descriptionmyturn" provide the text to display in the main action bar of the game.
  • "args" exposes information to the client side from the declared function.
  • "possibleactions" defines the possible actions the player can take and allows for them to be enforced so the functions aren't called at unexpected times.

State args

Guillotine has six games a player must select during the game with each one being selected only once. So ultimately we want to provide the list of available games for the current player to select. Though to simplify things, I'm going to ignore how the played games are tracked and just worry about making the six games available to select from.
function argSelectGame() {
  $available_games = [];
  foreach ($this->games as $game_type => $game) {
    $available_games[] = ['type' => $game_type, 'name' => $game['name']];
  }
 
  return [
    "available_games" => $available_games
  ];
}

I initially had the names of the games just hard-coded into this function, but I ran into issues keeping track of the current game since game state values can only be integers instead of strings. So I pulled the game details out to the material.inc.php file so they can be referenced everywhere.
$this->games = [
  'parliament' => [
    'id' => 1,
    'name' => clienttranslate('Parliament')
  ],
  'spades' => [
    'id' => 2,
    'name' => clienttranslate('Spades')
  ],
  'queens' => [
    'id' => 3,
    'name' => clienttranslate('Queens')
  ],
  'royalty' => [
    'id' => 4,
    'name' => clienttranslate('Royalty')
  ],
  'dominoes' => [
    'id' => 5,
    'name' => clienttranslate('Dominoes')
  ],
  'guillotine' => [
    'id' => 6,
    'name' => clienttranslate('Guillotine')
  ],
];

Prompt current user

Now things shift to the client side and we need to allow the user to select the game they want to play. Within the <game_name>.js file there should already exist a function onUpdateActionButtons which I just updated to handle this case.
onUpdateActionButtons: function( stateName, args )
{
  console.log( 'onUpdateActionButtons: '+stateName );
 
  if( this.isCurrentPlayerActive() )
  {
    switch( stateName )
    {
      case 'gameSelection':
        args.available_games?.forEach(game => {
          this.addActionButton(game.type, _(game.name), () => this.onGameSelection(game.type))
        });
        break;
    }
  }
},

The args.available_games along with type and name are direct references to what was returned from the argSelectGame function above. Currently it will error out because the onGameSelection function isn't defined, so let's add that to the "Player's action" section of the file.
onGameSelection: function(game_type) {
  const action = "gameSelection";
  if (!this.checkAction(action)) return;

  this.ajaxcall(
    "/" + this.game_name + "/" + this.game_name + "/" + action + ".html",
    {lock: true, selected_game: game_type}, this, function (result) {}, function (is_error) {}
  );
},

The call to checkAction is used to protect against the method being called when not expected and the action must match the name in the "possibleactions" in the state definition. The call to ajaxcall sends the selected game to the back end. (See the documentation for details on the function.)

Now we should get prompted to select the game with this



Keep track of selected game

For the back end to be able to accept the call that gives it the selected game an action needs to be defined. This is done the in <game_name>.action.php file. The details of the file and the functions used in it are defined in the documentation.
public function gameSelection() {
  self::setAjaxMode();

  $selected_game = self::getArg("selected_game", AT_alphanum, true);
  $this->game->gameSelection($selected_game);

  self::ajaxResponse();
}

As noted in the documentation for actions, they are just serving as a bridge from the front end to the main game logic. So this just extracts the "selected_game" argument that was passed along and calls a gameSelection function in the main <game_name>.game.php file. The function should go in the "Player actions" section.
function gameSelection($selected_game) {
  self::checkAction("gameSelection");

  $game_id = $this->games[$selected_game]['id'];
  self::setGameStateValue(SELECTED_GAME, $game_id);

  $game_name = $this->games[$selected_game]['name'];
  self::notifyAllPlayers('gameSelection',
    clienttranslate('${player_name} selects ${game_name} as the game to play'), [
      'i18n' => ['game_name'],
      'player_name' => self::getActivePlayerName(),
      'game_name' => $game_name,
    ]);
}

This keeps track of the selected game by calling setGameStateValue with the id defined in the game details defined in the first section. It also notifies all of the players in the game log which game was selected. SELECTED_GAME is a constant that I defined in a new file models\constants.inc.php. I like using a constant as I find it helps me avoid typos that I don't notice until testing.
<?php
// Game State Variables
define('SELECTED_GAME', 'selected_game');

Additionally, the game state needs to be initialized in a couple of sections of the <game_name>.game.php file. In the __construct function it needs to be defined.
self::initGameStateLabels([
  SELECTED_GAME => 10,
]);

If I didn't want a default value of 0 for the starting value I'd need to initialize it in the setupNewGame function, but since I am I can skip that. However, I know I'm going to have to reset the value for each new hand so I'll update the stNewHand function that was created for dealing out the cards. This won't be used until we play through a hand, but I feel like adding it in this case. :)
self::setGameStateValue(SELECTED_GAME, 0);

Wrap up

I'm still trying to understand how translations work and if everything I've done will be able to be translated appropriately. From what I understand my call to notifyAllPlayers should be good because the ${game_name} variable in the message is specified in the i18n parameter. I'm less certain about the addActionButton call in the front end as I'm passing in a variable to it. Something I'll need to dig further into to make sure I'm doing that correctly.

No comments: