no warnings qw(once);
# Program starts here
-$SIG{ALRM} = sub { output(); };
my $latest_update = undef;
+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;
output();
}
+my $getting_movelist = 0;
+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_waiting // $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.
+ for my $pos ($pos_waiting, $pos_calculating) {
+ next if (!defined($pos));
+ if ($pos->fen() eq $pos_for_movelist->fen()) {
+ $pos->{'pretty_history'} = \@pretty_movelist;
+ }
+ }
+ $getting_movelist = 0;
}
if ($line =~ /^([A-Za-z]+)(?:\([A-Z]+\))* tells you: (.*)$/) {
my ($who, $msg) = ($1, $2);
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.");
sub handle_pgn {
my ($body, $header, $url) = @_;
+
+ if ($stop_pgn_fetch) {
+ $stop_pgn_fetch = 0;
+ $http_timer = undef;
+ return;
+ }
+
my $pgn = Chess::PGN::Parse->new(undef, $body);
if (!defined($pgn) || !$pgn->read_game()) {
warn "Error in parsing PGN from $url\n";
} else {
- $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;
-
- # Sometimes, PGNs lose a move or two for a short while,
- # or people push out new ones non-atomically.
- # Thus, if we PGN doesn't change names but becomes
- # shorter, we mistrust it for a few seconds.
- my $trust_pgn = 1;
- if (defined($last_pgn_white) && defined($last_pgn_black) &&
- $last_pgn_white eq $pgn->white &&
- $last_pgn_black eq $pgn->black &&
- scalar(@uci_moves) < scalar(@last_pgn_uci_moves)) {
- if (++$pgn_hysteresis_counter < 3) {
- $trust_pgn = 0;
+ eval {
+ $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 $uci_move;
+ ($pos, $uci_move) = $pos->make_pretty_move($move);
+ push @uci_moves, $uci_move;
}
- }
- if ($trust_pgn) {
- $last_pgn_white = $pgn->white;
- $last_pgn_black = $pgn->black;
- @last_pgn_uci_moves = @uci_moves;
- $pgn_hysteresis_counter = 0;
- handle_position($pos);
+ $pos->{'pretty_history'} = $moves;
+
+ # Sometimes, PGNs lose a move or two for a short while,
+ # or people push out new ones non-atomically.
+ # Thus, if we PGN doesn't change names but becomes
+ # shorter, we mistrust it for a few seconds.
+ my $trust_pgn = 1;
+ if (defined($last_pgn_white) && defined($last_pgn_black) &&
+ $last_pgn_white eq $pgn->white &&
+ $last_pgn_black eq $pgn->black &&
+ scalar(@uci_moves) < scalar(@last_pgn_uci_moves)) {
+ if (++$pgn_hysteresis_counter < 3) {
+ $trust_pgn = 0;
+ }
+ }
+ if ($trust_pgn) {
+ $last_pgn_white = $pgn->white;
+ $last_pgn_black = $pgn->black;
+ @last_pgn_uci_moves = @uci_moves;
+ $pgn_hysteresis_counter = 0;
+ handle_position($pos);
+ }
+ };
+ if ($@) {
+ warn "Error in parsing moves from $url\n";
}
}
# Don't update too often.
my $age = Time::HiRes::tv_interval($latest_update);
if ($age < $remoteglotconf::update_max_interval) {
- Time::HiRes::alarm($remoteglotconf::update_max_interval + 0.01 - $age);
+ my $wait = $remoteglotconf::update_max_interval + 0.01 - $age;
+ $output_timer = AnyEvent->timer(after => $wait, cb => \&output);
return;
}
my $t = $tb_cache{$fen};
my $pv = $t->{'pv'};
- my $matelen = int((1 + scalar @$pv) / 2);
+ 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') {
$json->{'seldepth'} = $info->{'seldepth'};
$json->{'tablebase'} = $info->{'tablebase'};
- # single-PV only for now
- $json->{'pv_uci'} = $info->{'pv'};
+ $json->{'pv_uci'} = $info->{'pv'}; # Still needs to be there for the JS to calculate arrows; only for the primary PV, though!
$json->{'pv_pretty'} = [ prettyprint_pv($pos_calculating, @{$info->{'pv'}}) ];
my %refutation_lines = ();
pretty_move => $pretty_move,
pv_pretty => \@pretty_pv,
};
- $refutation_lines{$pv->[0]}->{'pv_uci'} = $pv;
};
}
}
$json->{'refutation_lines'} = \%refutation_lines;
- open my $fh, ">", $remoteglotconf::json_output . ".tmp"
+ my $encoded = JSON::XS::encode_json($json);
+ atomic_set_contents($remoteglotconf::json_output, $encoded);
+
+ if (exists($pos_calculating->{'pretty_history'}) &&
+ defined($remoteglotconf::json_history_dir)) {
+ my $halfmove_num = scalar @{$pos_calculating->{'pretty_history'}};
+ (my $fen = $pos_calculating->fen()) =~ tr,/ ,-_,;
+ my $filename = $remoteglotconf::json_history_dir . "/move$halfmove_num-$fen.json";
+
+ # Overwrite old analysis (assuming it exists at all) if we're
+ # using a different engine, or if we've calculated deeper.
+ # nodes is used as a tiebreaker. Don't bother about Multi-PV
+ # data; it's not that important.
+ my ($old_engine, $old_depth, $old_nodes) = get_json_analysis_stats($filename);
+ my $new_depth = $json->{'depth'} // 0;
+ my $new_nodes = $json->{'nodes'} // 0;
+ if (!defined($old_engine) ||
+ $old_engine ne $json->{'id'}{'name'} ||
+ $new_depth > $old_depth ||
+ ($new_depth == $old_depth && $new_nodes >= $old_nodes)) {
+ atomic_set_contents($filename, $encoded);
+ }
+ }
+}
+
+sub atomic_set_contents {
+ my ($filename, $contents) = @_;
+
+ open my $fh, ">", $filename . ".tmp"
or return;
- print $fh JSON::XS::encode_json($json);
+ print $fh $contents;
close $fh;
- rename($remoteglotconf::json_output . ".tmp", $remoteglotconf::json_output);
+ rename($filename . ".tmp", $filename);
+}
+
+sub get_json_analysis_stats {
+ my $filename = shift;
+
+ my ($engine, $depth, $nodes);
+
+ open my $fh, "<", $filename
+ or return undef;
+ local $/ = undef;
+ eval {
+ my $json = JSON::XS::decode_json(<$fh>);
+ $engine = $json->{'id'}{'name'} // die;
+ $depth = $json->{'depth'} // 0;
+ $nodes = $json->{'nodes'} // 0;
+ };
+ close $fh;
+ if ($@) {
+ warn "Error in decoding $filename: $@";
+ return undef;
+ }
+ return ($engine, $depth, $nodes);
}
sub uciprint {
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);
+ 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
+ pv => \@uci_moves,
+ score => $response->{'Response'}{'Score'},
};
output();
}