euchre-live

Euchre web-app for the socially distant family
git clone git://git.alexkarle.com/euchre-live.git
Log | Files | Refs | README | LICENSE

commit 0a1c8ab6e9e02b8a8293598b1a4d1ae8fc063701 (patch)
parent b92b277c80e007b8aa4e4f2687b2557032ccdcac
Author: Alex Karle <alex@karle.co>
Date:   Sat, 11 Apr 2020 11:18:50 -0400

Euchre::Dealer: Handle 'loner' value on order msg

The order msg (previously 'vote_trump') is used to vote for the trump
suit. It now passes the additional 'loner' boolean, which is used to
denote going alone!

To handle this properly / with the least confusion, I had to migrate the
table to 4 undefs and have people play into their "slot". This made
computing the winner idx much easier (no need to rotate the outcome of
trick_winner and accomodate whether someone sat out).

This has the added bonus of making it easier for clients to render each
slot on the table in the proper order.

Diffstat:
Mlib/Euchre/Dealer.pm | 56+++++++++++++++++++++++++++++++++-----------------------
Mlib/Euchre/Game.pm | 2++
Mpublic/debug.html | 14+++++++++-----
3 files changed, 44 insertions(+), 28 deletions(-)

diff --git a/lib/Euchre/Dealer.pm b/lib/Euchre/Dealer.pm @@ -33,11 +33,12 @@ our @EXPORT = qw( # turn => 0-3, # trump => 0-3, # caller => 0-3, -# table => [ c1, c2, c3, c4 ], # up to 4 cards +# table => [ c1, c2, c3, c4 ], # exactly 4, undef if not played # score => [X, Y], # phase => 'lobby', 'play', 'vote', 'end' # trump_nominee => 0-23, # pass_count => 0-7, +# out_player => -1-3, -1 if none, else idx of "out player" # } # # We decided the players would keep track of their own hands @@ -53,7 +54,7 @@ our @EXPORT = qw( # name => username, # seat => 0-3, # ws => websocket obj, -# hand => cards in hand +# hand => cards in hand, # } # # The players keyed on ws id is key (pun) because the closure in @@ -110,7 +111,7 @@ sub handle_msg { start_game => [\&start_game, 'lobby', "Game already started"], # Gameplay - vote_trump => [\&vote_trump, 'vote', "Not time for a vote", 1], + order => [\&order, 'vote', "Not time for a vote", 1], play_card => [\&play_card, 'play', "Can't play cards yet", 1], ); @@ -152,7 +153,7 @@ sub join_game { dealer => -1, trump => -1, tricks => [0, 0, 0, 0], - table => [], + table => [undef, undef, undef, undef], caller => -1, score => [0, 0], phase => 'lobby', @@ -245,10 +246,12 @@ sub start_new_round { # Shift dealer and deal $game->{dealer} = ($game->{dealer} + 1 % 4); - $game->{table} = []; + $game->{table} = [undef, undef, undef, undef]; $game->{tricks} = [0,0,0,0]; + $game->{out_player} = -1; deal_players_hands($game); + # Signal vote of player next to dealer... reset_turn($game); $game->{phase} = 'vote'; @@ -271,7 +274,7 @@ sub deal_players_hands { # msg.vote = 'suit' or 'pass' # msg.loner = 0 or 1 -sub vote_trump { +sub order { my ($p, $msg) = @_; my $game = $p->{game}; @@ -285,12 +288,15 @@ sub vote_trump { broadcast_gamestate($game); } } elsif (defined suit_to_id($msg->{vote})) { - # XXX handle loner (needs to handle next-player) - # NOTE: we let clients decide what's kosher to vote - # based on their hand state, pass_count + # TODO: add hand/suit validation? $game->{trump} = suit_to_id($msg->{vote}); $game->{caller} = $p->{seat}; $game->{phase} = 'play'; + if ($msg->{loner}) { + my $partner_seat = ($p->{seat} + 2) % 4; + $game->{out_player} = $partner_seat; + $game->{tricks}->[$partner_seat] = 'X'; + } reset_turn($game); broadcast_gamestate($game); } else { @@ -321,23 +327,23 @@ sub play_card { } # Update the table and current player - push @{$game->{table}}, $msg->{card}; + $game->{table}->[$seat] = $msg->{card}; next_turn($game); - if (@{$game->{table}} >= 4) { + # Adjust num cards on table by if there's an out player + my $played_cards = scalar grep { defined } @{$game->{table}}; + my $out_adj = ($game->{out_player} >= 0 ? 1 : 0); + if ($played_cards >= (4 - $out_adj)) { # End trick -- update tricks, clear table, and set current player - my @table = map { cname_to_id($_) } @{$game->{table}}; + my @table = map { defined($_) ? cname_to_id($_) : -1 } @{$game->{table}}; my $winner_id = trick_winner($game->{trump}, @table); - # this is the id in TABLE, not players - # at this point turn is back to the start... - $winner_id = ($winner_id + $game->{turn}) % 4; - $game->{tricks}->[$winner_id]++; $game->{turn} = $winner_id; - $game->{table} = []; + $game->{table} = [undef, undef, undef, undef]; - if (sum(@{$game->{tricks}}) >= 5) { + my @num_tricks = grep { /^\d+$/ } @{$game->{tricks}}; + if (sum(@num_tricks) >= 5) { # End round -- update scores, clear tricks, push dealer my ($team_id, $score) = score_round($game->{caller}, @{$game->{tricks}}); $game->{score}->[$team_id] += $score; @@ -390,6 +396,7 @@ sub broadcast_gamestate { msg_type => 'game_state', game => $msg, hand => $p->{hand}, + sit_out => $p->{sit_out}, }; $p->{ws}->send({ json => $json }); } @@ -406,15 +413,18 @@ sub send_error { sub next_turn { my ($game) = @_; - $game->{turn} = ($game->{turn} + 1) % 4; - # XXX pass again if loner! + my $turn = ($game->{turn} + 1) % 4; + if ($turn == $game->{out_player}) { + # It's a loner! Only gonna be one of these... + $turn = ($turn + 1) % 4; + } + $game->{turn} = $turn; } sub reset_turn { my ($game) = @_; - - $game->{turn} = ($game->{dealer} + 1) % 4; - # XXX pass again if loner! + $game->{turn} = $game->{dealer}; + next_turn($game); } 1; diff --git a/lib/Euchre/Game.pm b/lib/Euchre/Game.pm @@ -41,6 +41,8 @@ sub trick_winner { my $led = int($cards[0] / 6); my @values = @cards; for (my $i = 0; $i < @values; $i++) { + next if $cards[$i] < 0; # indicates loner + # Identify the card my $c = $cards[$i]; my $suit = int($c / 6); diff --git a/public/debug.html b/public/debug.html @@ -30,7 +30,7 @@ if (typeof msg.game.trump_nominee !== 'undefined') { gameState += 'Trump Nominee: ' + '<img class="card" src="cards/' + msg.game.trump_nominee + '.svg"><br>' - vote_trump_suit = msg.game.trump_nominee.substring(1,2); + order_suit = msg.game.trump_nominee.substring(1,2); } document.getElementById('hand').innerHTML = 'HAND: <br>' @@ -43,6 +43,9 @@ document.getElementById('table').innerHTML = '' for (var i = 0; i < msg.game.table.length; i++) { var c = msg.game.table[i] + if (c === null) { + c = '2B' // show back + } document.getElementById('table').innerHTML += '<img class="card" src="cards/' + c + '.svg">' } } else { @@ -72,11 +75,11 @@ function start_game() { ws.send(JSON.stringify({action:'start_game'})) } - function vote() { - ws.send(JSON.stringify({action:'vote_trump', vote: vote_trump_suit})) + function vote(a) { + ws.send(JSON.stringify({action:'order', vote: order_suit, loner: a})) } function pass() { - ws.send(JSON.stringify({action:'vote_trump', vote: 'pass'})) + ws.send(JSON.stringify({action:'order', vote: 'pass'})) } function play(card) { ws.send(JSON.stringify({action:'play_card', card: card})) @@ -107,7 +110,8 @@ <br> <br> <div id="hand">No cards in hand</div> <br> - <button onclick="vote()">Vote Trump</button> + <button onclick="vote(0)">Order</button> + <button onclick="vote(1)">Loner</button> <button onclick="pass()">Pass</button> <br><br> <div id="error" style="color:red"> </div>