package IGS::Protocol; use strict; my %buildin_events = ( 'auto_update' => [ sub { my $self = shift; my $upd = shift; if ($upd =~ m/^Match (\d+):/) { $self->feed_event ('game_begin', $1); } elsif ($upd =~ m/^Game (\d+):\s*(\S+)\s+vs\s+(\S+)\s*:\s*(.+?)\s*$/) { $self->feed_event ('game_over', $1, $2); } elsif ($upd =~ m/^(\S+) has disconnected/) { $self->feed_event ('player_disconnected', $1); } elsif ($upd =~ m/^(\S+) has connected/) { $self->feed_event ('player_connected', $1); } } ], 'req_games' => [sub { $_[0]->sendline ('games') }], 'req_players' => [sub { $_[0]->sendline ('who') }], 'req_moves' => [sub { $_[0]->sendline ("moves $_[1]") }], 'req_observe' => [sub { $_[0]->sendline ("observe $_[1]") }], 'req_unobserve' => [sub { $_[0]->sendline ("unobserve $_[1]") }], ); sub new { my $class = shift; bless { @_, events => { %buildin_events } }, $class; } sub init { my ($self, $user, $pass) = @_; $self->{login_name} = $user; $self->{login_pass} = $pass; $self->{login} = 1; $self->{free_pos} = []; } sub set_writer { my ($self, $cb) = @_; $self->{write} = $cb; } sub set_ev_cb { my ($self, $event, $cb) = @_; if (@{$self->{free_pos}}) { my $idx = pop @{$self->{free_pos}}; $self->{events}->{$event}->[$idx] = $cb; return $idx; } else { push @{$self->{events}->{$event}}, $cb; return ((scalar @{$self->{events}->{$event}}) - 1); } } sub unset_ev_cb { my ($self, $event, $idx) = @_; if ($self->{events}->{$event}->[$idx]) { print "REMOVED $idx\n" } $self->{events}->{$event}->[$idx] = undef; push @{$self->{free_pos}}, $idx; } sub feed_event { my ($self, $event, @args) = @_; return if not $self->{events}->{$event}; for (@{$self->{events}->{$event}}) { $_->($self, @args) if $_; } } sub send_queue { my $self = shift; my $str = pop @{$self->{cmd_queue}};; $str or return; $self->{write}->($str . "\015\012"); print "< $str\n"; } sub sendline { my ($self, $str) = @_; push @{$self->{cmd_queue}}, $str; } sub feed_data { my ($self, $buffer) = @_; my @lines; while ($$buffer =~ s/^([^\r\n]*)\r?\n//) { push @lines, $1; } $self->handle_line ($_) for @lines; if ($self->{login}) { if ($$buffer =~ s/^Login: //) { $self->sendline ($self->{login_name}); $self->{login} = 0; $self->send_queue (); } } } sub handle_line { my ($self, $line) = @_; if ($line =~ m/^1 1/) { $self->sendline ($self->{login_pass}); $self->send_queue (); } elsif ($line =~ m/^5 Invalid password/i) { print "wrong password"; exit (1); } elsif ($line =~ m/[89] File/) { if ($self->{status} eq 'file') { $self->{status} = undef; $self->feed_event ('recv_file', $self->{files}->{$self->{cur_file}}); } else { $self->{status} = 'file'; $self->{cur_file}++; } } elsif ($self->{status} eq 'file') { $self->{files}->{$self->{cur_file}} .= $line . "\n"; } elsif ($line =~ m/^21 \{([^}]+)\}/) { $self->feed_event ('auto_update', $1); } elsif ($line =~ m/^1 [58]/) { $self->send_queue (); } elsif ($line =~ m/^39 IGS entry on/ || $line =~ m/^IGS entry on/) { $self->sendline ("toggle client true"); $self->sendline ("toggle quiet false"); $self->feed_event ('logged_on'); #7 [##] white name [ rk ] black name [ rk ] (Move size H Komi BY FR) (###) #7 [126] goholic [ 1d*] vs. tjam [ 1d*] (270 19 0 0.5 13 I) ( 0) # "7 [%2d] %11s [%4s] vs. %11s [%4s] (%3d %4d %2d %4.1f %2d %s%s) (%3d)", } elsif ($line =~ m/^7 \[##\]/) { # ignore } elsif ($line =~ m/^7[ ] \[\s*(\d+)\s*\] \s+ (\S+) \s+ \[\s*(\S+)\s*\] \s+ vs\. \s+ (\S+) \s+ \[\s*(\S+)\s*\] \s+ \(\s* (\d+) \s+ (\d+) \s+ (\d+) \s+ (-?\d+\.?\d*) \s+ (\d+) [ ] (.)(.) \s*\) \s+\(\s*(\d+)\s*\)/x) { my $game = { number => $1, white => $2, white_rank => $3, black => $4, black_rank => $5, move => $6, size => $7, handicap => $8, komi => $9, byo => $10, Fflag => $11, Rflag => $12 }; $self->feed_event ('game_list', $game); # 27 Info Name Idle Rank | Info Name Idle Rank # 27 X -- -- ctian 2m 1k* | X 11 -- fastt 4m NR } elsif ($line =~ m/^27[ ]\s*([QRSX! ]+?) \s* ((?:--)|\d+) \s+ ((?:--)|\d+) \s+ (\S+) \s+ (\S+) \s+ (\S+)\s+ \| \s+([QRSX! ]+?) \s* ((?:--)|\d+) \s+ ((?:--)|\d+) \s+ (\S+) \s+ (\S+) \s+ (\S+)\s*/x) { my $player = { flags => $1, obs => $2, playing => $3, name => $4, idle => $5, rank => $6 }; my $player2 = { flags => $7, obs => $8, playing => $9, name => $10, idle => $11, rank => $12 }; $self->feed_event ('player_list', $player); $self->feed_event ('player_list', $player2); } elsif ($line =~ m/^27[ ]\s*([QRSX! ]+?) \s* ((?:--)|\d+) \s+ ((?:--)|\d+) \s+ (\S+) \s+ (\S+) \s+ (\S+)\s*/x) { my $player = { flags => $1, obs => $2, playing => $3, name => $4, idle => $5, rank => $6 }; $self->feed_event ('player_list', $player); } elsif ($line =~ m/^27[ ] [^|]* \| \s+([QRSX! ]+?) \s* ((?:--)|\d+) \s+ ((?:--)|\d+) \s+ (\S+) \s+ (\S+) \s+ (\S+)\s*/x) { my $player = { flags => $1, obs => $2, playing => $3, name => $4, idle => $5, rank => $6 }; $self->feed_event ('player_list', $player); } elsif ($line =~ m/^15\s+Game\s+(\d+)\s+\S:\s+ (\S+) \s+ \( \s*(-?\d+) \s+(-?\d+) \s+(-?\d+) \)\s+vs\s+ (\S+) \s+ \( \s*(-?\d+) \s+(-?\d+) \s+(-?\d+) \)/x) { print "GAMLI: $line\n"; $self->{games}->{$1} = { white => $2, white_time => $4, white_stones => $5, white_captures => $4, black => $5, black_time => $8, black_stones => $9, black_captures => $7 }; $self->{cur_game} = $1; } elsif ($line =~ m/^15 \s*(\d+)\((.)\): (\S+)/) { print "LIL: ($self->{cur_game}) $line\n"; my $move = { %{$self->{games}->{$self->{cur_game}}} }; if ($move) { $move->{number} = $1; $move->{color} = $2 eq 'W' ? 'white' : 'black'; $move->{coords} = $3; $self->feed_event ('game_move', $self->{cur_game}, $move); } } elsif ($line =~ m/^9 Game is titled:\s*(.+?)\s*$/) { $self->{games}->{$self->{cur_game}}->{title} = $1; $self->feed_event ('game_title', $self->{cur_game}, $1); } elsif ($line =~ m/^2 /) { # ignore } elsif ($line !~ m/^\s*$/) { print ">>>" . $line . "<\n"; } } 1;