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.

No comments: