use IPC::Open2;
use Time::HiRes;
use JSON::XS;
+use URI::Escape;
require 'Position.pm';
require 'Engine.pm';
require 'config.pm';
$SIG{ALRM} = sub { output(); };
my $latest_update = undef;
my $http_timer = undef;
+my $tb_retry_timer = undef;
+my %tb_cache = ();
+my $tb_lookup_running = 0;
$| = 1;
print UCILOG "Log starting.\n";
select(UCILOG);
$| = 1;
+
+open(TBLOG, ">tblog.txt")
+ or die "tblog.txt: $!";
+print TBLOG "Log starting.\n";
+select(TBLOG);
+$| = 1;
+
select(STDOUT);
# open the chess engine
sub handle_uci {
my ($engine, $line, $primary) = @_;
+ return if $line =~ /(upper|lower)bound/;
+
$line =~ s/ / /g; # Sometimes needed for Zappa Mexico
print UCILOG localtime() . " $engine->{'tag'} <= $line\n";
if ($line =~ /^info/) {
$pgn->quick_parse_game;
my $pos = Position->start_pos($pgn->white, $pgn->black);
my $moves = $pgn->moves;
+ my @uci_moves = ();
for my $move (@$moves) {
my ($from_row, $from_col, $to_row, $to_col, $promo) = $pos->parse_pretty_move($move);
+ push @uci_moves, Board::move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo);
$pos = $pos->make_move($from_row, $from_col, $to_row, $to_col, $promo);
}
+ $pos->{'history'} = \@uci_moves;
+ $pos->{'pretty_history'} = $moves;
handle_position($pos);
}
$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,
delete $info->{'score_cp' . $mpv};
delete $info->{'score_mate' . $mpv};
- while ($x[0] eq 'cp' || $x[0] eq 'mate' || $x[0] eq 'lowerbound' || $x[0] eq 'upperbound') {
+ while ($x[0] eq 'cp' || $x[0] eq 'mate') {
if ($x[0] eq 'cp') {
shift @x;
$info->{'score_cp' . $mpv} = shift @x;
}
my $info = $engine->{'info'};
+
+ #
+ # 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)) {
+ 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 + scalar @$pv) / 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.
$json->{'depth'} = $info->{'depth'};
$json->{'tbhits'} = $info->{'tbhits'};
$json->{'seldepth'} = $info->{'seldepth'};
+ $json->{'tablebase'} = $info->{'tablebase'};
# single-PV only for now
$json->{'pv_uci'} = $info->{'pv'};
}
$json->{'refutation_lines'} = \%refutation_lines;
- open my $fh, ">/srv/analysis.sesse.net/www/analysis.json.tmp"
+ open my $fh, ">", $remoteglotconf::json_output . ".tmp"
or return;
print $fh JSON::XS::encode_json($json);
close $fh;
- rename("/srv/analysis.sesse.net/www/analysis.json.tmp", "/srv/analysis.sesse.net/www/analysis.json");
+ rename($remoteglotconf::json_output . ".tmp", $remoteglotconf::json_output);
}
sub uciprint {
my ($info, $pos, $mpv, $invert) = @_;
if (defined($info->{'score_mate' . $mpv})) {
- if ($invert) {
- return 99999 - $info->{'score_mate' . $mpv};
+ my $mate = $info->{'score_mate' . $mpv};
+ my $score;
+ if ($mate > 0) {
+ # Side to move mates
+ $mate = 99999 - $mate;
} else {
- return -(99999 - $info->{'score_mate' . $mpv});
+ # Side to move is getting mated (note the double negative for $mate)
+ $mate = -99999 - $mate;
}
+ if ($invert) {
+ $score = -$score;
+ }
+ return $score;
} else {
if (exists($info->{'score_cp' . $mpv})) {
my $score = $info->{'score_cp' . $mpv};
if (exists($info->{'score_cp' . $mpv})) {
my $score = $info->{'score_cp' . $mpv} * 0.01;
if ($score == 0) {
- return "Score: 0.00";
+ if ($info->{'tablebase'}) {
+ return "Theoretical draw";
+ } else {
+ return "Score: 0.00";
+ }
}
if ($pos->{'toplay'} eq 'B') {
$score = -$score;
return $text;
}
+sub schedule_tb_lookup {
+ return if (!defined($remoteglotconf::tb_serial_key));
+ my $pos = $pos_waiting // $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://158.250.18.203: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 ($from_row, $from_col, $to_row, $to_col, $promo) = $pvpos->parse_pretty_move($move);
+ push @uci_moves, Board::move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo);
+ $pvpos = $pvpos->make_move($from_row, $from_col, $to_row, $to_col, $promo);
+ }
+ $tb_cache{$fen} = {
+ result => $pgn->result,
+ pv => \@uci_moves
+ };
+ 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 " . $response->{'Response'}{'StateString'};
+ }
+
+ # 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));