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 ce56d4e4163d0b60e460bb0d42b2b4233ffb691a (patch)
parent 7a5ea00ceb65d1874d44758cdce2050f85bc9e7e
Author: Alex Karle <alex@karle.co>
Date:   Sat, 28 Mar 2020 14:45:07 -0400

Dealer: Add initial state tracking and player comms

This modifies the webserver to detect websocket clients on the '/play'
endpoint, and registers each with the Dealer, a new module that stores
the global state of the server.

It also adds the basic scaffolding for the client-server interaction
with the handle_msg dispatch sub.

As a first pass, we add the join_game action, where a client can join a
game, and this gets broadcasted to all others in the game.

~ckarle to test :)

Diffstat:
Mgloat.pl | 28++++++++++++++++++++++++++--
Alib/Euchre/Dealer.pm | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 190 insertions(+), 2 deletions(-)

diff --git a/gloat.pl b/gloat.pl @@ -4,11 +4,35 @@ # Those who Euchre Gloat never Win # ~ Andy Karle use Mojolicious::Lite; +use Mojo::JSON qw(decode_json); use FindBin; use lib "$FindBin::RealBin/lib"; -use Euchre::Game; +use Euchre::Dealer; -get '/' => { text => 'hi world' }; +get '/' => sub { + my $c = shift; + $c->reply->static('index.html'); +}; + +websocket '/play' => sub { + my $c = shift; + + my $id = ''.$c->tx; + app->log->debug("New player: $id"); + + # Register the player with the Dealer + register_player($c->tx); + + $c->on(message => sub { + my ($c, $msg) = @_; + handle_msg($id, decode_json($msg)); + }); + + $c->on(finish => sub { + app->log->debug("Player $id exiting"); + gloaters_never_win($id); + }); +}; app->start; diff --git a/lib/Euchre/Dealer.pm b/lib/Euchre/Dealer.pm @@ -0,0 +1,164 @@ +# Euchre::Dealer -- the Server API +use strict; +use warnings; + +package Euchre::Dealer; + +use List::Util; +use Mojo::JSON qw(encode_json); + +use Euchre::Card; +use Euchre::Game; + +require Exporter; +our @ISA = qw(Exporter); +our @EXPORT = qw( + handle_msg + register_player + gloaters_never_win +); + +# XXX: The first draft of this was written quickly and chose +# to use global hashes over objects. I think globals are eventually +# necessary, becuase the server needs it all in memory. It's just +# unfortunate... (or common wisdom tells me so) +# +# Nevertheless here's the rough idea: +# +# %GAMES -- each game is a hash with +# { +# players => [ p1, p2, p3, p4 ], # ws ids in %PLAYERS +# tricks => [ p1, p2, p3, p4 ], # ints per player +# dealer => 0-3, +# turn => 0-3, +# trump => 0-3, +# callers => 0-3, # player XXX +# table => [ c1, c2, c3, c4 ], # up to 4 cards +# score => [X, Y], +# } +# +# We decided the players would keep track of their own hands +# (after the initial deal), which simplifies state +# +# +# %PLAYERS -- all active players (across all games), keyed on client +# websocket id +# +# { +# game_id => key in %GAMES, +# name => username, +# seat => 0-3, +# ws => websocket obj +# } +# +# The players keyed on ws id is key (pun) because the closure in +# the webserver will always pass the proper client ws id to the +# handler, allowing us to identify who is playing cards and what +# game to attribute the action to. + +our %GAMES; +our %PLAYERS; + +# On ws connection, we add the player to %PLAYERS so that all +# future handle_msg's know how to send messages back to the +# player. No name or game_id known (yet). Player in lobby +sub register_player { + my ($tx) = @_; + my $id = ''.$tx; + $PLAYERS{$id} = { ws => $tx }; + print "Player $id has joined the server\n"; + if ($ENV{DEBUG}) { + use Data::Dumper; + print Dumper(\%PLAYERS); + print Dumper(\%GAMES); + } +} + +# finish handler to cleanup state +sub gloaters_never_win { + my ($id) = @_; + if (!exists $PLAYERS{$id}) { + warn "gloaters_never_win called on unknown player\n"; + } + # TODO: handle the game cleanup...? should we quit game? pause? + delete $PLAYERS{$id}; +} + +# Top level handler to dispatch into the appropriate message. +# Takes in the client ws id and the JSON msg and runs the +# appropriate handler, which is responsible for responding via ws +sub handle_msg { + my ($cid, $msg) = @_; + + my %dispatch = ( + join_game => \&join_game, + ); + + if (exists $dispatch{$msg->{action}}) { + $dispatch{$msg->{action}}->($cid, $msg); + } else { + die "Unknown API action: $msg->{action}"; + } +} + +# player_name +# game_id +sub join_game { + my ($cid, $msg) = @_; + + my $id = $msg->{game_id}; + + # init game if needed + if (!exists $GAMES{$id}) { + $GAMES{$id} = { + players => [], + turn => -1, + dealer => -1, + trump => -1, + tricks => [0, 0, 0, 0], + table => [], + callers => -1, + score => [0, 0], + }; + } + + # Handle full game case + my $numPlayers = scalar @{$GAMES{$id}->{players}}; + if ($numPlayers >= 4) { + send_error($cid, { msg => 'Already 4 players' }); + } + + # Add player to Game and cross-link in %PLAYERS for handle_msg + $PLAYERS{$cid}->{name} = $msg->{player_name}; + $PLAYERS{$cid}->{seat} = $numPlayers; # no +1, idx by 0; + push @{$GAMES{$id}->{players}}, $cid; + + # XXX: for fast prototyping we just broadcast gamestate + broadcast_gamestate($GAMES{$id}); +} + + +# XXX: The most simplest (bulkiest) message we can send is to just +# broadcast the gamestate to all clients. This will be our temporary +# debugging method / MVP. We can trim down the communication later +sub broadcast_gamestate { + my ($game) = @_; + + # Get all players in the game + my @all_ws = map { $PLAYERS{$_}->{ws} } @{$game->{players}}; + + my $json = encode_json({ msg_type => 'game_state', game => $game }); + for my $ws (@all_ws) { + $ws->send({ json => $json}); + } +} + + +sub send_error { + my ($cid, $msg) = @_; + my $ws = $PLAYERS{$cid}->{ws}; + $msg->{msg_type} = 'error'; + $ws->send({ json => encode_json($msg) }); +} + +1;