Overview
After finishing the scoring for Dominoes, that leaves scoring for Guillotine for all of the games to be completed. That shouldn't take much to complete, so I'll also dig into collecting stats for the game.
Guillotine Scoring
Guillotine follows similar rules to the other non-Dominoes games only with more things scoring points. The catch is that the first and last tricks are worth 5 points each just for winning them. Building on the existing
GLTScorer
interface I opted to pass in an optional trick_winners
array.class GLTGuillotineScorer implements GLTScorer {
function remainingPoints(array $cards_in_hands): bool {
if (count($cards_in_hands) > 0) {
return true;
}
return false;
}
function score(array $player_ids, array $won_cards, array $trick_winners = null): array {
$player_to_points = [];
foreach ($player_ids as $player_id) {
$player_to_points[$player_id] = 0;
}
$player_to_points[$trick_winners[FIRST_TRICK_WINNER]] += 5;
$player_to_points[$trick_winners[LAST_TRICK_WINNER]] += 5;
foreach ($won_cards as $card) {
$player_id = $card['location_arg'];
if ($card['type'] == SPADE) {
$player_to_points[$player_id] += 5;
}
if ($card['type_arg'] == QUEEN) {
$player_to_points[$player_id] += 10;
}
if ($card['type'] == HEART && $card['type_arg'] == KING) {
$player_to_points[$player_id] += 10;
}
}
return $player_to_points;
}
}
Now that parameter needs to be populated. I'm less thrilled with this since it requires another place to know which game was selected, but I opted to update the
stEndHand
function where the scoring is calculated.$selected_game_id = self::getGameStateValue(SELECTED_GAME);
$trick_winners = null;
if ($selected_game_id == 6) {
// Guillotine needs to know the winner of the first and last tricks
$trick_winners = [
FIRST_TRICK_WINNER => self::getGameStateValue(FIRST_TRICK_WINNER),
LAST_TRICK_WINNER => self::getGameStateValue(LAST_TRICK_WINNER)
];
}
$player_to_points = $scorer->score(array_keys($players), $cards, $trick_winners);
As can be seen by the use of
self::getGameStateValue
I opted to use game state variables to keep track of the first and last trick winners. Which means they need to be defined as part of the __construct
in the <game_name>.game.php
file. Then kept track of in the stNextPlayer
function.// Guillotine is the only game that worries about this.
if ($this->cards->countCardInLocation(CARDS_WON) == 0) {
self::setGameStateValue(FIRST_TRICK_WINNER, $winning_player_id);
}
...
if ($this->cards->countCardInLocation(HAND) == 0) {
// Guillotine is the only game that cards about LAST_TRICK_WINNER
self::setGameStateValue(LAST_TRICK_WINNER, $winning_player_id);
$this->gamestate->nextState("endHand");
} else {
...
And finally, reset them just to be safe in the
resetTrackingValues
function that I'm calling from the stEndHand
function.function resetTrackingValues() {
self::setGameStateValue(SELECTED_GAME, 0);
// Dominoes
self::setGameStateValue(OUT_FIRST, 0);
self::setGameStateValue(OUT_SECOND, 0);
self::setGameStateValue(SPINNER, 0);
// Guillotine
self::setGameStateValue(FIRST_TRICK_WINNER, 0);
self::setGameStateValue(LAST_TRICK_WINNER, 0);
}
After doing this it occurred to me that the scorer classes that have
remainingPoints
calculation might be better if they just check if the the won tricks have all of the points in them. Making that change seems like it might help with validating things. Currently, I'm just assuming that everything is fine, but it's usually better to be a bit more pessimistic and do more validation to ensure things haven't gotten into an unexpected state. Something I'll think more about as I continue on.Progress Tracking
According to the documentation around the states, at least one of them should update the game progression. I originally had this after each players turn, but reduced it to only be updated at the end of a turn, which is done by setting
updateGameProgression => true
inside of the configuration for a state. Having that will automatically call the getGameProgression
function that is pre-generated to return 0 by default. Since there are 24 games to be played it seemed a simple enough equation to use the count of the games that were played.function getGameProgression() {
$game_sql = "SELECT count(*) count FROM player_game WHERE played = 1";
$played_games = self::getUniqueValueFromDB($game_sql);
return ($played_games / 24) * 100;
}
End the game
To see any statistics the game needs to come to an end. It seems to make sense to do this at the end of a hand. Given how I've implemented things this could be one of two states -
endHand
or dominoesEndHand
- so I could either duplicate the check or create a separate state. I went with the separate state, though I'm not really sure what the tradeoffs are between the choices. (Probably, less database writing of things and back and forth with the front end if I duplicated the check, so maybe I'll change that in the future.)I created
checkEndGame
and updated endHand
and dominoesEndHand
to point to it.31 => [
"name" => "checkEndGame",
"description" => "",
"type" => "game",
"action" => "stCheckEndGame",
"transitions" => ["newHand" => 2, "endGame" => 99]
],
Then the implementation of the
stCheckEndGame
function is just checking how many played games there are.function stCheckEndGame() {
$game_sql = "SELECT count(*) count FROM player_game WHERE played = 1";
$played_games = self::getUniqueValueFromDB($game_sql);
if ($played_games == 24) {
$this->gamestate->nextState("endGame");
} else {
$this->gamestate->nextState("newHand");
}
}
Stats
Stats are defined in a
stats.json
file and have some documentation describing how to set them up and how to use them.Points per game played
The desire is to know how you did within each type of game played. So for each game, I added a stat to keep track of the number of points the player got for it.
{
"player": {
"points_from_parliament": {
"id": 11,
"name": "Points from Parliament",
"type": "int"
},
"points_from_spades": {
"id": 12,
"name": "Points from Spades",
"type": "int"
},
"points_from_queens": {
"id": 13,
"name": "Points from Queens",
"type": "int"
},
"points_from_royalty": {
"id": 14,
"name": "Points from Royalty",
"type": "int"
},
"points_from_dominoes": {
"id": 15,
"name": "Points from Domnioes",
"type": "int"
},
"points_from_guillotine": {
"id": 16,
"name": "Points from Guillotine",
"type": "int"
}
}
After updating this you need to press the "Reload statistics configuration" button on the manage game page otherwise, they don't show up. Though for the stats to have any values you need to populate them which is done in a couple of places in the
<game_name>.game.php
file. The first is just initializing them in the setupNewGame
function.self::initStat('player', POINTS_FROM_PARLIAMENT, 0);
self::initStat('player', POINTS_FROM_SPADES, 0);
self::initStat('player', POINTS_FROM_QUEENS, 0);
self::initStat('player', POINTS_FROM_ROYALTY, 0);
self::initStat('player', POINTS_FROM_DOMINOES, 0);
self::initStat('player', POINTS_FROM_GUILLOTINE, 0);
The real work is updating the stat as hands are scored. Since I created the
updateScores
function for doing this I made the update there.self::incStat($points, $game_stat, $player_id);
That line is deceptively simple as it required adding a
$game_stat
parameter to the updateScores
function. Then an update to the two locations that call it. Where it's called in the stDominoesEndHand
function the parameter is just hard coded to points_from_dominoes
. The other call is in the stEndHand
function, which makes use of the GTLScorer
objects. So I added another gameStat
function to those that would return the appropriate value for the scorer being used.$this->updateScores($players, $player_to_points, $scorer->gameStat());
Points on specific player's calls
This got a little tricky as what I really wanted was a stat that connects to players, but I couldn't find something that allowed me to dynamically name stats. So instead, I called them
points_on_own_calls
and points_on_player_1_calls
which isn't as clear. So I added these to the stats.json
file."points_on_own_calls": {
"id": 17,
"name": "Points on own calls",
"type": "int"
},
"points_on_player_1_calls": {
"id": 18,
"name": "Points from player to the left calls",
"type": "int"
},
"points_on_player_2_calls": {
"id": 19,
"name": "Points from second player to the left calls",
"type": "int"
},
"points_on_player_3_calls": {
"id": 20,
"name": "Points from player to the right calls",
"type": "int"
}
Then updated the
updateScores
function to figure out who the dealer of the game was in relation to the player being scored.function updateScores($players, $player_to_points, $game_stat) {
$dealer_id = self::getGameStateValue(DEALER);
$player_1_id = self::getPlayerAfter($dealer_id);
$player_2_id = self::getPlayerAfter($player_1_id);
$player_3_id = self::getPlayerAfter($player_2_id);
foreach ($player_to_points as $player_id => $points) {
self::incStat($points, $game_stat, $player_id);
$player_call = POINTS_ON_OWN_CALLS;
if ($player_id == $player_1_id) $player_call = POINTS_ON_PLAYER_3_CALLS;
else if ($player_id == $player_2_id) $player_call = POINTS_ON_PLAYER_2_CALLS;
else if ($player_id == $player_3_id) $player_call = POINTS_ON_PLAYER_1_CALLS;
self::incStat($points, $player_call, $player_id);
$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]);
}
This works, but my head hurts a little looking at it and I should probably rename the variables to reduce some of the confusion. The first part gets the player IDs in clockwise order from the dealer. Then in the foreach block, it figures out which of those IDs match the current player being scored, which determines where the dealer sits in relation to them.
Advance game state
Since functions can be called from the chat, I decided to create a function that would move the game to the final round.
function advanceGameState() {
$players = self::loadPlayersBasicInfos();
$player_ids = array_keys($players);
for ($i=0; $i<20; $i++) {
$current_player_id = $player_ids[($i % 4)];
$current_game_id = ($i % 6) + 1;
// After 12 the games being played repeat, so offsetting it by one.
if ($i >= 12) {
$current_game_id = (($i + 1) % 6) + 1;
}
$current_game = $this->games[$current_game_id];
// Update games played
$this->recordSelectedGame($current_player_id, $current_game_id);
// Update scores
$player_to_points = [];
foreach ($player_ids as $player_id) {
$player_to_points[$player_id] = 0;
}
$point_total = 0;
switch ($current_game_id) {
case 1:
$point_total = -50;
break;
case 2:
$point_total = 30;
break;
case 3:
$point_total = 30;
break;
case 4:
$point_total = 30;
break;
case 5:
$point_total = -40;
break;
case 6:
$point_total = 100;
break;
}
$player_to_points[$current_player_id] = $point_total;
$this->updateScores($players, $player_to_points, 'points_from_'.$current_game['type']);
}
}
I could probably make it a little more sophisticated, but my first pass just gives the calling player all of the points for that game. Additionally, the browser needs to be refreshed after running it to see the update completion state and the visual representation of the games that have been selected.
Conclusion
The game can be played to the end and a score with stats can be output. Now we're digging into the nice to-haves and things that will make playing better. Currently on my list are:
- Allow a player to undo the card they just played. It's not ideal, but it happens and I know I hate it when a game doesn't allow me to fix a silly mistake that I would've in person. Though I know others are more strict, so perhaps this would be a game option to allow this.
- Allow a player preference for automatically passing if the player cannot play.
I also think there's some refactoring I should do, maybe go back over my notes and revisit those places where I made design decisions and see if they still hold up. Not to mention there's game metadata that I've been lax about and I should update.