euchre-live

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

Dealer.pm (6864B) [raw]


      1 # Euchre::Dealer -- enforcer of turns, speaker of state
      2 #  * Communicates Game state to Players at table
      3 #  * Ensures everyone is following the rules
      4 use strict;
      5 use warnings;
      6 
      7 package Euchre::Dealer;
      8 
      9 use Euchre::Errors;
     10 use Euchre::Game;
     11 
     12 use Class::Tiny qw(id), {
     13     password => '',
     14     game     => sub { Euchre::Game->new() },
     15     players  => sub { {} },
     16     settings => sub { {} },
     17     start_time => sub { time },
     18 };
     19 
     20 sub BUILD {
     21     my ($self) = @_;
     22     # de lazy the attributes
     23     $self->$_ for qw(start_time);
     24 }
     25 
     26 sub add_player {
     27     my ($self, $p, $password) = @_;
     28 
     29     $password //= '';
     30     if ($self->password && $password ne $self->password) {
     31         return BAD_PASS;
     32     }
     33 
     34     if (grep { $_->name eq $p->{name} } values %{$self->players}) {
     35         return UNIQUE_USER;
     36     }
     37     $self->players->{$p->{id}} = $p;
     38     $self->broadcast_gamestate();
     39 
     40     return SUCCESS;
     41 }
     42 
     43 sub remove_player {
     44     my ($self, $p) = @_;
     45 
     46     $p->stand_up();
     47     delete $self->players->{$p->{id}};
     48     $self->broadcast_gamestate();
     49 
     50     return SUCCESS;
     51 }
     52 
     53 # Top level handler to dispatch into the appropriate message.
     54 # Called by the Host for table specific routines
     55 # Takes in the player id and the JSON msg and runs the
     56 # appropriate handler, which is responsible for responding via ws
     57 sub handle_msg {
     58     my ($self, $cid, $msg) = @_;
     59 
     60     # Crazy magic dispatch of
     61     #
     62     #   action => [ handler, req-keys, req-phase, phase-err, needs-turn ]
     63     #
     64     # The last three are optional, but are useful to dedupe common
     65     # assertions (like, needs to be their turn)
     66     my %dispatch = (
     67         # Game management endpoints
     68         chat         => [\&chat, ['msg']],
     69         take_seat    => [\&take_seat, ['seat']],
     70         stand_up     => [\&stand_up, []],
     71         start_game   => [\&start_game, [], 'lobby', START_GAME],
     72         restart_game => [\&restart_game, [], 'end', RESTART_GAME],
     73 
     74         # Gameplay
     75         order        => [\&order, ['vote'], 'vote', ORDER, 1],
     76         dealer_swap  => [\&dealer_swap, ['card'], 'dealer_swap', DEALER_SWAP, 1],
     77         play_card    => [\&play_card, ['card'], 'play', PLAY_CARD, 1],
     78     );
     79 
     80 
     81     if (!exists $dispatch{$msg->{action}}) {
     82         warn "Unknown dealer API action: $msg->{action}";
     83         return;
     84     }
     85 
     86     my $p = $self->players->{$cid};
     87     my ($handler, $req_keys, $req_phase, $phase_err, $turn_based) =
     88         @{$dispatch{$msg->{action}}};
     89 
     90     # Validate that all required msg keys are present
     91     for my $k (@$req_keys) {
     92         if (!exists $msg->{$k}) {
     93             $p->error(MISSING_PARAM);
     94             return;
     95         }
     96     }
     97 
     98     # Next validate phase, turn, and handle success/failure
     99     if ($req_phase && ($self->game->phase ne $req_phase)) {
    100         $p->error($phase_err);
    101     } elsif ($turn_based && ($p->seat != $self->game->turn)) {
    102         $p->error(TURN);
    103     } else {
    104         if (my $errno = $handler->($self, $p, $msg)) {
    105             $p->error($errno);
    106         } else {
    107             # All successful Dealer handlers broadcast to all players on success
    108             # (except for chat...)
    109             if ($msg->{action} ne 'chat') {
    110                 $self->broadcast_gamestate();
    111             }
    112         }
    113     }
    114 }
    115 
    116 # seat
    117 sub take_seat {
    118     my ($self, $p, $msg) = @_;
    119 
    120     my $seat = $msg->{seat};
    121 
    122     if ($seat > 3 || $seat < 0) {
    123         return INVALID_SEAT;
    124     }
    125 
    126     if (grep { $_->seat == $seat } values %{$self->players}) {
    127         return TAKEN_SEAT;
    128     } else {
    129         # Move from standing (or sitting) to sitting
    130         if (!$p->is_spectator()) {
    131             $p->stand_up();
    132         }
    133         $p->seat($seat);
    134     }
    135 
    136     return SUCCESS;
    137 }
    138 
    139 sub stand_up {
    140     my ($self, $p) = @_;
    141     return $p->stand_up();
    142 }
    143 
    144 # start_seat: -1 - 3
    145 sub start_game {
    146     my ($self, $p, $msg) = @_;
    147 
    148     if ($self->num_players() < 4) {
    149         return PARTIAL_GAME;
    150     }
    151 
    152     if ($msg->{start_seat} > 4) {
    153         return INVALID_SEAT;
    154     }
    155 
    156     if (!defined $msg->{start_seat} || $msg->{start_seat} < 0) {
    157         $msg->{start_seat} = int(rand(4));
    158     }
    159 
    160     return $self->game->start_game($msg->{start_seat});
    161 }
    162 
    163 sub restart_game {
    164     my ($self) = @_;
    165     $self->game(Euchre::Game->new());
    166     return SUCCESS;
    167 }
    168 
    169 sub num_players {
    170     my ($self) = @_;
    171     return scalar grep { !$_->is_spectator } values %{$self->players};
    172 }
    173 
    174 # msg.vote  = 'suit' or 'pass'
    175 # msg.loner = 0 or 1
    176 sub order {
    177     my ($self, $p, $msg) = @_;
    178     return $self->game->order($msg->{vote}, $msg->{loner});
    179 }
    180 
    181 # msg.card => 'AH'
    182 sub play_card {
    183     my ($self, $p, $msg) = @_;
    184     my ($errno, $do_update) = $self->game->play_card($msg->{card});
    185 
    186     if ($errno) {
    187         return $errno;
    188     } else {
    189         if ($do_update) {
    190             Mojo::IOLoop->timer(2 => sub { $self->broadcast_gamestate() });
    191         }
    192         return SUCCESS;
    193     }
    194 }
    195 
    196 # Based on validation, we KNOW $p is the dealer
    197 # msg.card = card to swap
    198 sub dealer_swap {
    199     my ($self, $p, $msg) = @_;
    200     return $self->game->dealer_swap($msg->{card});
    201 }
    202 
    203 sub broadcast_gamestate {
    204     my ($self) = @_;
    205 
    206     # Translate to human readable names for clients
    207     my @snames = map { $_->name } @{$self->spectators};
    208 
    209     my $msg = {
    210         %{$self->game},
    211         players => $self->player_names,
    212         spectators => \@snames,
    213         hand_lengths => $self->game->hand_lengths,
    214     };
    215     delete $msg->{hands};
    216 
    217     for my $p (values %{$self->players}) {
    218 
    219         my $json = {
    220             msg_type => 'game_state',
    221             game => $msg,
    222             is_spectator => $p->is_spectator ? 1 : 0,
    223             table_id => $self->id,
    224             settings => $self->settings,
    225         };
    226 
    227         if (!$p->is_spectator) {
    228             $json->{hand} = $self->game->hands->[$p->seat];
    229         } else {
    230             $json->{hand} = [];
    231         }
    232         $p->send($json);
    233     }
    234 }
    235 
    236 # Simple stateless broadcast to all clients in game
    237 sub chat {
    238     my ($self, $p, $msg) = @_;
    239 
    240     my $json = {
    241         msg_type => 'chat',
    242         msg      => $p->name . ": $msg->{msg}"
    243     };
    244     $self->broadcast($json);
    245 }
    246 
    247 # Send a message to all players at the table
    248 sub broadcast {
    249     my ($self, $json) = @_;
    250     for my $p (values %{$self->players}) {
    251         $p->send($json);
    252     }
    253 }
    254 
    255 # TODO: When we decide to broadcast Euchre tournies of millions,
    256 # rewrite this to be an array to minimize number of times we
    257 # iterate through it on broadcast_gamestate ... until then? prosper
    258 sub spectators {
    259     my ($self) = @_;
    260     my @specs = grep { $_->is_spectator } values %{$self->players};
    261     return \@specs;
    262 }
    263 
    264 sub player_names {
    265     my ($self) = @_;
    266     my $seated = ['Empty', 'Empty', 'Empty', 'Empty'];
    267     for my $p (grep { !$_->is_spectator } values %{$self->players}) {
    268         $seated->[$p->seat] = $p->name;
    269     }
    270     return $seated;
    271 }
    272 
    273 # Used to decide when to cleanup
    274 sub is_active {
    275     my ($self) = @_;
    276     my $n = scalar keys %{$self->players};
    277     return $n != 0;
    278 }
    279 
    280 1;