X-Git-Url: https://git.sesse.net/?p=remoteglot;a=blobdiff_plain;f=remoteglot.pl;h=d8a7925c9137cfe548b0b9729a98f8daf3defac7;hp=a79239672116df2389bcdf68f0c9ba688fccdeee;hb=HEAD;hpb=f120f5172ba09d12928acc7228e4aa331cbdc190 diff --git a/remoteglot.pl b/remoteglot.pl index a792396..b5db356 100755 --- a/remoteglot.pl +++ b/remoteglot.pl @@ -1,9 +1,8 @@ #! /usr/bin/perl # -# remoteglot - Connects an abitrary UCI-speaking engine to ICS for easier post-game -# analysis, or for live analysis of relayed games. (Do not use for -# cheating! Cheating is bad for your karma, and your abuser flag.) +# remoteglot - Connects an abitrary UCI-speaking engine to a (live) PGN, +# for live analysis of relayed games. # # Copyright 2007 Steinar H. Gunderson # Licensed under the GNU General Public License, version 2. @@ -14,7 +13,6 @@ use AnyEvent::Handle; use AnyEvent::HTTP; use Chess::PGN::Parse; use EV; -use Net::Telnet; use File::Slurp; use IPC::Open2; use Time::HiRes; @@ -47,12 +45,6 @@ $dbh->{RaiseError} = 1; $| = 1; -open(FICSLOG, ">ficslog.txt") - or die "ficslog.txt: $!"; -print FICSLOG "Log starting.\n"; -select(FICSLOG); -$| = 1; - open(UCILOG, ">ucilog.txt") or die "ucilog.txt: $!"; print UCILOG "Log starting.\n"; @@ -95,46 +87,14 @@ if (defined($engine2)) { print "Chess engine ready.\n"; -# now talk to FICS -my ($t, $ev1); -if (defined($remoteglotconf::server)) { - $t = Net::Telnet->new(Timeout => 10, Prompt => '/fics% /'); - $t->input_log(\*FICSLOG); - $t->open($remoteglotconf::server); - $t->print($remoteglotconf::nick); - $t->waitfor('/Press return to enter the server/'); - $t->cmd(""); - - # set some options - $t->cmd("set shout 0"); - $t->cmd("set seek 0"); - $t->cmd("set style 12"); - - $ev1 = AnyEvent->io( - fh => fileno($t), - poll => 'r', - cb => sub { # what callback to execute - while (1) { - my $line = $t->getline(Timeout => 0, errmode => 'return'); - return if (!defined($line)); - - chomp $line; - $line =~ tr/\r//d; - handle_fics($line); - } - } - ); -} if (defined($remoteglotconf::target)) { if ($remoteglotconf::target =~ /^(?:\/|https?:)/) { - fetch_pgn($remoteglotconf::target); - } elsif (defined($t)) { - $t->cmd("observe $remoteglotconf::target"); + my $target = $remoteglotconf::target; + # Convenience. + $target =~ s#https://lichess.org/broadcast/.*/([^/]+)?$#https://lichess.org/api/stream/broadcast/round/$1.pgn#; + fetch_pgn($target); } } -if (defined($t)) { - print "FICS ready.\n"; -} # Engine events have already been set up by Engine.pm. EV::run; @@ -171,82 +131,6 @@ my $pos_for_movelist = undef; my @uci_movelist = (); my @pretty_movelist = (); -sub handle_fics { - my $line = shift; - if ($line =~ /^<12> /) { - handle_position(Position->new($line)); - $t->cmd("moves"); - } - if ($line =~ /^Movelist for game /) { - my $pos = $pos_calculating; - if (defined($pos)) { - @uci_movelist = (); - @pretty_movelist = (); - $pos_for_movelist = Position->start_pos($pos->{'player_w'}, $pos->{'player_b'}); - $getting_movelist = 1; - } - } - if ($getting_movelist && - $line =~ /^\s* \d+\. \s+ # move number - (\S+) \s+ \( [\d:.]+ \) \s* # first move, then time - (?: (\S+) \s+ \( [\d:.]+ \) )? # second move, then time - /x) { - eval { - my $uci_move; - ($pos_for_movelist, $uci_move) = $pos_for_movelist->make_pretty_move($1); - push @uci_movelist, $uci_move; - push @pretty_movelist, $1; - - if (defined($2)) { - ($pos_for_movelist, $uci_move) = $pos_for_movelist->make_pretty_move($2); - push @uci_movelist, $uci_move; - push @pretty_movelist, $2; - } - }; - if ($@) { - warn "Error when getting FICS move history: $@"; - $getting_movelist = 0; - } - } - if ($getting_movelist && - $line =~ /^\s+ \{.*\} \s+ (?: \* | 1\/2-1\/2 | 0-1 | 1-0 )/x) { - # End of movelist. - if (defined($pos_calculating)) { - if ($pos_calculating->fen() eq $pos_for_movelist->fen()) { - $pos_calculating->{'history'} = \@pretty_movelist; - } - } - $getting_movelist = 0; - } - if ($line =~ /^([A-Za-z]+)(?:\([A-Z]+\))* tells you: (.*)$/) { - my ($who, $msg) = ($1, $2); - - next if (grep { $_ eq $who } (@remoteglotconf::masters) == 0); - - if ($msg =~ /^fics (.*?)$/) { - $t->cmd("tell $who Executing '$1' on FICS."); - $t->cmd($1); - } elsif ($msg =~ /^uci (.*?)$/) { - $t->cmd("tell $who Sending '$1' to the engine."); - print { $engine->{'write'} } "$1\n"; - } elsif ($msg =~ /^pgn (.*?)$/) { - my $url = $1; - $t->cmd("tell $who Starting to poll '$url'."); - fetch_pgn($url); - } elsif ($msg =~ /^stoppgn$/) { - $t->cmd("tell $who Stopping poll."); - $stop_pgn_fetch = 1; - $http_timer = undef; - } elsif ($msg =~ /^quit$/) { - $t->cmd("tell $who Bye bye."); - exit; - } else { - $t->cmd("tell $who Couldn't understand '$msg', sorry."); - } - } - #print "FICS: [$line]\n"; -} - # Starts periodic fetching of PGNs from the given URL. sub fetch_pgn { my ($url) = @_; @@ -261,14 +145,19 @@ sub fetch_pgn { }; if ($@) { warn "$url: $@"; - $http_timer = AnyEvent->timer(after => 1.0, cb => sub { + $http_timer = AnyEvent->timer(after => $remoteglotconf::poll_frequency, cb => sub { fetch_pgn($url); }); } } else { - AnyEvent::HTTP::http_get($url, sub { - handle_pgn(@_, $url); - }); + my $buffer = ''; + AnyEvent::HTTP::http_get($url, + on_body => sub { + handle_partial_pgn(@_, \$buffer, $url); + }, + sub { + end_pgn(@_, \$buffer, $url); + }); } } @@ -276,14 +165,32 @@ my ($last_pgn_white, $last_pgn_black); my @last_pgn_uci_moves = (); my $pgn_hysteresis_counter = 0; -sub handle_pgn { - my ($body, $header, $url) = @_; +sub handle_partial_pgn { + my ($body, $header, $buffer, $url) = @_; if ($stop_pgn_fetch) { $stop_pgn_fetch = 0; $http_timer = undef; - return; + return 0; + } + $$buffer .= $body; + while ($$buffer =~ s/^\s*(.*)\n\n\n//s) { + handle_pgn($1, $url); } + return 1; +} + +sub end_pgn { + my ($body, $header, $buffer, $url) = @_; + handle_pgn($$buffer, $url); + $$buffer = ""; + $http_timer = AnyEvent->timer(after => $remoteglotconf::poll_frequency, cb => sub { + fetch_pgn($url); + }); +} + +sub handle_pgn { + my ($body, $url) = @_; my $pgn = Chess::PGN::Parse->new(undef, $body); if (!defined($pgn)) { @@ -338,9 +245,12 @@ sub handle_pgn { if ($pgn->result eq '1-0' || $pgn->result eq '1/2-1/2' || $pgn->result eq '0-1') { $pos->{'result'} = $pgn->result; } - $pos->{'history'} = \@repretty_moves; + my @extra_moves = (); + $pos = extend_from_manual_override($pos, \@repretty_moves, \@extra_moves); extract_clock($pgn, $pos); + $pos->{'history'} = \@repretty_moves; + $pos->{'extra_moves'} = \@extra_moves; # Sometimes, PGNs lose a move or two for a short while, # or people push out new ones non-atomically. @@ -368,7 +278,7 @@ sub handle_pgn { } } - $http_timer = AnyEvent->timer(after => 1.0, cb => sub { + $http_timer = AnyEvent->timer(after => $remoteglotconf::poll_frequency, cb => sub { fetch_pgn($url); }); } @@ -384,6 +294,7 @@ sub handle_position { for my $key ('white_clock', 'black_clock', 'white_clock_target', 'black_clock_target') { $pos_calculating->{$key} //= $pos->{$key}; } + $pos_calculating->{'extra_moves'} = $pos->{'extra_moves'}; return; } @@ -450,17 +361,6 @@ sub handle_position { $engine->{'info'} = {}; $last_move = time; - - # - # Output a command every move to note that we're - # still paying attention -- this is a good tradeoff, - # since if no move has happened in the last half - # hour, the analysis/relay has most likely stopped - # and we should stop hogging server resources. - # - if (defined($t)) { - $t->cmd("date"); - } } sub parse_infos { @@ -771,6 +671,11 @@ sub output_screen { } return unless (exists($pos_calculating->{'board'})); + + my $extra_moves = $pos_calculating->{'extra_moves'}; + if (defined($extra_moves) && scalar @$extra_moves > 0) { + $text .= " Manual move extensions: " . join(' ', @$extra_moves) . "\n"; + } if (exists($info->{'pv1'}) && exists($info->{'pv2'})) { # multi-PV @@ -1241,6 +1146,34 @@ sub plot_score { return undef; } +sub extend_from_manual_override { + my ($pos, $moves, $extra_moves) = @_; + + my $q = $dbh->prepare('SELECT next_move FROM game_extensions WHERE fen=? AND history=? AND player_w=? AND player_b=? AND (CURRENT_TIMESTAMP - ts) < INTERVAL \'1 hour\''); + while (1) { + my $player_w = $pos->{'player_w'}; + my $player_b = $pos->{'player_b'}; + if ($player_w =~ /^base64:(.*)$/) { + $player_w = MIME::Base64::decode_base64($1); + } + if ($player_b =~ /^base64:(.*)$/) { + $player_b = MIME::Base64::decode_base64($1); + } + #use Data::Dumper; print Dumper([$pos->fen(), JSON::XS::encode_json($moves), $player_w, $player_b]); + $q->execute($pos->fen(), JSON::XS::encode_json($moves), $player_w, $player_b); + my $ref = $q->fetchrow_hashref; + if (defined($ref)) { + my $move = $ref->{'next_move'}; + ($pos) = $pos->make_pretty_move($move); + push @$moves, $move; + push @$extra_moves, $move; + } else { + last; + } + } + return $pos; +} + sub extract_clock { my ($pgn, $pos) = @_; @@ -1302,7 +1235,6 @@ sub find_clock_start { return; } - # TODO(sesse): Maybe we can get the number of moves somehow else for FICS games. # The history is needed for id_for_pos. if (!exists($pos->{'history'})) { return;