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;