euchre-live

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

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;