Saturday, January 6, 2024

Make Playing a Little Easier

Overview

Finally getting to the point where the game can be played most of the way through. Scoring is available for all but two of the games and which games have been selected are shown for each player. The next step I wanted to take was to make the gameplay a little quicker, so let's get into it.

Player preference to not verify card play

Currently, to play a card the player must select the card, then click the "Play card" button. Some players (and certainly me while testing it) will just want the cards to be played when they are selected. Since this is an individual player preference it should be handled with the user preferences. Which involved creating a new gamepreferences.json file and defining the preference with some details about its behavior.
{
    "100": {
        "name": "Prompt for card play confirmation",
        "needReload": true,
        "values": {
            "1": {
                "name": "Enabled"
            },
            "2": {
                "name": "Disabled"
            }
        },
        "default": 1
    }
}

All of the changes I wanted to make related to this were in the front end and referencing this preference is done through this.prefs[100]. The first thing I did was to not show the action button to the player. This change was done in the onUpdateActionButtons function.
case 'playerTurn':
    if (this.prefs[100].value == 1) {
        this.addActionButton('btnPlayCard', _('Play card'), 'onBtnPlayCard');
    }
    break;

I check if the value is 1, indicating it's enabled and that we want to show the action button.  Then I want to play a card when it is selected. In the setup function, we want to add a listener to the player's hand.
if (this.prefs[100].value == 2) {
    dojo.connect(this.playerHand, 'onChangeSelection', this, 'onHandCardSelect');
}

In this case, we're checking if the value is 2, indicating it's disabled and the action button won't be shown. Here's the onHandCardSelection function.
onHandCardSelect: function(control_name, item_id) {
    console.log("onHandCardSelect listener");
    if (!this.isCurrentPlayerActive()) return;
    if (item_id === undefined) return;

    this.onBtnPlayCard();
},

The check for undefined short circuits this function the second time it is called - when the card is unselected.

Finally, there's the case when someone has preselected the card they want to play and we want to play it as soon as it gets to their turn. This means a change to the onEnteringState function.
onEnteringState: function( stateName, args ) {
    console.log( 'Entering state: '+stateName );
    switch( stateName )
    {
    case 'playerTurn':
        if (this.isCurrentPlayerActive()) {
            if (this.prefs[100].value == 2) {
                const selected_cards = this.playerHand.getSelectedItems();
                if (selected_cards.length === 1) {
                    this.onBtnPlayCard();
                }
            }
        }
        break;
 
    case 'dummmy':
        break;
    }
},

End a hand early when there are no more points

Some games need to be played all the way through, but others can have all of the points in the hand come out in the first round. It's a little tedious to need to play through the rest of the cards when nothing will come of it. Since this will vary based on the game selected it seems like a good place to revisit the scorer classes I created for calculating how many points each player took in their hands. I added a new remainingPoints function that returns true if there are cards in hand that score points for the game. Let's take a look at the implementation for the game Queens.
function remainingPoints(array $cards_in_hands): bool {
    foreach ($cards_in_hands as $card) {
        if ($card['type_arg'] == QUEEN || ($card['type'] == HEART && $card['type_arg'] == KING)) {
            return true;
        }
    }
    return false;
}

This checks the given array of cards to see if there are any Queens or the King of Hearts. This will be used in the stNextPlayer function in the <game_name>.game.php file.
if ($this->cards->countCardInLocation(HAND) == 0) {
    $this->gamestate->nextState("endHand");
} else {
    $scorer = $this->getScorer();
    $cards_in_hands = $this->cards->getCardsInLocation(HAND);
    if (!$scorer->remainingPoints($cards_in_hands)) {
        $cards_left = [];
        $players = $this->loadPlayersBasicInfos();
        foreach ($players as $player_id => $player) {
            $cards_left_list = [];
            $hand = $this->cards->getCardsInLocation(HAND, $player_id);
            usort($hand, [$this, "sortCards"]);
            foreach ($hand as $card) {
                $cards_left_list[] = $this->suits[$card['type']]['name'].''.$this->values_label[$card['type_arg']];
            }
            $cards_left[] = self::getPlayerNameById($player_id).' - '.implode(', ', $cards_left_list);
        }
        $cards_left_final = implode('<br>', $cards_left);
        self::notifyAllPlayers('earlyEnd', clienttranslate('Ending the hand early as all scoring cards are out<br><br>Cards left:<br>${cards_left}'), [
            'cards_left' => $cards_left_final,
            'remaining_cards' => $cards_in_hands,
        ]);
        $this->gamestate->nextState("endHand");
    } else {
        $this->gamestate->nextState("nextTrick");
    }
}

If no points are remaining in anyone's hand, this provides a notification to inform players what cards are still out there. The front end will make use of that notification to update where the cards are visually. As is usual the <game_name>.js file needs to be updated in two places. The first is adding a subscription to the "earlyEnd" notification in the setupNotifications function. The next is the implementation of the function called for that notification - notif_earlyEnd.
notif_earlyEnd: function(notif) {
    for (let i in notif.args.remaining_cards) {
        const card = notif.args.remaining_cards[i];
        this.playCardOnTable(card.location_arg, card.type, card.type_arg, card.id);
    }

    document.querySelectorAll('.cardontable').forEach(e => this.slideToObjectAndDestroy(e, 'playertables'));
},

This puts all of the cards onto the table, then slides them to the "playertables" container, which holds the played cards and removes them from view.

Conclusion

I thought checking for point cards was going to be harder than it ended up being. I started exploring creating a Card class to encapsulate checking for equality and seeing if I could make it read better when they were created, but I ended up giving up on it. It seemed a small improvement for the work, though maybe I'll have another idea around it in the future.

There are still some things that might speed up the gameplay - for instance, recognizing if the player can't lose the rest of the tricks - but I think I want to be able to finish a game. That means finishing up the scoring for Guillotine and finally getting around to dealing with Dominoes. That might need to be split over a couple of sessions as it plays completely differently.

No comments: