Overview
Currently, one hand can be played through, but it won't continue through a full game. Some simple tweaks need to be done to allow the next hand of cards to be dealt out, but the majority of the work is figuring out how to keep track of the games that a player has played.
Deal out the next hand
Allow the "endHand" state to transition to the "newHand" state.
30 => [
"name" => "endHand",
"description" => "",
"type" => "game",
"action" => "stEndHand",
"transitions" => ["nextHand" => 2]
],
Update the
stEndHand
function to follow that transition by adding $this->gamestate->nextState("nextHand");
to the end of it. This moved to the next hand but didn't deal out the cards to the players because the cards weren't in the deck. To get them requires updating the stNewHand
function to move all the cards into the deck by calling $this->cards->moveAllCardsInLocation(null, DECK);
before the cards are shuffled. Passing null
as the first argument gathers cards from all locations. I created a constant for the 'deck' location to avoid typos.Finally, to get the cards showing on the front end it needs to subscribe to the "newHand" notification, which the supporting function looks like the following.
notif_newHand : function(notif) {
// We received a new full hand of 8 cards.
this.playerHand.removeAll();
for ( var i in notif.args.cards) {
var card = notif.args.cards[i];
var color = card.type;
var value = card.type_arg;
this.playerHand.addToStockWithId(this.getCardUniqueId(color, value), card.id);
}
},
Now things should look appropriate after finishing a hand in the game.
Deadlock error
While playing through I encountered a deadlock error that seemed to be related to accessing state values. This suggested to me that maybe I had entered the action for a player before the previous action had fully been processed. So I introduced some synchronous notifications with some delays. The delays will help give players time to process what changed in the game. I'm also assuming synchronous notifications require that all clients have processed the notification before moving on, which should help with that deadlock as well.
Keep track of games played
To indicate that a game has been played there needs to be some way of keeping track of when they are selected. I considered trying to use game states to keep track of this, but that seemed unwieldy at best and I wasn't seeing a great way to get the state of all of the games. In addition, this seemed like a good time to explore creating a database table (besides the instructions given in the Hearts demo).
CREATE TABLE IF NOT EXISTS `player_game` (
`player_game_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`player_id` INT NOT NULL,
`game_id` SMALLINT NOT NULL,
`played` BOOL NOT NULL DEFAULT 0,
PRIMARY KEY (`player_game_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
When the game is initialized this new table needs to be populated with all the games for each player. So in the
setupNewGame
function I called the following. (Technically, I pulled it out to a separate function...)// Populate games to play for player
$players = $this->loadPlayersBasicInfos();
$game_sql = "INSERT INTO player_game (player_id, game_id) VALUES ";
$game_values = [];
foreach ($players as $player_id => $player) {
foreach ($this->games as $game_id => $game) {
$game_values[] = "('$player_id', '$game_id')";
}
}
$game_sql .= implode($game_values, ',');
self::DbQuery($game_sql);
Next, I wanted to rely on those entries to populate the available games for the player to pick from. Way back when that logic was being wired up I'd hard-coded the values in the
argSelectGame
function. So now it will pull the games from the database.function argSelectGame() {
$player_id = self::getActivePlayerId();
return [
"available_games" => $this->gameStates($player_id, true)
];
}
I pulled most of the logic out to a separate function since I wanted to also know which games had been played as part of the setup.
function gameStates($player_id, $available_only = false) {
$game_sql = "SELECT player_game_id, player_id, game_id, played FROM player_game WHERE player_id=$player_id";
if ($available_only) {
$game_sql .= " AND played=0";
}
$available_games = self::getCollectionFromDb($game_sql);
$game_states = [];
foreach ($available_games as &$player_game) {
$current_game = $this->games[$player_game['game_id']];
$game_states[] = [
'player_id' => $player_id,
'game_id' => $player_game['game_id'],
'game_type' => $current_game['type'],
'game_name' => $current_game['name'],
'played' => $player_game['played']
];
}
return $game_states;
}
Understanding DB queries
I'm showing the finished product, but when I first put this together I didn't have the
player_game_id
column in the database. I had defined the player_id
and game_id
columns as a joined primary key. So when I was testing getting the tables for all players I was only getting one game for each, not six.That's because by default the DB query tool returns an associative array using the first column in the query as the key. (You can see this in the documentation for the
getCollectionFromDB
function.)I opted to add a separate primary key and add it to the query to avoid the issue.
Show game selection state
While it's important to show the player the correct games that they have remaining to select from, invariably others want to know this as well. It also helps to understand how far through the game folks are if they can see everyone's options. BGA has player panel sections intended to provide a nice summary of the current game state. So let's add the games for each player to that and show if they've been played or not.
The first step was to make the data available as part of the
getAllDatas
function in the back end.$player_game_states = [];
foreach ($result['players'] as $player) {
$player_game_states[$player['id']] = $this->gameStates($player['id']);
}
$result['player_game_states'] = $player_game_states;
This builds up an array of game states for each player which will be walked through in the
<game_name>.js
file. The 'player_board_' + player_id
is the ID attribute for the player panel for that particular player. See the documentation around the player panel for more details.// Setting up player boards
for( var player_id in gamedatas.players )
{
var player = gamedatas.players[player_id];
for (var i in gamedatas.player_game_states[player_id]) {
var game_state = gamedatas.player_game_states[player_id][i];
dojo.place(this.format_block('jstpl_game_display', {
player_id: player_id,
game_type: game_state.game_type,
game_name: game_state.game_name,
game_abbr: game_state.game_name.substring(0, 1),
played: ((game_state.played === '1') ? 'played' : 'available')
}), 'player_board_' + player_id);
this.addTooltipToClass('game_display_' + game_state.game_type, _(game_state.game_name), '');
}
}
This is making use of a template variable "jstpl_game_display" that is added to the
<game_name>_<game_name>.tpl
file. I'm not certain how I populate game_abbr
is appropriate for translations, but figured that the tooltip giving the full name should be sufficient for getting players around it.var jstpl_game_display = '<div id="glt_game_${game_type}_${player_id}" class="game_display game_display_${game_type} ${played}">${game_abbr}</div>';
Finally, add a little style to the
<game_name>.css
file so that the games take up a single row and provide a visual clue if they've been played..game_display {
font-size: smaller;
font-weight: bold;
float: left;
margin-left: 10px;
}
.game_display.played {
font-weight: normal;
text-decoration: line-through;
color: gray;
}
Update the player panel when they select a game
When the player has selected a game the system needs to record the decision they made. When I was going about doing this I opted to update how games were defined in the
material.inc.php
file to key off from the ID instead of the type. In truth, this change was needed to implement the gameStates
function above and I forgot to mention it.$this->games = [
1 => [
'type' => 'parliament',
'name' => clienttranslate('Parliament')
],
2 => [
'type' => 'spades',
'name' => clienttranslate('Spades')
],
3 => [
'type' => 'queens',
'name' => clienttranslate('Queens')
],
4 => [
'type' => 'royalty',
'name' => clienttranslate('Royalty')
],
5 => [
'type' => 'dominoes',
'name' => clienttranslate('Dominoes')
],
6 => [
'type' => 'guillotine',
'name' => clienttranslate('Guillotine')
],
];
This led to the
gameSelection
function being updated to be the following.function gameSelection($selected_game) {
self::checkAction("gameSelection");
$player_id = self::getActivePlayerId();
$game_id = null;
$game_name = null;
foreach ($this->games as $id => $game) {
if ($game['type'] == $selected_game) {
$game_id = $id;
$game_name = $game['name'];
break;
}
}
self::setGameStateValue(SELECTED_GAME, $game_id);
$this->recordSelectedGame($player_id, $game_id);
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,
'game_type' => $selected_game,
]
);
$this->gamestate->nextState("startHand");
}
The significant changes to it were adding the call to
recordSelectedGame
(which will be shown next) and adding the game_type
to the notification so that we can find the appropriate game in the view to show that it's been selected. Here's the recordSelectedGame
function.function recordSelectedGame($player_id, $game_id) {
self::DbQuery("UPDATE player_game SET played=1 WHERE player_id='$player_id' AND game_id='$game_id'");
}
Finally, in the front end, the
notif_gameSelection
function is updated to add the "played" class to the element for a different styling.document.getElementById('glt_game_' + notif.args.game_type + '_' + notif.args.dealer_id)
.classList.add('played');
Conclusion
There's still plenty to do to make this function nicely, but it can be played all the way through and it keeps track of the games the player has played.
Granted the game doesn't reach an ending state, but I think before that I want to add in some nice to-haves to make it a little easier to play. Some current thoughts:
- Provide an option to play the card when clicked instead of requiring the player to click the card, and then the play card button.
- When a hand has no more points to gain in it, provide a way to end the hand right away.
Hopefully, adding those features will make testing it a little easier too. :)
No comments:
Post a Comment