Thursday, January 11, 2024

Change How Cards are Played for Dominoes

Overview

When we last saw our hero game being developed it was just starting to handle Dominoes - wiring up some of the plumbing and being able to determine if a played card is valid. Now I want to make it look more like how it would in person, by showing the played cards in the center of the table and laid in order by suit.

Layout cards in the center of the table

The first step towards this is to create a place for the cards to go by modifying the <game_name>_<game_name>.tpl file.
<div id="playertables" class="whiteblock">
    <!-- BEGIN player -->
    <div class="playertable playertable_{DIR}">
        <div class="playertablename" style="color:#{PLAYER_COLOR}">{PLAYER_NAME}</div>
        <div id="dealer_p{PLAYER_ID}" class="playertableselectedgame">{SELECTED_GAME}</div>
        <div id="playertablecard_{PLAYER_ID}" class="playertablecard"></div>
    </div>
    <!-- END player -->
    <div id="dominoesplayarea"></div>
</div>

<div id="myhand_wrap" class="whiteblock">
    <h3>{MY_HAND}</h3>
    <div id="myhand">
    </div>
</div>

<script type="text/javascript">

// Javascript HTML templates
var jstpl_card = '<div class="card cardontable" id="cardontable_${card_id}" style="background-position:-${x}px -${y}px"></div>';
var jstpl_game_display = '<div id="glt_game_${game_type}_${player_id}" class="game_display game_display_${game_type} ${played}">${game_abbr}</div>';
</script>

The changes here are moving the "whiteblock" class from the inner div beginning the player to the playertables div and adding the dominoesplayarea div. The last change is a change to the id attribute for the jstpl_card template. Previously it was cardontable_${player_id}. Unfortunately, when the same player puts multiple cards on the table the IDs collide and the behavior gets a little funny when you try moving them around. Fortunately, nothing was looking for them based on the player so the change didn't impact much other than swapping out the player_id for card_id when building the template.

Then the styling needs to be updated to allow for cards to be put in this newly created div.
#dominoesplayarea {
    position: relative;
    width: 440px;
    height: 300px;
    top: 140px;
    left: 180px;
}

#playertables {
    position: relative;
    width: 800px;
    height: 580px;
}

This makes the playertables div take up enough space to allow for the cards to be played and puts the dominoesplayarea tucked inside of the area the player puts their cards during the other games.

Okay, let's actually put a card there

When a card is played the back end sends a playCard notification to the front end that currently makes the card visible under the player's name. I could just modify that to be aware of what game was being played, but I opted to create a different one to try to keep the understanding of what the game is on the back end. So the playCard function in the back end now looks like this.
function playCard($card_id) {
    self::checkAction("playCard");

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

    $selected_game_id = self::getGameStateValue(SELECTED_GAME);
    if ($this->games[$selected_game_id]['type'] == "dominoes") {
        $this->checkPlayableCardForDominoes($current_card);
        $this->cards->moveCard($card_id, CARDS_ON_TABLE, $player_id);

        if (!self::getGameStateValue(SPINNER)) {
            self::setGameStateValue(SPINNER, $current_card['type_arg']);
        }

        self::notifyAllPlayers(
            'playCardForDominoes',
            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,
                'spinner' => self::getGameStateValue(SPINNER)
            ]
        );

        $this->gamestate->nextState("turnTaken");
    } else {
        $this->checkPlayableCard($player_id, $current_card);
        $this->cards->moveCard($card_id, CARDS_ON_TABLE, $player_id);
 
        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");
    }
}

I realized while developing the front end that it needed to know what the spinner was to help with placing cards appropriately. (The spinner is rotated perpendicular to the other cards so players don't forget what rank it is.) Now the front end needs to handle this notification. Reminder - that involves updating the setupNotifications function to subscribe to it and indicate the function to call. I also called setSynchronous for it with a 100 delay, just like for the playCard notification. Here's the notif_playCardForDominoes function.
notif_playCardForDominoes : function(notif) {
     this.playCardInCenter(
        notif.args.player_id,
        notif.args.suit,
        notif.args.value,
        notif.args.card_id,
        notif.args.spinner
    );
},

Yeah - not too exciting, since the same logic will need to be called in the setup function, but we'll get to that after the real work is shown.
playCardInCenter : function(player_id, suit, value, card_id, spinner) {
    dojo.place(this.format_block('jstpl_card', {
        x : this.cardwidth * (value - 2),
        y : this.cardheight * (suit - 1),
        card_id : card_id
    }), 'dominoesplayarea');
 
    if (player_id != this.player_id) {
        // Some opponent played a card
        // Move card from player panel
        this.placeOnObject('cardontable_' + card_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_' + card_id, 'myhand_item_' + card_id);
            this.playerHand.removeFromStockById(card_id);
        }
    }

    dojo.style('cardontable_' + card_id, 'z-index', value);
    const x_pos = 20 + (suit-1) * 110;
    const y_pos = 200 - ((value-7) * 20) - (Number(value) > Number(spinner) ? 30 : 0);
    this.slideToObjectPos('cardontable_' + card_id, 'dominoesplayarea', x_pos, y_pos).play();

    if (value === spinner) {
        this.rotateTo('cardontable_' + card_id, 90);
    }
},

This is very similar to the playCardOnTable function that handles other games, but the jstpl_card template is put into a different div, and the new element for the template is slid to a more specific spot. The z-index is used to avoid cards going hiding on a refresh since they are placed overlapping each other. Finally, if it's a spinner rotate it.

Now we need to update the setup function so when someone reloads the page it won't mess with things.
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;

    if (gamedatas.selected_game_type === 'dominoes') {
        this.playCardInCenter(player_id, suit, value, card.id, gamedatas.spinner);
    } else {
        this.playCardOnTable(player_id, suit, value, card.id);
    }
}

With this function being aware of the selected game it makes me seriously consider the benefit of using a different notification. I'm going to stick with it for now since it's working, but might revisit that choice after Dominoes is supported.

Conclusion

Almost there! I'm psyched with how the cards are showing up for Dominoes and the game can mostly be played through. Just a couple of remaining details.
  • When an Ace is played the player can play as many cards as they want to - as long as they're valid - before passing.
  • Need to end the game, which involves scoring it.
  • Need to tighten up the passing and at least know if the player passed when they could've played.

No comments: