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!)
No comments:
Post a Comment