Friday, December 29, 2023

Scoring a Hand

 

Overview

During the last bit of work, the playing of cards started following the rules and now the game could figure out who won the trick and give them the cards. Now we can make use of the game that was selected at the start of the hand to figure out how to score the hand. Before that, we need to introduce the concept of the end of a hand.

As I was working on this some things were annoying me that I felt the need to address before I worked through the scoring.

Keep track of dealer

Created a new constant in the modules\constant.inc.php file and added a new game state label to keep track of the dealer.
self::initGameStateLabels([
    SELECTED_GAME => 10,
    TRICK_SUIT => 11,
    DEALER => 12,
]);

The setupNewGame function needs to initialize the dealer. I felt compelled to keep the dealer as the initial player selected by making the next player active, then going back two and allowing the logic for a new hand to move it forward one. Logically, it's not going to matter to the players, but it makes me happy.
self::setGameStateInitialValue(DEALER,
    self::getPlayerBefore(self::getPlayerBefore($this->activeNextPlayer())));

Update the dealer in the stNewHand function and update the active player to be the new dealer.
$new_dealer_id = self::getPlayerAfter(self::getGameStateValue(DEALER));
self::setGameStateValue(DEALER, $new_dealer_id);
$this->gamestate->changeActivePlayer($new_dealer_id);

Indicate the game being played

While there's a notification in the game log about what game was selected it seemed like a good idea to show the selected game somewhere else. Putting it with the player's hand seemed like a good way to not only indicate the game but who the dealer was for that round.

Share the dealer and selected game in the getAllDatas function.
$selected_game = null;
$selected_game_id = self::getGameStateValue(SELECTED_GAME);
foreach ($this->games as $game_type => $game) {
    if ($game['id'] == $selected_game_id) {
        $selected_game = $game['name'];
        break;
    }
}

$result[DEALER] = self::getGameStateValue(DEALER);
$result[SELECTED_GAME] = $selected_game;

I modified the template to have a place to show the selected game for the dealer. It'll start with "Choosing game..." and then replace that after the game is selected. Unfortunately, after typing this I realized the default text wouldn't be translated. It'll need to be updated so it can be passed in as a translated value, which will involve putting a placeholder in there instead.
<div id="playertables">
    <!-- BEGIN player -->
    <div class="playertable whiteblock playertable_{DIR}">
        <div class="playertablename" style="color:#{PLAYER_COLOR}">{PLAYER_NAME}</div>
        <div class="playertableselectedgame" id="dealer_p{PLAYER_ID}">Choosing game...</div>
        <div class="playertablecard" id="playertablecard_{PLAYER_ID}"></div>
    </div>
    <!-- END player -->
</div>

There are some styling changes so the selected game only shows if the player is the dealer.
.playertableselectedgame {
    display: none;
}

.playertableselectedgame.show_dealer {
    display: block;
}

Now show the selected game for the dealer in the setup method.
document.getElementById('dealer_p' + this.gamedatas.dealer).classList.add('show_dealer');
document.getElementById('dealer_p' + this.gamedatas.dealer).innerHTML = this.gamedatas.selected_game;

Now we need to show the game name as soon as it is selected. So the back end function of gameSelection needs to be updated to pass the ID of the dealer.
self::notifyAllPlayers('gameSelection',
    clienttranslate('${player_name} selects ${game_name} as the game to play'),
    [
        'i18n' => ['game_name'],
        'player_name' => self::getActivePlayerName(),
        'dealer_id' => $player_id,
        'game_name' => $game_name,
    ]
);

Then, the front end needs to handle the "gameSelection" notification so it shows the game name. Remember to subscribe to it in the setupNotifications function.
notif_gameSelection : function(notif) {
    document.getElementById('dealer_p'+notif.args.dealer_id).innerHTML = notif.args.game_name;
},

Finally, during each round, we need to update the front end with who the current dealer is. For this a new notification was created in the back end in the stNewHand function.
self::notifyAllPlayers('newRound', '', [
    'dealer_id' => $new_dealer_id
]);

The front end subscribes to the "newRound" notification and updates the view by adding the "show_dealer" class to the appropriate player.
notif_newRound : function(notif) {
    document.querySelectorAll('.show_dealer').forEach(e => e.classList.remove('show_dealer'));
    document.getElementById('dealer_p' + notif.args.dealer_id).classList.add('show_dealer');
},

Scoring

Hand end

Let's use a new state, "endHand", and allow "nextPlayer" to transition to it.
22 => [
    "name" => "nextPlayer",
    "description" => "",
    "type" => "game",
    "action" => "stNextPlayer",
    "updateGameProgression" => true,
    "transitions" => ["nextPlayer" => 21, "nextTrick" => 20, "endHand" => 30]
],

30 => [
    "name" => "endHand",
    "description" => "",
    "type" => "game",
    "action" => "stEndHand",
    "transitions" => []
],

The stEndHand function will determine the points each player gained for the hand.
function stEndHand() {
    $players = self::loadPlayersBasicInfos();
    $cards = $this->cards->getCardsInLocation(CARDS_WON);
    $scorer = null;

    $selected_game_id = self::getGameStateValue(SELECTED_GAME);
    switch ($selected_game_id) {
        case 1:
            $scorer = new GLTParliamentScorer();
            break;
        default:
            throw new BgaUserException(sprintf(self::_("The selected game id, %s, does not have a scorer"), $selected_game_id));
            break;
    }

    $player_to_points = $scorer->score(array_keys($players), $cards);

    foreach ($player_to_points as $player_id => $points) {
        $sql = "UPDATE player SET player_score=player_score+$points WHERE player_id='$player_id'";
        self::DbQuery($sql);
        self::notifyAllPlayers(
            "points",
            clienttranslate('${player_name} gained ${points} points'),
            [
                'player_name' => $players[$player_id]['player_name'],
                'points' => $points,
                'player_id' => $player_id,
            ]
        );
    }

    $new_scores = self::getCollectionFromDb("SELECT player_id, player_score FROM player", true);
    self::notifyAllPlayers("newScores", '', ['newScores' => $new_scores]);
}

Currently, this is only scoring the game of Parliament, but a similar pattern should work for the rest (except for maybe Dominoes...). This posts notifications for each player to everyone indicating how many points that player gained this hand. Then it sends another notification that is used to update the score totals.

Let's look at the GLTParliamentScorer class.
<?php

require_once('GLTScorer.interface.php');

class GLTParliamentScorer implements GLTScorer {
    function score(array $player_ids, array $won_cards) {
        $player_to_points = [];
        $player_card_counts = [];
        foreach ($player_ids as $id) {
            $player_to_points[$id] = 0;
            $player_card_counts[$id] = 0;
        }
 
        foreach ($won_cards as $card) {
            $player_id = $card['location_arg'];

            // Find K of hearts
            if ($card['type'] == HEART && $card['type_arg'] == 13) {
                $player_to_points[$player_id] -= 10;
            }

            $player_card_counts[$player_id] += 1;
        }

        foreach ($player_card_counts as $player_id => $count) {
            $player_to_points[$player_id] -= ($count / 4) * 5;
        }

        return $player_to_points;
    }
}

The GLTScorer interface is just defining the interface of the score method. The scoring for Parliament is -5 for each trick and -10 for the player who wins the K of Hearts. I pulled this out to a separate class to encapsulate the behavior, but there are some trade-offs with it. I did it so that they would be easier to unit test (though I didn't do that...), but you lose access to some of the functionality that is available in the main class that extends Table.

The front end subscribes to the "newScores" notification to update the scores in the view.
notif_newScores : function(notif) {
    for (var player_id in notif.args.newScores) {
        this.scoreCtrl[player_id].toValue(notif.args.newScores[player_id]);
    }
},

Conclusion

I'm not going to go into each of the Scorer implementations since they only vary in how the game is scored, but I'm sure Dominoes will end up being its own set of things because of how different that game plays from the others. I'm also realizing in talking about why I encapsulated the scoring in separate files I should take some time to reinforce for myself how I would test those classes. (Maybe I'll find some bugs or clarify how it might work for Dominioes!)

Sunday, December 24, 2023

Clean up Playing a Hand

 

Overview

In the last bit of work, I got it so a hand of cards could be played through, but this ignored the rules of the game. Before going any further I'd like to go back and fill in those details.

Correct hand sorting

Guillotine has a quirky ordering of the cards with the 10 as the second highest under the A - so A,10,K,Q,J,9,8,7. (Except for the game Dominoes in which the 10 goes back to its usual place between the J and 9, but we'll deal with that later.) The sorting of cards is handled in the JavaScript file. Specifically, when creating the cards a weight is specified, and the higher the value of the weight the further to the right it is placed in the hand. So instead of just using the card ID for the weight I tweaked that with the following.
// From the setup function in the <game_name>.js file
for (var suit = 1; suit <= 4; suit++) {
    for (var value = 7; value <= 14; value++) {
        const card_type_id = this.getCardUniqueId(suit, value);
        const card_weight = this.getCardWeight(suit, value);
        this.playerHand.addItemType(card_type_id, card_weight, g_gamethemeurl + 'img/cards.jpg', card_type_id);
    }
}

This method goes under the "Utility methods" section.
getCardWeight: function(suit, value) {
    var base_weight = this.getCardUniqueId(suit, value);
    if (value == 10) {
        return base_weight + 3;
    } else if (value == 14) {
        return base_weight + 1;
    } else {
        return base_weight;
    }
},

Nothing fancy, just bumping up the weight of 10 and the A (to make space for the 10).

Enforce following suit

This requires two changes to the playCard function. The first checks if there's a suit for the trick already defined and if the player didn't play a card of that suit, but has one in their hand then raises an error to let them know. The second sets a value for the current suit if one isn't already set.
function playCard($card_id) {
    self::checkAction("playCard");

    $player_id = self::getActivePlayerId();
    $current_card = $this->cards->getCard($card_id);

    // Here's where we ensure the player is following suit.
    $this->checkPlayableCard($player_id, $current_card);

    $this->cards->moveCard($card_id, 'cardsontable', $player_id);

    // This sets a suit for the trick if a value isn't already set.
    if (!self::getGameStateValue(TRICK_SUIT)) self::setGameStateValue(TRICK_SUIT, $current_card['type']);

    self::notifyAllPlayers('playCard', clienttranslate('${player_name} plays ${suit_displayed}${value_displayed}'), [
        'player_name' => self::getActivePlayerName(),
        'suit_displayed' => $this->suits[$current_card['type']]['name'],
        'value_displayed' => $this->values_label[$current_card['type_arg']],
        'suit' => $current_card['type'],
        'value' => $current_card['type_arg'],
        'card_id' => $card_id,
        'player_id' => $player_id
    ]);

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

Finally, we need to make sure to clear the suit for the trick for each new trick. So modify the stNewTrick function to add the following.
self::setGameStateValue(TRICK_SUIT, 0);

Determine the winner of the trick

When advancing to the next player, check if four cards are on the table. If there are, then find the card with the highest value of the suit lead for the trick. Give the cards to that player and make them the next active player. This change is made in the stNextPlayer function.
function stNextPlayer() {
    if ($this->cards->countCardInLocation(CARDS_ON_TABLE) == 4) {
        $cards_on_table = $this->cards->getCardsInLocation(CARDS_ON_TABLE);
        $trick_suit = self::getGameStateValue(TRICK_SUIT);
        $winning_card = null;

        foreach ($cards_on_table as $card) {
            if ($card['type'] == $trick_suit) {
                $winning_card = $this->higherCard($winning_card, $card);
            }
        }

        $winning_player_id = $winning_card['location_arg'];

        $this->gamestate->changeActivePlayer($winning_player_id);
        $this->cards->moveAllCardsInLocation(CARDS_ON_TABLE, CARDS_WON, null, $winning_player_id);

        $players = self::loadPlayersBasicInfos();
        self::notifyAllPlayers('trickWin', clienttranslate('${player_name} wins the trick'), [
            'player_id' => $winning_player_id,
            'player_name' => $players[ $winning_player_id ]['player_name']
        ]);
        self::notifyAllPlayers('giveAllCardsToPlayer','', [
            'player_id' => $winning_player_id
        ]);

        $this->gamestate->nextState("nextTrick");
    } else {
        // Not end of trick or hand, so move to the next player
        $player_id = self::activeNextPlayer();
        self::giveExtraTime($player_id);
        $this->gamestate->nextState("nextPlayer");
    }
}

function higherCard($higher_card, $new_card) {
    if ($higher_card === null) return $new_card;
    if ($new_card === null) return $higher_card;

    if ($higher_card['type_arg'] == 10) {
        if ($new_card['type_arg'] == 14) return $new_card;
        else return $higher_card;
    } else if ($new_card['type_arg'] == 10) {
        if ($higher_card['type_arg'] == 14) return $higher_card;
        else return $new_card;
    } else if ($new_card['type_arg'] > $higher_card['type_arg']) {
        return $new_card;
    } else return $higher_card;
}

The logic around figuring out which card is the winning card is complicated since 10 ranks under the A, but the image of the cards we're working with has them in the typical rank for a poker deck.

Conclusion

These changes made it start behaving like a trick-taking game. However, it's going to be really hard to win if no score is kept track of. So that's next on the list.

Saturday, December 23, 2023

Play a Hand

Overview

In continuing to build out Guillotine for Board Game Arena (see the previous post), I'm looking to enable playing cards from a hand. For now, I'm going to focus on cycling through the players and collecting the cards, ignoring any of the rules around placement. Mostly because as I was putting this together I had forgotten about some of the details that were covered in the tutorial, so I want to be sure to detail those things.

Enable playing of a card

Like with almost everything in developing behavior in a game for Board Game Arena, it starts with a state. For this, I created two of them. The first, newTrick, was not strictly needed, but I know eventually I'll need to keep track of the suit led for a trick to enforce following suit and this is where it would need to be reset. Also, when adding any new state, another state needs to be updated to transition to the new one, so "gameSelection" was updated for this.
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" => ["startHand" => 20]
],

20 => [
    "name" => "newTrick",
    "description" => "",
    "type" => "game",
    "action" => "stNewTrick",
    "transitions" => ["playerTurn" => 21]
],

21 => [
    "name" => "playerTurn",
    "description" => clienttranslate('${actplayer} must play a card'),
    "descriptionmyturn" => clienttranslate('${you} must play a card'),
    "type" => "activeplayer",
    "args" => "argPlayerTurn",
    "possibleactions" => ["playCard"],
    "transitions" => []
],

Since "newTrick" isn't needed yet, the action backing it is just a function calling the "playerTurn" transition.
function stNewTrick() {
    $this->gamestate->nextState("playerTurn");
}

For "playerTurn", the intention for the args would be to indicate the playable cards so that some visual indicator could be given. For now, that's not critical and the argPlayerTurn function is just returning an empty array. That leaves supporting the playCard possible action which involves several steps.

Action button

Update the onUpdateActionButtons to add an action button as part of the switch statement there.
case 'playerTurn':
    this.addActionButton('btnPlayCard', _('Play card'), 'onBtnPlayCard');
    break;

Then implement the onBtnPlayCard method to pass to the back end what card was selected to be played. This is skipping some validation that should happen to make sure things don't get into a funny state - ensure only one card is selected and that the card is a valid play - the focus, for now, is on just getting the card played.
onBtnPlayCard: function() {
    const action = "playCard";
    if (!this.checkAction(action)) return;

    const selected_cards = this.playerHand.getSelectedItems();
    const card_id = selected_cards[0].id;

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

Support action call

To allow the ajaxcall from the button there needs to be an action function defined in the <game_name>.action.php file. As a reminder, the first and last lines are dictated to us as required and the middle part is just about extracting the data passed in and calling a supporting function with the game logic (<game_name>.game.php file).
public function playCard() {
    self::setAjaxMode();

    $card_id = self::getArg("card_id", AT_posint, true);
    $this->game->playCard($card_id);

    self::ajaxResponse();
}

The playCard function handles moving the card to the desired location, then fires off a notification to log that to the players as well as trigger the visual update.
function playCard($card_id) {
    self::checkAction("playCard");

    $player_id = self::getActivePlayerId();

    $current_card = $this->cards->getCard($card_id);
    $this->cards->moveCard($card_id, 'cardsontable', $player_id);

    self::notifyAllPlayers('playCard', clienttranslate('${player_name} plays ${suit_displayed}${value_displayed}'), [
        'player_name' => self::getActivePlayerName(),
        'suit_displayed' => $this->suits[$current_card['type']]['name'],
        'value_displayed' => $this->values_label[$current_card['type_arg']],
        'suit' => $current_card['type'],
        'value' => $current_card['type_arg'],
        'card_id' => $card_id,
        'player_id' => $player_id
    ]);
}

I hadn't internalized the dual nature of these notifications, so I was confused when I was looking over examples that passed back things that had nothing to do with the message. Now that I understand they are serving both, it made things a lot clearer.

Update card visuals

The first step with this is to list to the "playCard" notification we generated in the last step above. This requires updating the setupNotifications function in the <game_name>.js file with the following line.
dojo.subscribe('playCard', this, "notif_playCard");

Then define the referenced "notif_playCard" function.
notif_playCard : function(notif) {
    const player_id = notif.args.player_id;
    const suit = notif.args.suit;
    const value = notif.args.value;
    const card_id = notif.args.card_id;

    dojo.place(this.format_block('jstpl_card', {
        x : this.cardwidth * (value - 2),
        y : this.cardheight * (suit - 1),
        player_id : player_id
    }), 'playertablecard_' + player_id);
 
    if (player_id != this.player_id) {
        // Some opponent played a card
        // Move card from player panel
        this.placeOnObject('cardontable_' + player_id, 'overall_player_board_' + player_id);
    } else {
        // You played a card. If it exists in your hand, move card from there and remove
        // corresponding item
        if ($('myhand_item_' + card_id)) {
            this.placeOnObject('cardontable_' + player_id, 'myhand_item_' + card_id);
            this.playerHand.removeFromStockById(card_id);
        }
    }
 
    // In any case: move it to its final destination
    this.slideToObject('cardontable_' + player_id, 'playertablecard_' + player_id).play();
}

This has some template and styling changes that need to happen to make things work and look ok. The <game_name>_<game_name>.tpl file needs to define "jstpl_card", which is placed in the script section under the "Javascript HTML templates" comment.
var jstpl_card = '<div class="card cardontable" id="cardontable_${player_id}" style="background-position:-${x}px -${y}px"></div>';

The dojo.place call that references the "jstpl_card" template also has 'playertablecard_' + player_id as its second parameter, which indicates where it should insert the template it referenced, so we need to define that in the <game_name>_<game_name>.tpl file. I made use of the layout used for the hearts tutorial game.
<div id="playertables">
    <!-- BEGIN player -->
    <div class="playertable whiteblock playertable_{DIR}">
        <div class="playertablename" style="color:#{PLAYER_COLOR}">{PLAYER_NAME}</div>
        <div class="playertablecard" id="playertablecard_{PLAYER_ID}"></div>
    </div>
    <!-- END player -->
</div>

To get the layout working I also snagged the css from the hearts tutorial. This will create four boxes for each player where their played card can be shown.
#playertables {
    position: relative;
    width: 710px;
    height: 340px;
}

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

.playertablename {
    font-weight: bold;
}

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

.playertable_N {
    left: 50%;
    top: 0px;
    margin-left: -90px /* half of 180 */
}

.playertable_S {
    left: 50%;
    bottom: 0px;
    margin-left: -90px /* half of 180 */
}

.playertable_W {
    left: 0px;
    top: 50%;
    margin-top: -55px /* half of 130 */
}

.playertable_E {
    right: 0px;
    top: 50%;
    margin-top: -55px /* half of 130 */
}

.playertablecard {
    display: inline-block;
    position: relative;
    margin-top: 5px;
    width: 72px;
    height: 96px;
}

.playertablename {
    font-weight: bold;
}

Note, I said can be shown. That's because while listening to the notification will remove the card from the player's hand and show the card moving to the table. We won't keep track of the cards that have been played to the table and show them. For that, we need one more piece.

Show cards on the table

Back in the "Support action call" section I mentioned how the playCard method moved the card to the desired location. Just to help connect the dots, here's the specific call I'm talking about:
$this->cards->moveCard($card_id, 'cardsontable', $player_id);

The moveCard function is part of the Deck component and the location value is just a string value that can be anything you want. So the cards that are in the "cardsontable" location need to be passed over as part of the getAllDatas so the front end can work with it.
$result['cardsontable'] = $this->cards->getCardsInLocation('cardsontable');

Then in the front end (<game_name>.js file) in the setup method we need to show the cards on the table.
for (let i in gamedatas.cardsontable) {
    const card = gamedatas.cardsontable[i];
    const suit = card.type;
    const value = card.type_arg;
    var player_id = card.location_arg;
    this.playCardOnTable(player_id, suit, value, card.id);
}

The playCardOnTable function is just the guts of the notif_playCard function extracted to a new method since the same thing was being done in both locations. That should get it so the current play can play a card to the table, but it will stay with the same player and prompt them to play another card.

Advance to the next player

For this to happen we want another state that will change the active player. Again, we're adding a state, so we need something to transition to it - we'll update "playerTurn" with a new transition.
21 => [
    "name" => "playerTurn",
    "description" => clienttranslate('${actplayer} must play a card'),
    "descriptionmyturn" => clienttranslate('${you} must play a card'),
    "type" => "activeplayer",
    "args" => "argPlayerTurn",
    "possibleactions" => ["playCard"],
    "transitions" => ["cardPlayed" => 22]
],

22 => [
    "name" => "nextPlayer",
    "description" => "",
    "type" => "game",
    "action" => "stNextPlayer",
    "updateGameProgression" => true,
    "transitions" => ["nextPlayer" => 21]
],

For the new transition to be used, the playCard function needs to be updated to use it.
$this->gamestate->nextState("cardPlayed");

The stNextPlayer function should go into the "Game state action" section and handle moving to the next player.
function stNextPlayer() {
    $player_id = self::activeNextPlayer();
    self::giveExtraTime($player_id);
    $this->gamestate->nextState("nextPlayer");
}

I saw the giveExtraTime call in the Hearts tutorial and assumed it was a good thing to do, but I haven't looked into it.

Handle finishing a trick

Now that it's advancing to the next player it needs to gather the cards when everyone has played one. Again this is going to skip some logic in the interest of keeping focused on the basic behavior. So let's not worry about who won the trick and just have someone gather them and keep going around the table playing cards. This involves updating the stNextPlayer function.
function stNextPlayer() {
    if ($this->cards->countCardInLocation('cardsontable') == 4) {
        $winning_player_id = self::activeNextPlayer();
        $this->cards->moveAllCardsInLocation('cardsontable', 'cardswon', null, $winning_player_id);

        $players = self::loadPlayersBasicInfos();
        self::notifyAllPlayers('trickWin', clienttranslate('${player_name} wins the trick'), [
            'player_id' => $winning_player_id,
            'player_name' => $players[ $winning_player_id ]['player_name']
        ]);
        self::notifyAllPlayers('giveAllCardsToPlayer','', [
            'player_id' => $winning_player_id
        ]);

        $this->gamestate->nextState("nextTrick");
    } else {
        // Not end of trick or hand, so move to the next player
        $player_id = self::activeNextPlayer();
        self::giveExtraTime($player_id);
        $this->gamestate->nextState("nextPlayer");
    }
}

Now if four cards have been played it pretends the next player won the trick and moves them to the "cardswon" location for that player. Then it sends out two notifications. The first lets all the players know about who one the trick and gives a little pause to give players a chance to see the hand. So in the <game_name>.js file an update is made to the setupNotifications function and the implementation of the function supporting the subscription to the notification.
dojo.subscribe('trickWin', this, "notif_trickWin");
this.notifqueue.setSynchronous('trickWin', 1000);

notif_trickWin : function(notif) {
    // We do nothing here (just wait so players can view the 4 cards played before they're gone).
}

The second notification is to show the gathering of the cards and make room for the next trick of cards to be played.
dojo.subscribe('giveAllCardsToPlayer', this, "notif_giveAllCardsToPlayer");

notif_giveAllCardsToPlayer : function(notif) {
    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();
    }
},
This makes use of a few things that I haven't dug much into, but are all related to the dojo framework. I'm not going to dig further into that now.

Conclusion

Now all of the cards can be played out, but it won't wrap up the trick and deal out another hand. Seems before starting that I should fix up some of this playing and figure out who won a trick and enforce following of suit.

 

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.

Monday, December 18, 2023

Getting a Hand of Cards to Show

Overview

This is the next step in my learning how to develop a game for the online platform Board Game Arena (BGA). This skips over some things that are necessary for development that are covered in the Hearts tutorial as well as duplicating parts of it. The intent is to capture things with a different focus and to help reinforce some connections that I missed when I was going through the tutorial.

The game I'm working on through this uses a poker deck of cards, but the tooling is designed to work with all sorts of card types that are used in various games. Some naming of things might not make as much sense in this context, but there's a reason behind it. :)

Initialize a deck of cards

The Deck component (see documentation) provides the basics for the back end side of things. This allows you to define and work with a deck in the abstract as the card images are not needed until we get to the front end. To use the Deck you need a database table to hold details about each card.

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,
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;

Initialize a deck and point it at this table as part of the construction of the game.
$this->cards = self::getNew("module.common.deck");
$this->cards->init("card"); // The string passed here must match the name of the table created.

Create the card definitions and put them in a location.
$cards = [];
foreach ($this->suits as $suit_id => $suit) {
  for ($value=7; $value<=14; $value++) { // The game I'm working on is only using 7 - A
    $cards[] = ['type' => $suit_id, 'type_arg' => $value, 'nbr' => 1); // create one card of each type
  }
}
$this->cards->createCards($cards, 'deck'); // 'deck' is the location to put them in

The $this->suits needs to be defined somewhere and since it's value never changes the suggestion is to put it in material.inc.php (see documentation) so it can be referenced everywhere in the game logic.
$this->suits = [
  1 => ['name' => clienttranslate('spade'),
        'nametr' => self::_('spade')],
  2 => ['name' => clienttranslate('heart'),
        'nametr' => self::_('heart')],
  3 => ['name' => clienttranslate('club'),
        'nametr' => self::_('club')],
  4 => ['name' => clienttranslate('diamond'),
        'nametr' => self::_('diamond')],
];

Deal out the initial hand of cards

The heart and soul of a game created in BGA is the state machine as it controls the flow of the game and what is possible at each. It's important to consider when the cards should be dealt out and what should happen. The game I'm working on is a trick taking game that will need to the cards dealt out at the start of each hand. Therefore, I created a state for a newHand that goes in the states.inc.php file (see documentation). Be sure the initial state, "gameState", transitions to this new state with "transitions" => ["" => 2]".
2 => [
  "name" => "newHand",
  "description" => "",
  "type" => "game",
  "action" => "stNewHand",
  "updateGameProgression" => true,
  "transitions" => []
],

That state references an action function, stNewHand, that will deal the cards out.
$this->cards->shuffle('deck');
// Deal 8 cards to each player
$players = self::loadPlayersBasicInfos();
foreach ($players as $player_id => $player) {
  $cards = $this->cards->pickCards(8, 'deck', $player_id);
  // Notify player about their cards
  self::notifyPlayer($player_id, 'newHand', '', ['cards' => $cards]);
}

The call to self::notifyPlayer isn't strictly necessary to get the cards to show up, but it will be necessary as the game progresses and future hands are dealt out.

Finally, in terms of the backend, the hands of cards need to be made available to the front end, so it can know what to show for the hand. There is a function called getAllDatas that is used for making that happen. Adding the following to it will get the player's hand that is logged into the app.
$result['hand'] = $this->cards->getCardsInLocation('hand', $current_player_id);

Show the player's hand

The first thing is to add an image of the deck to your project. Ideally, it should be a single image to reduce loading time. I added the file img\cards.jpg to my project which you'll see referenced in the code later on. (See general advice around this in the documentation.)

Then the view needs to be updated to have a place to show the player's hand. This goes in the <game_name>_<game_name>.tpl file - something like the following will work:
<div id="myhand_wrap" class="whiteblock">
  <h3>{MY_HAND}</h3>
  <div id="myhand">
  </div>
</div>

Which then needs to be populated using javascript, so in the <game_name>.js file add a function like:
displayHand: function(cards) {
  for (var i in cards) {
    var card = cards[i];
    var suit = card.type;
    var value = card.type_arg;
    this.playerHand.addToStockWithId(this.getCardUniqueId(suit, value), card.id);
  }
},

However, for this to work requires a couple of things to be set up. BGA provides a component called Stock to help with this (see documentation). Add it to the define block at the top of the file by adding "ebg/stock".
define([
  "dojo","dojo/_base/declare",
  "ebg/core/gamegui",
  "ebg/counter",
  "ebg/stock"
],

Next, as part of the setup function define the player's hand and create the cards that can go in it. Note, when creating the hand (the call to this.playerHand.create) it expects the id of the container in the view (we defined that as 'myhand' at the beginning of this section) and the width and height of a single card in the image we added. I'm making use of a full poker deck image, though the game I'm building only makes use of the cards 7 and up, so I note there are 13 images per row, but only create values 7 through 14.
this.playerHand = new ebg.stock();
this.playerHand.create(this, $('myhand'), 72, 96);
this.playerHand.image_items_per_row = 13;
this.playerHand.setSelectionMode(1);

// Create card types
for (var suit = 1; suit <= 4; suit++) {
  for (var value = 7; value <= 14; value++) {
    var card_type_id = this.getCardUniqueId(suit, value);
    this.playerHand.addItemType(card_type_id, card_type_id, g_gamethemeurl + 'img/cards.jpg', card_type_id);
  }
}

Finally, we display the cards that are in the player's hand that we did on the backend. The setup function has a gamedatas parameter that has all the details in it, so we can refer to 'hand' that we used in the previous section when we dealt out the cards ($result['hand']). Additionally, we'll just call the displayHand function we created earlier.
this.displayHand(this.gamedatas.hand);

Conclusion

Now we have a hand of cards showing! Though, this is still only the very beginning of things as the only thing that can be done is see the first hand.