X-Git-Url: https://git.sesse.net/?p=remoteglot;a=blobdiff_plain;f=remoteglot.pl;h=9d611c0e6cfe2700cc0f8a5118f36b2e318b72fa;hp=9742a5a09a8686e93270290961ac4fa3704a2dae;hb=refs%2Fheads%2Fmaster;hpb=9dbe80127346dce1003c423a118321a3eb1a0b59 diff --git a/remoteglot.pl b/remoteglot.pl index 9742a5a..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; @@ -35,7 +33,6 @@ my $output_timer = undef; my $http_timer = undef; my $stop_pgn_fetch = 0; my $tb_retry_timer = undef; -my %tb_cache = (); my $tb_lookup_running = 0; my $last_written_json = undef; @@ -48,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"; @@ -96,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; @@ -152,7 +111,7 @@ sub handle_uci { return if ($engine->{'stopping'} && $line !~ /^bestmove/); $engine->{'stopping'} = 0; - if ($line =~ /^info/) { + if ($line =~ /^info/ && $line !~ / cluster /) { my (@infos) = split / /, $line; shift @infos; @@ -172,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) = @_; @@ -262,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); + }); } } @@ -277,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)) { @@ -339,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. @@ -369,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); }); } @@ -385,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; } @@ -451,19 +361,6 @@ sub handle_position { $engine->{'info'} = {}; $last_move = time; - - schedule_tb_lookup(); - - # - # 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 { @@ -690,46 +587,6 @@ sub output { # for new positions is off. undef $pos_calculating_started; - # - # If we have tablebase data from a previous lookup, replace the - # engine data with the data from the tablebase. - # - my $fen = $pos_calculating->fen(); - if (exists($tb_cache{$fen})) { - for my $key (qw(pv score_cp score_mate nodes nps depth seldepth tbhits splicepos)) { - delete $info->{$key . '1'}; - delete $info->{$key}; - } - $info->{'nodes'} = 0; - $info->{'nps'} = 0; - $info->{'depth'} = 0; - $info->{'seldepth'} = 0; - $info->{'tbhits'} = 0; - - my $t = $tb_cache{$fen}; - my $pv = $t->{'pv'}; - my $matelen = int((1 + $t->{'score'}) / 2); - if ($t->{'result'} eq '1/2-1/2') { - $info->{'score_cp'} = 0; - } elsif ($t->{'result'} eq '1-0') { - if ($pos_calculating->{'toplay'} eq 'B') { - $info->{'score_mate'} = -$matelen; - } else { - $info->{'score_mate'} = $matelen; - } - } else { - if ($pos_calculating->{'toplay'} eq 'B') { - $info->{'score_mate'} = $matelen; - } else { - $info->{'score_mate'} = -$matelen; - } - } - $info->{'pv'} = $pv; - $info->{'tablebase'} = 1; - } else { - $info->{'tablebase'} = 0; - } - # # Some programs _always_ report MultiPV, even with only one PV. # In this case, we simply use that data as if MultiPV was never @@ -814,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 @@ -934,7 +796,6 @@ sub output_json { $json->{'move_source_url'} = $remoteglotconf::move_source_url; } $json->{'score'} = score_digest($info, $pos_calculating, ''); - $json->{'using_lomonosov'} = defined($remoteglotconf::tb_serial_key); $json->{'nodes'} = $info->{'nodes'}; $json->{'nps'} = $info->{'nps'}; @@ -1072,7 +933,7 @@ sub output_json { } if (exists($pos_calculating->{'history'}) && - defined($remoteglotconf::json_history_dir)) { + defined($remoteglotconf::json_history_dir) && defined($json->{'engine'}{name})) { my $id = id_for_pos($pos_calculating); my $filename = $remoteglotconf::json_history_dir . "/" . $id . ".json"; @@ -1180,13 +1041,21 @@ sub score_digest { } if (exists($info->{'splicepos' . $mpv})) { my $sp = $info->{'splicepos' . $mpv}; - if ($mate < 0) { - return ['tb', -$sp]; + if ($mate > 0) { + return ['T', $sp]; } else { - return ['tb', $sp]; + return ['t', $sp]; } } else { - return ['m', $mate]; + if ($mate > 0) { + return ['M', $mate]; + } elsif ($mate < 0) { + return ['m', -$mate]; + } elsif ($pos->{'toplay'} eq 'B') { + return ['M', 0]; + } else { + return ['m', 0]; + } } } else { if (exists($info->{'score_cp' . $mpv})) { @@ -1216,9 +1085,9 @@ sub long_score { if (exists($info->{'splicepos' . $mpv})) { my $sp = $info->{'splicepos' . $mpv}; if ($mate > 0) { - return sprintf "White wins in %u/%u", int(($sp + 1) * 0.5), $sp; + return sprintf "White wins in %u", int(($sp + 1) * 0.5); } else { - return sprintf "Black wins in %u/%u", int(($sp + 1) * 0.5), $sp; + return sprintf "Black wins in %u", int(($sp + 1) * 0.5); } } else { if ($mate > 0) { @@ -1277,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) = @_; @@ -1338,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; @@ -1395,84 +1291,6 @@ sub find_clock_start { $dbh->commit; } -sub schedule_tb_lookup { - return if (!defined($remoteglotconf::tb_serial_key)); - my $pos = $pos_calculating; - return if (exists($tb_cache{$pos->fen()})); - - # If there's more than seven pieces, there's not going to be an answer, - # so don't bother. - return if ($pos->num_pieces() > 7); - - # Max one at a time. If it's still relevant when it returns, - # schedule_tb_lookup() will be called again. - return if ($tb_lookup_running); - - $tb_lookup_running = 1; - my $url = 'http://tb7-api.chessok.com:6904/tasks/addtask?auth.login=' . - $remoteglotconf::tb_serial_key . - '&auth.password=aquarium&type=0&fen=' . - URI::Escape::uri_escape($pos->fen()); - print TBLOG "Downloading $url...\n"; - AnyEvent::HTTP::http_get($url, sub { - handle_tb_lookup_return(@_, $pos, $pos->fen()); - }); -} - -sub handle_tb_lookup_return { - my ($body, $header, $pos, $fen) = @_; - print TBLOG "Response for [$fen]:\n"; - print TBLOG $header . "\n\n"; - print TBLOG $body . "\n\n"; - eval { - my $response = JSON::XS::decode_json($body); - if ($response->{'ErrorCode'} != 0) { - die "Unknown tablebase server error: " . $response->{'ErrorDesc'}; - } - my $state = $response->{'Response'}{'StateString'}; - if ($state eq 'COMPLETE') { - my $pgn = Chess::PGN::Parse->new(undef, $response->{'Response'}{'Moves'}); - if (!defined($pgn) || !$pgn->read_game()) { - warn "Error in parsing PGN\n"; - } else { - $pgn->quick_parse_game; - my $pvpos = $pos; - my $moves = $pgn->moves; - my @uci_moves = (); - for my $move (@$moves) { - my $uci_move; - ($pvpos, $uci_move) = $pvpos->make_pretty_move($move); - push @uci_moves, $uci_move; - } - $tb_cache{$fen} = { - result => $pgn->result, - pv => \@uci_moves, - score => $response->{'Response'}{'Score'}, - }; - output(); - } - } elsif ($state =~ /QUEUED/ || $state =~ /PROCESSING/) { - # Try again in a second. Note that if we have changed - # position in the meantime, we might query a completely - # different position! But that's fine. - } else { - die "Unknown response state " . $state; - } - - # Wait a second before we schedule another one. - $tb_retry_timer = AnyEvent->timer(after => 1.0, cb => sub { - $tb_lookup_running = 0; - schedule_tb_lookup(); - }); - }; - if ($@) { - warn "Error in tablebase lookup: $@"; - - # Don't try this one again, but don't block new lookups either. - $tb_lookup_running = 0; - } -} - sub open_engine { my ($cmdline, $tag, $cb) = @_; return undef if (!defined($cmdline));