Group
Extension

Poker-Robot/lib/Poker/Robot.pm

package Poker::Robot;
use Moo;
use Mojo::JSON qw(j);
use Mojo::Log;
use Mojo::UserAgent;
use Poker::Robot::Login;
use Poker::Robot::Ring;
use Poker::Robot::Chair;
use DBI;
use DBD::SQLite;
use EV;

=encoding utf8

=head1 NAME

Poker::Robot - base class for building L<Mojo Poker|https://github.com/nathanielgraham/Mojo-Poker> poker robots 

=head1 VERSION

Version 0.02

=cut

our $VERSION = '0.02';

=head1 SYNOPSIS

    package Poker::Robot::Mybot;
    use Moo;
    
    # Poker::Robot::Random shows a working example
    extends 'Poker::Robot::Random';

    # override default method 
    sub move {
      # move selection logic goes here
    }

    # and elsewhere in a script ...
    use Poker::Robot::Mybot;

    # Note: you must pick a unique username!
    $robot = Poker::Robot::Mybot->new(
      websocket => 'wss://localhost:443/websocket',
      username => 'Mybot',  
      ring_ids => [ 1 ], 
    );

    $robot->connect;

=head1 INTRODUCTION

Handlers are automatically executed at appropriate stages of the game, allowing your bot to run on autopilot.  By default, these handlers return legal but essentially random values. Your job is to override them in your subclass with something that makes more sense. Poker::Robot::Random shows a working example.

=head1 SERVERS

Download and install L<Mojo Poker|https://github.com/nathanielgraham/Mojo-Poker> to set up your own poker server. This is where you can deploy your bot once it is ready and have it compete against other bots and humans in real-time.

=head1 LOGGING

To see what your bot is doing, do a tail -f on robot.log

=head1 ATTRIBUTES

=head2 websocket

Websocket address of the test server.  Default is wss://localhost/websocket

=cut

has 'websocket' => (
  is      => 'rw',
  builder => '_build_websocket',
);

sub _build_websocket {
  return 'wss://localhost:443/websocket';
}

=head2 ring_ids

Required.  Ids of ring games to join. Before setting this attribute, bring up the test site on your browser to see which tables have open seats.

=cut

has 'ring_ids' => (
  is      => 'rw',
  isa     => sub { die "Not an array!" unless ref $_[0] eq 'ARRAY' },
  builder => '_build_ring_ids',
);

sub _build_ring_ids {
  return [];
}

has 'log' => (
  is  => 'rw',
  isa => sub { die "Not a Mojo::Log!" unless $_[0]->isa('Mojo::Log') },
  default =>
    sub { return Mojo::Log->new( path => 'robot.log' ) },
);

has 'login_id' => ( is => 'rw', );

has 'username' => (
  is       => 'rw',
  required => 1,
);

has 'user_id' => ( is => 'rw', );

has 'password' => ( is => 'rw', );

has 'bookmark' => ( is => 'rw', );

has 'login_list' => (
  is      => 'rw',
  isa     => sub { die "Not a hash.\n" unless ref( $_[0] ) eq 'HASH' },
  builder => '_build_login_list',
);

sub _build_login_list {
  return {};
}

sub fetch_login {
  my ( $self, $id ) = @_;
  return $self->login_list->{$id};
}

has 'table_list' => (
  is      => 'rw',
  isa     => sub { die "Not a hash.\n" unless ref( $_[0] ) eq 'HASH' },
  builder => '_build_table_list',
);

sub _build_table_list {
  return {};
}

sub fetch_ring {
  my ( $self, $id ) = @_;
  return $self->table_list->{$id};
}

sub response_handler {
  my ( $self, $aref ) = @_;

  if ( ref $aref ne 'ARRAY' ) {
    $self->log->info('invalid_format');
    return;
  }

  my ( $cmd, $opts ) = @$aref;

  if ( ref $cmd || !exists $self->client_update->{$cmd} ) {
    $self->log->info("invalid_client_update: $cmd");
    return;
  }

  $self->client_update->{$cmd}( $self, $opts );
}

has 'client_update' => (
  is      => 'rw',
  isa     => sub { die "Not a hash.\n" unless ref( $_[0] ) eq 'HASH' },
  builder => '_build_client_update',
);

sub _build_client_update {
  return {

    # SERVER CODES
    guest_login      => sub { shift->guest_login(shift) },
    login_snap       => sub { shift->login_snap(shift) },
    ring_snap        => sub { shift->ring_snap(shift) },
    tour_snap        => sub { shift->tour_snap(shift) },
    player_snap      => sub { shift->player_snap(shift) },
    table_snap       => sub { shift->table_snap(shift) },
    message_snap     => sub { },
    table_update     => sub { shift->table_update(shift) },
    player_update    => sub { shift->player_update(shift) },
    login_update     => sub { shift->login_update(shift) },
    new_game         => sub { shift->new_game(shift) },
    end_game         => sub { shift->end_game(shift) },
    deal_hole        => sub { shift->deal_hole(shift) },
    begin_new_round  => sub { shift->begin_new_round(shift) },
    begin_new_action => sub { shift->begin_new_action(shift) },
    deal_community => sub { shift->deal_community(shift) },
    showdown       => sub { shift->showdown(shift) },
    high_winner    => sub { shift->high_winner(shift) },
    low_winner     => sub { shift->low_winner(shift) },
    move_button    => sub { shift->move_button(shift) },
    forced_logout  => sub { shift->forced_logout(shift) },

    # NOTIFICATION CODES
    notify_login        => sub { shift->notify_login(shift) },
    notify_update_login => sub { shift->notify_update_login(shift) },
    notify_logout       => sub { shift->notify_logout(shift) },
    notify_create_ring  => sub { shift->notify_create_ring(shift) },
    notify_join_table   => sub { shift->notify_join_table(shift) },
    notify_unjoin_table => sub { shift->notify_unjoin_ring(shift) },
    notify_post         => sub { shift->notify_bet(shift) },
    notify_bet          => sub { shift->notify_bet(shift) },
    notify_check        => sub { shift->notify_check(shift) },
    notify_fold         => sub { shift->notify_fold(shift) },
    notify_discard      => sub { shift->notify_discard(shift) },
    notify_draw         => sub { shift->notify_draw(shift) },
    notify_credit_chips => sub { shift->notify_credit_chips(shift) },
    notify_table_chips  => sub { shift->notify_table_chips(shift) },
    notify_lobby_update => sub { },
    notify_message      => sub { },
    notify_pick_game    => sub { },
    notify_lr_update    => sub { },

    # RESPONSE CODES
    join_ring_res   => sub { shift->join_ring_res(shift) },
    unjoin_ring_res => sub { shift->unjoin_ring_res(shift) },
    watch_table_res => sub { shift->watch_table_res(shift) },
    unwatch_table_res => sub { shift->unwatch_table_res(shift) },
    login_res         => sub { shift->login_res(shift) },
    logout_res        => sub { shift->logout_res(shift) },
    register_res      => sub { shift->register_res(shift) },
    bet_res           => sub { shift->bet_res(shift) },
    check_res         => sub { shift->check_res(shift) },
    fold_res          => sub { shift->fold_res(shift) },
    discard_res       => sub { shift->discard_res(shift) },
    draw_res          => sub { shift->draw_res(shift) },
    credit_chips_res  => sub { shift->add_chips_res(shift) },
    pick_game_res     => sub { },
  };
}

=head1 HANDLERS

The following handlers can be overriden in your subclass with custom code for you robot.  At some point I'll get around to documenting this better, but this will have to do for now.  

=head2 SERVER CODES

    guest_login     
    login_snap   
    ring_snap   
    tour_snap        
    player_snap     
    table_snap       
    message_snap     
    table_update    
    player_update    
    login_update     
    new_game         
    end_game         
    deal_hole        
    begin_new_round  
    begin_new_action 
    deal_community 
    showdown       
    high_winner    
    low_winner     
    move_button    
    forced_logout  

=head2 NOTIFICATION CODES

    notify_login        
    notify_update_login 
    notify_logout       
    notify_create_ring  
    notify_join_table   
    notify_unjoin_table 
    notify_post         
    notify_bet          
    notify_check        
    notify_fold         
    notify_discard      
    notify_draw         
    notify_credit_chips 
    notify_table_chips  
    notify_lobby_update
    notify_message    
    notify_pick_game    
    notify_lr_update   

=head2 RESPONSE CODES

    join_ring_res   
    unjoin_ring_res 
    watch_table_res 
    unwatch_table_res
    login_res         
    logout_res        
    register_res      
    bet_res           
    check_res         
    fold_res          
    discard_res       
    draw_res         
    pick_game_res     
    reload_res 

=head2 REQUEST CODES

    join_ring
    unjoin_ring
    watch_table
    unwatch_table
    login
    logout
    register
    bet      
    check      
    fold          
    discard       
    draw        
    pick_game
    reload 

=cut

sub forced_logout {
  my ( $self, $opts ) = @_;
}

sub add_ring {
  my ( $self, $opts ) = @_;
  delete $self->table_list->{ $opts->{table_id} };
  my $ring = Poker::Robot::Ring->new($opts);
  $self->table_list->{ $opts->{table_id} } = $ring;
  if ( exists $self->ring_hash->{ $opts->{table_id} } ) {
    $self->respond( [ 'watch_table', { table_id => $opts->{table_id} } ] );
  }
}

sub add_login {
  my ( $self, $opts ) = @_;
  $self->login_list->{ $opts->{login_id} } = Poker::Robot::Login->new($opts);
}

# SERVER CODES

sub guest_login {
  my ( $self, $opts ) = @_;
  $self->login_id( $opts->{login_id} );
  if ( defined $self->bookmark ) {
    $self->respond( [ 'login_book', { bookmark => $self->bookmark } ] );
  }
  else {
    my $reg = [ 'register', { username => $self->username } ];
    $reg->[1]->{password} = $self->password if $self->password;
    $reg->[1]->{email}    = $self->email    if $self->email;
    $reg->[1]->{birthday} = $self->birthday if $self->birthday;
    $reg->[1]->{handle}   = $self->handle   if $self->handle;
    $self->respond($reg);
  }
}

sub login_snap {
  my ( $self, $opts ) = @_;
  $self->add_login($_) for (@$opts);
}

sub ring_snap {
  my ( $self, $opts ) = @_;
  $self->add_ring($_) for (@$opts);
}

sub tour_snap {
  my ( $self, $opts ) = @_;
}

sub table_update {
  my ( $self, $opts ) = @_;
  $self->table_snap($opts);
}

sub table_snap {
  my ( $self, $opts ) = @_;
  my $ring = $self->table_list->{ $opts->{table_id} };
  %$ring = ( %$ring, %$opts );
}

sub player_snap {
  my ( $self, $opts ) = @_;
  for my $r (@$opts) {
    $self->_join_table($r);
  }
}

sub player_update {
  my ( $self, $opts ) = @_;
  my $ring  = $self->table_list->{ $opts->{table_id} };
  my $chair = $ring->chairs->[ $opts->{chair} ];
  %$chair = ( %$chair, %$opts ) if $chair;
}

sub new_game {
  my ( $self, $opts ) = @_;
  my $ring = $self->table_list->{ $opts->{table_id} };
  $ring->reset;
  %$ring = ( %$ring, %$opts );
}

sub end_game {
  my ( $self, $opts ) = @_;
  my $ring = $self->table_list->{ $opts->{table_id} };
  $ring->game_over(1);
}

sub deal_hole {
  my ( $self, $opts ) = @_;
  my $ring = $self->table_list->{ $opts->{table_id} };
  $ring->chairs->[ $opts->{chair} ]->cards( $opts->{cards} );
}

sub begin_new_round {
  my ( $self, $opts ) = @_;
  my $ring = $self->table_list->{ $opts->{table_id} };
  for my $chair ( grep { defined } @{ $ring->chairs } ) {
    $chair->in_pot_this_round(0);
  }
  %$ring = ( %$ring, %$opts );
}

sub deal_community {
  my ( $self, $opts ) = @_;
  my $ring = $self->table_list->{ $opts->{table_id} };
  %$ring = ( %$ring, %$opts );
}

sub showdown {
  my ( $self, $opts ) = @_;
  my $ring = $self->table_list->{ $opts->{table_id} };
  %$ring = ( %$ring, %$opts );
}

sub high_winner {
  my ( $self, $opts ) = @_;
  my $ring = $self->table_list->{ $opts->{table_id} };
  %$ring = ( %$ring, %$opts );
}

sub low_winner {
  my ( $self, $opts ) = @_;
  my $ring = $self->table_list->{ $opts->{table_id} };
  %$ring = ( %$ring, %$opts );
}

sub move_button {
  my ( $self, $opts ) = @_;
  my $ring = $self->table_list->{ $opts->{table_id} };
  %$ring = ( %$ring, %$opts );
}

# NOTIFICATION CODES

sub notify_login {
  my ( $self, $opts ) = @_;
  $self->login_list->{ $opts->{login_id} } = Poker::Robot::Login->new($opts)
    unless $opts->{login_id} == $self->login_id;
}

sub notify_update_login {
  my ( $self, $opts ) = @_;
  my $login = $self->login_list->{ $opts->{login_id} };
  %$login = %$opts;
}

sub notify_logout {
  my ( $self, $opts ) = @_;
  delete $self->login_list->{ $opts->{login_id} };
}

sub notify_create_ring {
  my ( $self, $opts ) = @_;
  $self->add_ring($opts);
}

sub notify_join_table {
  my ( $self, $opts ) = @_;
  $self->_join_table($opts);
}

sub _join_table {
  my ( $self, $opts ) = @_;
  my $ring = $self->table_list->{ $opts->{table_id} };
  $ring->chairs->[ $opts->{chair} ] = Poker::Robot::Chair->new($opts);
}

sub notify_unjoin_ring {
  my ( $self, $opts ) = @_;
  my $ring = $self->table_list->{ $opts->{table_id} };
  $ring->chairs->[ $opts->{chair} ] = undef;
}

sub notify_fold {
  my ( $self, $opts ) = @_;
  my $ring  = $self->table_list->{ $opts->{table_id} };
  my $chair = $ring->chairs->[ $opts->{chair} ];
  $chair->is_in_hand(0);
  $chair->cards( [] );
}

sub notify_bet {
  my ( $self, $opts ) = @_;
  my $ring  = $self->table_list->{ $opts->{table_id} };
  my $chair = $ring->chairs->[ $opts->{chair} ];
  $chair->in_pot_this_round( $chair->in_pot_this_round + $opts->{chips} );
  $chair->in_pot( $chair->in_pot + $opts->{chips} );
}

sub notify_check {
  my ( $self, $opts ) = @_;
}

sub notify_discard {
  my ( $self, $opts ) = @_;
  my $ring  = $self->table_list->{ $opts->{table_id} };
  my $chair = $ring->chairs->[ $opts->{chair} ];
  unless ( $chair->login_id == $self->login_id ) {
    for my $id ( @{ $opts->{card_idx} } ) {
      splice( @{ $chair->cards }, $id, 1 );
    }
  }
}

sub notify_draw {
  my ( $self, $opts ) = @_;
}

sub notify_credit_chips {
  my ( $self, $opts ) = @_;
  my $login = $self->login_list->{ $opts->{login_id} };
  $login->chips->{ $opts->{director_id} } = $opts->{chips};
}

sub notify_table_chips {
  my ( $self, $opts ) = @_;
  my $ring = $self->table_list->{ $opts->{table_id} };
  $ring->chair->[ $opts->{chair} ]->chips( $opts->{chips} );
}

# RESPONSE CODES

sub join_ring_res {
  my ( $self, $opts ) = @_;
}

sub unjoin_ring_res {
  my ( $self, $opts ) = @_;
}

sub watch_table_res {
  my ( $self, $opts ) = @_;
  $self->table_snap($opts);
  my $login = $self->login_list->{ $self->login_id };
  my $chips = $login->chips->{ $opts->{director_id} };
  my $table = $self->table_list->{ $opts->{table_id} };
  return unless $chips && $table;
  $chips = $table->table_max if $table->table_max && $chips > $table->table_max;
  $self->respond(
    [
      'join_ring',
      { table_id => $opts->{table_id}, chips => $chips, auto_rebuy => $chips }
    ]
  );
}

sub unwatch_table_res {
  my ( $self, $opts ) = @_;
}

sub login_res {
  my ( $self, $opts ) = @_;
  if ( $opts->{success} ) {
    $self->login_id( $opts->{login_id} );
    my $login = $self->login_list->{ $opts->{login_id} };
    %$login = %$opts;
  }
}

sub login_update {
  my ( $self, $opts ) = @_;
  my $login = $self->login_list->{ $self->{login_id} };
  %$login = %$opts;
}

sub logout_res {
  my ( $self, $opts ) = @_;
  $self->tx->finish;
}

sub register_res {
  my ( $self, $opts ) = @_;
  if ( $opts->{success} ) {
    $self->login_id( $opts->{login_id} );
    my $login = $self->login_list->{ $opts->{login_id} };
    %$login = %$opts;
    $self->_replace_bot($opts);
  }
  else {
    $self->respond( ['logout'] );
  }
}

sub bet_res {
  my ( $self, $opts ) = @_;
}

sub check_res {
  my ( $self, $opts ) = @_;
}

sub fold_res {
  my ( $self, $opts ) = @_;
}

sub discard_res {
  my ( $self, $opts ) = @_;
}

sub draw_res {
  my ( $self, $opts ) = @_;
}

sub credit_chips_res {
  my ( $self, $opts ) = @_;
}

1;

has 'db' => ( is => 'rw', );

sub _build_db {
  my $self = shift;
  return DBI->connect( "dbi:SQLite:dbname=robots.db", "", "" );
}

has 'ring_hash' => (
  is      => 'rw',
  isa     => sub { die "Not a hash!" unless ref $_[0] eq 'HASH' },
);

sub _build_ring_hash {
  my $self = shift;
  return { map { $_ => 1 } @{ $self->ring_ids } } ;
}

has 'move_timer' => (
  is  => 'rw',
  isa => sub { die "Not a hash!" unless ref $_[0] eq 'HASH' },
  default => sub { {} },
);

has 'tx' => ( is => 'rw', );

has 'ua' => (
  is      => 'rw',
  builder => '_build_ua',
);

sub _build_ua {
  return Mojo::UserAgent->new( inactivity_timeout => 0 );
}

has 'valid_actions' => (
  is      => 'rw',
  builder => '_build_valid_actions',
);

sub _build_valid_actions {
  return {
    bet     => sub { shift->bet(shift) },
    check   => sub { shift->check(shift) },
    fold    => sub { shift->fold(shift) },
    draw    => sub { shift->draw(shift) },
    discard => sub { shift->discard(shift) },
    choice  => sub { shift->choice(shift) },
    bring   => sub { shift->bet(shift) },
  };
}

has 'email' => ( is => 'rw', );

has 'birthday' => ( is => 'rw', );

has 'handle' => ( is => 'rw', );

sub respond {
  my ( $self, $data ) = @_;

  my $json = j($data);
  $self->tx->send( $json );
  $self->log->info("robot: $json");
}

sub begin_new_action {
  my ( $self, $opts ) = @_;
  my $ring = $self->table_list->{ $opts->{table_id} };
  %$ring = ( %$ring, %$opts );
  my $table = $self->table_list->{ $opts->{table_id} };

  my $login_id = $table->chairs->[ $opts->{action} ]->login_id;
  $self->move($table) if $login_id == $self->login_id;
}

sub move {
  my ( $self, $table ) = @_;
}

sub size_bring {
  my ( $self, $table ) = @_;
  my @bets = ( $table->bring, $table->max_bet );
  $table->bet_size( $bets[ int( rand( scalar @bets ) ) ] );
}

sub bet {
  my ( $self, $table ) = @_;
  $self->respond(
    [ 'bet', { table_id => $table->table_id, chips => $table->bet_size } ] );
}

sub check {
  my ( $self, $table ) = @_;
  $self->respond( [ 'check', { table_id => $table->table_id } ] );
}

sub fold {
  my ( $self, $table ) = @_;
  $self->respond( [ 'fold', { table_id => $table->table_id } ] );
}

sub choice {
  my ( $self, $table ) = @_;
  $self->respond(
    [
      'pick_game', { table_id => $table->table_id, game => $table->game_choice }
    ]
  );
}

sub discard {
  my ( $self, $table ) = @_;
  $self->respond(
    [
      'discard',
      { table_id => $table->table_id, card_idx => $table->card_select }
    ]
  );
}

sub draw {
  my ( $self, $table ) = @_;
  $self->respond(
    [
      'draw', { table_id => $table->table_id, card_idx => $table->card_select }
    ]
  );
}

sub connect {
  my $self = shift;

  $self->ua->websocket(
    $self->websocket => sub {
      my ( $ua, $tx ) = @_;
      $self->log->error($tx->error->{message}) if $tx->error;

      # Check if WebSocket handshake was successful
      $self->log->error('WebSocket handshake failed!') and return unless $tx->is_websocket;
      $self->tx($tx);

      # Wait for WebSocket to be closed
      $tx->on(
        finish => sub {
          my ( $tx, $code ) = @_;
          $self->log->error( $tx->error->{message}) if $tx->error;
          $self->log->info("WebSocket closed with code $code.");
        }
      );

      $tx->on(
        json => sub {
          my ( $tx, $js ) = @_;
          if ($js) {
            $self->log->info('server: ' . j($js));
            $self->response_handler($js);
          }
        }
      );
      $tx->send('["guest_login"]');
    }
  );
  EV::run;
}

sub _select_bot {
  my $self = shift;
  my $sql  = 'SELECT * FROM bots WHERE username = ?';
  my $sth  = $self->db->prepare($sql);
  $sth->execute( $self->username );
  my $opts = $sth->fetchrow_hashref;
  if ( ref $opts eq 'HASH' ) {
    $self->bookmark( $opts->{bookmark} );
  }
}

sub _replace_bot {
  my ( $self, $opts ) = @_;
  my $sql = <<SQL;
REPLACE INTO bots (username, password, bookmark, modified)
VALUES (?,?,?,?)
SQL
  my $sth = $self->db->prepare($sql);
  $sth->execute( $opts->{username}, $opts->{password}, $opts->{bookmark},
    time );
}

sub _create_bots {
  my $self = shift;
  my $sql  = <<SQL;
CREATE TABLE bots (
  id INTEGER PRIMARY KEY NOT NULL,
  bookmark varchar(40) NOT NULL,
  username varchar(255) NOT NULL,
  password varchar(40),
  modified datetime
);
SQL

  $self->db->do($sql);
  $self->db->do('CREATE UNIQUE INDEX bots_idx1 ON bots (username)');
  $self->db->do('CREATE UNIQUE INDEX bots_idx2 ON bots (bookmark)');
}

sub BUILD {
  my $self = shift;
  $self->ring_hash( $self->_build_ring_hash );
  $self->db( $self->_build_db );
  eval { $self->db->prepare("SELECT 1 FROM bots") } or $self->_create_bots;
  $self->_select_bot; 
}

=head1 AUTHOR

Nathaniel Graham, C<ngraham@cpan.org> 

=head1 BUGS

Please report any bugs or feature requests directly to C<ngraham@cpan.org>

=head1 LICENSE AND COPYRIGHT

Copyright 2016 Nathaniel Graham.

This program is free software; you can redistribute it and/or modify it
under the terms of the MIT license.

=cut

1;  


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.