Host.pm (7344B) [raw]
1 # Euchre::Host -- responsible for all things in the Lobby 2 # (join/create/leave table actions, registering players, etc) 3 use strict; 4 use warnings; 5 6 package Euchre::Host; 7 8 if (!defined $::LOG) { 9 use Mojo::Log; 10 $::LOG = Mojo::Log->new(); 11 } 12 13 use Euchre::Errors; 14 use Euchre::Dealer; 15 use Euchre::Player; 16 17 require Exporter; 18 our @ISA = qw(Exporter); 19 our @EXPORT = qw( 20 handle_msg 21 register_player 22 gloaters_never_win 23 prune_tables 24 prune_players 25 list_tables 26 stats 27 ); 28 29 # Global State 30 our %PLAYERS; 31 our %DEALERS; 32 our %PINDEX; # Player id => Dealer id 33 34 # Stats 35 our $TOTAL_PLAYERS = 0; 36 our $TOTAL_TABLES = 0; 37 our $START_TIME = localtime(time); 38 39 # On ws connection, we add the player to %PLAYERS so that all future 40 # handle_msg's know how to send messages back to the player. 41 sub register_player { 42 my ($tx) = @_; 43 my $id = ''.$tx; 44 $PLAYERS{$id} = Euchre::Player->new( 45 id => $id, 46 ws => $tx, 47 ); 48 $TOTAL_PLAYERS++; 49 } 50 51 # finish handler to cleanup state 52 sub gloaters_never_win { 53 my ($id) = @_; 54 if (!exists $PLAYERS{$id}) { 55 warn "gloaters_never_win called on unknown player\n"; 56 return; 57 } 58 59 # TODO: cleanup stale tables 60 my $p = $PLAYERS{$id}; 61 try_leave_table($p); 62 63 $::LOG->info("Player " . $p->name . " went inactive"); 64 delete $PLAYERS{$id}; 65 } 66 67 # Top level handler to dispatch into the appropriate message. 68 # Takes in the client ws id and the JSON msg and runs the 69 # appropriate handler, which is responsible for responding via ws 70 # 71 # If action not part of dispatch table, assume it is a Dealer action. 72 # ID the table of the Dealer of the client and dispatch to them 73 sub handle_msg { 74 my ($cid, $msg) = @_; 75 76 my %dispatch = ( 77 ping => \&pong, 78 join_table => \&join_table, 79 leave_table => \&leave_table, 80 ); 81 82 my $p = $PLAYERS{$cid}; 83 if (!defined $p) { 84 # Unknown client -- warn and return 85 warn localtime(time) . " Unknown client contacting server\n"; 86 return; 87 } 88 89 if (exists $dispatch{$msg->{action}}) { 90 $dispatch{$msg->{action}}->($p, $msg); 91 } else { 92 require_table($p) or return; 93 my $d = $DEALERS{$PINDEX{$p->id}}; 94 $d->handle_msg($cid, $msg); 95 } 96 } 97 98 99 # Checks that client at a table, sends error if not 100 # Returns 1 if at table, else 0 101 sub require_table { 102 my ($p) = @_; 103 104 my $at_table = exists $PINDEX{$p->id}; 105 if (!$at_table) { 106 $p->error(NOT_AT_TABLE); 107 } 108 109 return $at_table; 110 } 111 112 sub pong { 113 my ($p) = @_; 114 $p->send({ msg_type => 'pong' }); 115 } 116 117 118 # player_name 119 # table 120 # password (opt) 121 # settings (hash-ref, opt) 122 sub join_table { 123 my ($p, $msg) = @_; 124 125 require_keys(@_, qw(table player_name)) or return; 126 127 # If currently at a table, leave it (safe to call if not at a table) This 128 # is important because some clients may not have implemented the 129 # leave_table in all cases, and we need to prevent one Player from having 130 # multiple Tables for a whole lot of reasons (messages from multiple 131 # tables, failure to detect and cleanup state, etc) 132 if (exists $PINDEX{$p->id}) { 133 leave_table($p); 134 } 135 136 my $tid = $msg->{table}; 137 138 $p->name($msg->{player_name}); 139 140 # init table if needed 141 if (!exists $DEALERS{$tid}) { 142 $DEALERS{$tid} = Euchre::Dealer->new( 143 id => $tid, 144 (exists $msg->{settings} ? (settings => $msg->{settings}) : ()), 145 (exists $msg->{password} ? (password => $msg->{password}) : ()), 146 ); 147 $TOTAL_TABLES++; 148 $::LOG->info("Player " . $p->name . " created table $tid"); 149 } 150 151 my $d = $DEALERS{$tid}; 152 if (my $errno = $d->add_player($p, $msg->{password})) { 153 $p->error($errno); 154 } else { 155 $::LOG->info("Player " . $p->name . " joined table $tid"); 156 $PINDEX{$p->id} = $tid; 157 } 158 } 159 160 sub leave_table { 161 my ($p) = @_; 162 163 require_table($p) or return; 164 165 # Let the dealer do its own cleanup, then cleanup our state 166 my $d = $DEALERS{$PINDEX{$p->id}}; 167 if (my $errno = $d->remove_player($p)) { 168 $p->error($errno); 169 } else { 170 # Success! Was removed properly, delete PINDEX 171 # Also delete the Dealer itself if that was the last player 172 # to leave and the game looks finished 173 $::LOG->info("Player " . $p->name . " left table " . $d->id); 174 if (!$d->is_active && $d->game->phase eq 'end') { 175 $::LOG->info("Deleting Table " . $d->id . " that appears to have finished"); 176 delete $DEALERS{$PINDEX{$p->id}}; 177 } 178 delete $PINDEX{$p->id}; 179 } 180 } 181 182 sub list_tables { 183 my @tables; 184 for my $k (keys %DEALERS) { 185 my $d = $DEALERS{$k}; 186 push @tables, { 187 name => $k, 188 phase => $d->game->phase, 189 has_password => $d->password ? 1 : 0, 190 players => $d->player_names, 191 spectators => [map { $_->name } @{$d->spectators}], 192 settings => $d->settings, 193 }; 194 } 195 return \@tables; 196 } 197 198 199 # Global server stats for games in progress 200 # Poor man's monitoring :) 201 sub stats { 202 my $num_tables = scalar keys %DEALERS; 203 my $num_players = scalar keys %PLAYERS; 204 205 my $msg = ""; 206 $msg .= "Tables: Start Time\tTable tname\tPlayer name\n"; 207 $msg .= "===========================================================\n"; 208 for my $d (values %DEALERS) { 209 $msg .= localtime($d->start_time) . "\t" . $d->id . "\n"; 210 for my $p (values %{$d->players}) { 211 $msg .= localtime($p->start_time) . "\t\t" . $p->name . "\n"; 212 } 213 } 214 $msg .= "-----------------------------------------------------------\n"; 215 $msg .= "$num_tables\tTables\n"; 216 $msg .= "\n\nPlayers: Start Time\tTable tname\n"; 217 $msg .= "===========================================================\n"; 218 for my $p (values %PLAYERS) { 219 $msg .= localtime($p->start_time) . "\t" . $p->name . "\n"; 220 } 221 $msg .= "-----------------------------------------------------------\n"; 222 $msg .= "$num_players\tPlayers\n"; 223 224 $msg .= "\n\nServer Stats\n"; 225 $msg .= "===========================================================\n"; 226 $msg .= "Server Start: $START_TIME\n"; 227 $msg .= "Lifetime Tables: $TOTAL_TABLES\n"; 228 $msg .= "Lifetime Players: $TOTAL_PLAYERS\n"; 229 $msg .= "-----------------------------------------------------------\n"; 230 $msg .= "\n\nUptime: " . `uptime`; 231 232 return $msg; 233 } 234 235 sub require_keys { 236 my ($p, $msg, @req_keys) = @_; 237 for my $k (@req_keys) { 238 if (!exists $msg->{$k}) { 239 $p->error(MISSING_PARAM); 240 return 0; 241 } 242 } 243 return 1; 244 } 245 246 # Prune empty tables, to be called in a IOLoop 247 sub prune_tables { 248 for my $k (keys %DEALERS) { 249 if (!$DEALERS{$k}->is_active) { 250 $::LOG->info("Pruning inactive table " . $DEALERS{$k}->id); 251 delete $DEALERS{$k}; 252 } 253 } 254 } 255 256 # Prune empty players, to be called in a IOLoop 257 sub prune_players { 258 for my $p (keys %PLAYERS) { 259 if (!$PLAYERS{$p}->is_active) { 260 $::LOG->info("Pruning inactive player " . $PLAYERS{$p}->name); 261 try_leave_table($PLAYERS{$p}); 262 delete $PLAYERS{$p}; 263 } 264 } 265 } 266 267 sub try_leave_table { 268 my ($p) = @_; 269 if (exists $PINDEX{$p->id}) { 270 leave_table($p); 271 } 272 } 273 274 1;