]> git.sesse.net Git - remoteglot/blob - remoteglot.pl
Make sure analysis contempt is off; Stockfish > 9 enables it, and it gives rollercoas...
[remoteglot] / remoteglot.pl
1 #! /usr/bin/perl
2
3 #
4 # remoteglot - Connects an abitrary UCI-speaking engine to ICS for easier post-game
5 #              analysis, or for live analysis of relayed games. (Do not use for
6 #              cheating! Cheating is bad for your karma, and your abuser flag.)
7 #
8 # Copyright 2007 Steinar H. Gunderson <steinar+remoteglot@gunderson.no>
9 # Licensed under the GNU General Public License, version 2.
10 #
11
12 use AnyEvent;
13 use AnyEvent::Handle;
14 use AnyEvent::HTTP;
15 use Chess::PGN::Parse;
16 use EV;
17 use Net::Telnet;
18 use File::Slurp;
19 use IPC::Open2;
20 use Time::HiRes;
21 use JSON::XS;
22 use URI::Escape;
23 use DBI;
24 use DBD::Pg;
25 require 'Position.pm';
26 require 'Engine.pm';
27 require 'config.pm';
28 use strict;
29 use warnings;
30 no warnings qw(once);
31
32 # Program starts here
33 my $latest_update = undef;
34 my $output_timer = undef;
35 my $http_timer = undef;
36 my $stop_pgn_fetch = 0;
37 my $tb_retry_timer = undef;
38 my %tb_cache = ();
39 my $tb_lookup_running = 0;
40 my $last_written_json = undef;
41
42 # Persisted so we can restart.
43 # TODO: Figure out an appropriate way to deal with database restarts
44 # and/or Postgres going away entirely.
45 my $dbh = DBI->connect($remoteglotconf::dbistr, $remoteglotconf::dbiuser, $remoteglotconf::dbipass)
46         or die DBI->errstr;
47 $dbh->{RaiseError} = 1;
48
49 $| = 1;
50
51 open(FICSLOG, ">ficslog.txt")
52         or die "ficslog.txt: $!";
53 print FICSLOG "Log starting.\n";
54 select(FICSLOG);
55 $| = 1;
56
57 open(UCILOG, ">ucilog.txt")
58         or die "ucilog.txt: $!";
59 print UCILOG "Log starting.\n";
60 select(UCILOG);
61 $| = 1;
62
63 open(TBLOG, ">tblog.txt")
64         or die "tblog.txt: $!";
65 print TBLOG "Log starting.\n";
66 select(TBLOG);
67 $| = 1;
68
69 select(STDOUT);
70 umask 0022;  # analysis.json should not be served to users.
71
72 # open the chess engine
73 my $engine = open_engine($remoteglotconf::engine_cmdline, 'E1', sub { handle_uci(@_, 1); });
74 my $engine2 = open_engine($remoteglotconf::engine2_cmdline, 'E2', sub { handle_uci(@_, 0); });
75 my $last_move;
76 my $last_text = '';
77 my ($pos_calculating, $pos_calculating_second_engine);
78
79 uciprint($engine, "setoption name UCI_AnalyseMode value true");
80 uciprint($engine, "setoption name Analysis Contempt value Off");
81 while (my ($key, $value) = each %remoteglotconf::engine_config) {
82         uciprint($engine, "setoption name $key value $value");
83 }
84 uciprint($engine, "ucinewgame");
85
86 if (defined($engine2)) {
87         uciprint($engine2, "setoption name UCI_AnalyseMode value true");
88         uciprint($engine2, "setoption name Analysis Contempt value Off");
89         while (my ($key, $value) = each %remoteglotconf::engine2_config) {
90                 uciprint($engine2, "setoption name $key value $value");
91         }
92         uciprint($engine2, "setoption name MultiPV value 500");
93         uciprint($engine2, "ucinewgame");
94 }
95
96 print "Chess engine ready.\n";
97
98 # now talk to FICS
99 my ($t, $ev1);
100 if (defined($remoteglotconf::server)) {
101         $t = Net::Telnet->new(Timeout => 10, Prompt => '/fics% /');
102         $t->input_log(\*FICSLOG);
103         $t->open($remoteglotconf::server);
104         $t->print($remoteglotconf::nick);
105         $t->waitfor('/Press return to enter the server/');
106         $t->cmd("");
107
108         # set some options
109         $t->cmd("set shout 0");
110         $t->cmd("set seek 0");
111         $t->cmd("set style 12");
112
113         $ev1 = AnyEvent->io(
114                 fh => fileno($t),
115                 poll => 'r',
116                 cb => sub {    # what callback to execute
117                         while (1) {
118                                 my $line = $t->getline(Timeout => 0, errmode => 'return');
119                                 return if (!defined($line));
120
121                                 chomp $line;
122                                 $line =~ tr/\r//d;
123                                 handle_fics($line);
124                         }
125                 }
126         );
127 }
128 if (defined($remoteglotconf::target)) {
129         if ($remoteglotconf::target =~ /^(?:\/|https?:)/) {
130                 fetch_pgn($remoteglotconf::target);
131         } elsif (defined($t)) {
132                 $t->cmd("observe $remoteglotconf::target");
133         }
134 }
135 if (defined($t)) {
136         print "FICS ready.\n";
137 }
138
139 # Engine events have already been set up by Engine.pm.
140 EV::run;
141
142 sub handle_uci {
143         my ($engine, $line, $primary) = @_;
144
145         return if $line =~ /(upper|lower)bound/;
146
147         $line =~ s/  / /g;  # Sometimes needed for Zappa Mexico
148         print UCILOG localtime() . " $engine->{'tag'} <= $line\n";
149
150         # If we've sent a stop command, gobble up lines until we see bestmove.
151         return if ($engine->{'stopping'} && $line !~ /^bestmove/);
152         $engine->{'stopping'} = 0;
153
154         if ($line =~ /^info/) {
155                 my (@infos) = split / /, $line;
156                 shift @infos;
157
158                 parse_infos($engine, @infos);
159         }
160         if ($line =~ /^id/) {
161                 my (@ids) = split / /, $line;
162                 shift @ids;
163
164                 parse_ids($engine, @ids);
165         }
166         output();
167 }
168
169 my $getting_movelist = 0;
170 my $pos_for_movelist = undef;
171 my @uci_movelist = ();
172 my @pretty_movelist = ();
173
174 sub handle_fics {
175         my $line = shift;
176         if ($line =~ /^<12> /) {
177                 handle_position(Position->new($line));
178                 $t->cmd("moves");
179         }
180         if ($line =~ /^Movelist for game /) {
181                 my $pos = $pos_calculating;
182                 if (defined($pos)) {
183                         @uci_movelist = ();
184                         @pretty_movelist = ();
185                         $pos_for_movelist = Position->start_pos($pos->{'player_w'}, $pos->{'player_b'});
186                         $getting_movelist = 1;
187                 }
188         }
189         if ($getting_movelist &&
190             $line =~ /^\s* \d+\. \s+                     # move number
191                        (\S+) \s+ \( [\d:.]+ \) \s*       # first move, then time
192                        (?: (\S+) \s+ \( [\d:.]+ \) )?    # second move, then time 
193                      /x) {
194                 eval {
195                         my $uci_move;
196                         ($pos_for_movelist, $uci_move) = $pos_for_movelist->make_pretty_move($1);
197                         push @uci_movelist, $uci_move;
198                         push @pretty_movelist, $1;
199
200                         if (defined($2)) {
201                                 ($pos_for_movelist, $uci_move) = $pos_for_movelist->make_pretty_move($2);
202                                 push @uci_movelist, $uci_move;
203                                 push @pretty_movelist, $2;
204                         }
205                 };
206                 if ($@) {
207                         warn "Error when getting FICS move history: $@";
208                         $getting_movelist = 0;
209                 }
210         }
211         if ($getting_movelist &&
212             $line =~ /^\s+ \{.*\} \s+ (?: \* | 1\/2-1\/2 | 0-1 | 1-0 )/x) {
213                 # End of movelist.
214                 if (defined($pos_calculating)) {
215                         if ($pos_calculating->fen() eq $pos_for_movelist->fen()) {
216                                 $pos_calculating->{'history'} = \@pretty_movelist;
217                         }
218                 }
219                 $getting_movelist = 0;
220         }
221         if ($line =~ /^([A-Za-z]+)(?:\([A-Z]+\))* tells you: (.*)$/) {
222                 my ($who, $msg) = ($1, $2);
223
224                 next if (grep { $_ eq $who } (@remoteglotconf::masters) == 0);
225
226                 if ($msg =~ /^fics (.*?)$/) {
227                         $t->cmd("tell $who Executing '$1' on FICS.");
228                         $t->cmd($1);
229                 } elsif ($msg =~ /^uci (.*?)$/) {
230                         $t->cmd("tell $who Sending '$1' to the engine.");
231                         print { $engine->{'write'} } "$1\n";
232                 } elsif ($msg =~ /^pgn (.*?)$/) {
233                         my $url = $1;
234                         $t->cmd("tell $who Starting to poll '$url'.");
235                         fetch_pgn($url);
236                 } elsif ($msg =~ /^stoppgn$/) {
237                         $t->cmd("tell $who Stopping poll.");
238                         $stop_pgn_fetch = 1;
239                         $http_timer = undef;
240                 } elsif ($msg =~ /^quit$/) {
241                         $t->cmd("tell $who Bye bye.");
242                         exit;
243                 } else {
244                         $t->cmd("tell $who Couldn't understand '$msg', sorry.");
245                 }
246         }
247         #print "FICS: [$line]\n";
248 }
249
250 # Starts periodic fetching of PGNs from the given URL.
251 sub fetch_pgn {
252         my ($url) = @_;
253         if ($url =~ m#^/#) {  # Local file.
254                 eval {
255                         local $/ = undef;
256                         open my $fh, "<", $url
257                                 or die "$url: $!";
258                         my $pgn = <$fh>;
259                         close $fh;
260                         handle_pgn($pgn, '', $url);
261                 };
262                 if ($@) {
263                         warn "$url: $@";
264                         $http_timer = AnyEvent->timer(after => 1.0, cb => sub {
265                                 fetch_pgn($url);
266                         });
267                 }
268         } else {
269                 AnyEvent::HTTP::http_get($url, sub {
270                         handle_pgn(@_, $url);
271                 });
272         }
273 }
274
275 my ($last_pgn_white, $last_pgn_black);
276 my @last_pgn_uci_moves = ();
277 my $pgn_hysteresis_counter = 0;
278
279 sub handle_pgn {
280         my ($body, $header, $url) = @_;
281
282         if ($stop_pgn_fetch) {
283                 $stop_pgn_fetch = 0;
284                 $http_timer = undef;
285                 return;
286         }
287
288         my $pgn = Chess::PGN::Parse->new(undef, $body);
289         if (!defined($pgn)) {
290                 warn "Error in parsing PGN from $url [body='$body']\n";
291         } elsif (!$pgn->read_game()) {
292                 warn "Error in reading PGN game from $url [body='$body']\n";
293         } elsif ($body !~ /^\[/) {
294                 warn "Malformed PGN from $url [body='$body']\n";
295         } else {
296                 eval {
297                         # Skip to the right game.
298                         while (defined($remoteglotconf::pgn_filter) &&
299                                !&$remoteglotconf::pgn_filter($pgn)) {
300                                 $pgn->read_game() or die "Out of games during filtering";
301                         }
302
303                         $pgn->parse_game({ save_comments => 'yes' });
304                         my $white = $pgn->white;
305                         my $black = $pgn->black;
306                         $white =~ s/,.*//;  # Remove first name.
307                         $black =~ s/,.*//;  # Remove first name.
308                         my $tags = $pgn->tags();
309                         my $pos;
310                         if (exists($tags->{'FEN'})) {
311                                 $pos = Position->from_fen($tags->{'FEN'});
312                                 $pos->{'last_move'} = 'none';
313                                 $pos->{'player_w'} = $white;
314                                 $pos->{'player_b'} = $black;
315                                 $pos->{'start_fen'} = $tags->{'FEN'};
316                         } else {
317                                 $pos = Position->start_pos($white, $black);
318                         }
319                         if (exists($tags->{'Variant'}) &&
320                             $tags->{'Variant'} =~ /960|fischer/i) {
321                                 $pos->{'chess960'} = 1;
322                         } else {
323                                 $pos->{'chess960'} = 0;
324                         }
325                         my $moves = $pgn->moves;
326                         my @uci_moves = ();
327                         my @repretty_moves = ();
328                         for my $move (@$moves) {
329                                 my ($npos, $uci_move) = $pos->make_pretty_move($move);
330                                 push @uci_moves, $uci_move;
331
332                                 # Re-prettyprint the move.
333                                 my ($from_row, $from_col, $to_row, $to_col, $promo) = parse_uci_move($uci_move);
334                                 my ($pretty, undef) = $pos->{'board'}->prettyprint_move($from_row, $from_col, $to_row, $to_col, $promo);
335                                 push @repretty_moves, $pretty;
336                                 $pos = $npos;
337                         }
338                         if ($pgn->result eq '1-0' || $pgn->result eq '1/2-1/2' || $pgn->result eq '0-1') {
339                                 $pos->{'result'} = $pgn->result;
340                         }
341                         $pos->{'history'} = \@repretty_moves;
342
343                         extract_clock($pgn, $pos);
344
345                         # Sometimes, PGNs lose a move or two for a short while,
346                         # or people push out new ones non-atomically. 
347                         # Thus, if we PGN doesn't change names but becomes
348                         # shorter, we mistrust it for a few seconds.
349                         my $trust_pgn = 1;
350                         if (defined($last_pgn_white) && defined($last_pgn_black) &&
351                             $last_pgn_white eq $pgn->white &&
352                             $last_pgn_black eq $pgn->black &&
353                             scalar(@uci_moves) < scalar(@last_pgn_uci_moves)) {
354                                 if (++$pgn_hysteresis_counter < 3) {
355                                         $trust_pgn = 0; 
356                                 }
357                         }
358                         if ($trust_pgn) {
359                                 $last_pgn_white = $pgn->white;
360                                 $last_pgn_black = $pgn->black;
361                                 @last_pgn_uci_moves = @uci_moves;
362                                 $pgn_hysteresis_counter = 0;
363                                 handle_position($pos);
364                         }
365                 };
366                 if ($@) {
367                         warn "Error in parsing moves from $url: $@\n";
368                 }
369         }
370         
371         $http_timer = AnyEvent->timer(after => 1.0, cb => sub {
372                 fetch_pgn($url);
373         });
374 }
375
376 sub handle_position {
377         my ($pos) = @_;
378         find_clock_start($pos, $pos_calculating);
379                 
380         # If we're already chewing on this and there's nothing else in the queue,
381         # ignore it.
382         if (defined($pos_calculating) && $pos->fen() eq $pos_calculating->fen()) {
383                 $pos_calculating->{'result'} = $pos->{'result'};
384                 for my $key ('white_clock', 'black_clock', 'white_clock_target', 'black_clock_target') {
385                         $pos_calculating->{$key} //= $pos->{$key};
386                 }
387                 return;
388         }
389
390         # If we're already thinking on something, stop and wait for the engine
391         # to approve.
392         if (defined($pos_calculating)) {
393                 # Store the final data we have for this position in the history,
394                 # with the precise clock information we just got from the new
395                 # position. (Historic positions store the clock at the end of
396                 # the position.)
397                 #
398                 # Do not output anything new to the main analysis; that's
399                 # going to be obsolete really soon.
400                 $pos_calculating->{'white_clock'} = $pos->{'white_clock'};
401                 $pos_calculating->{'black_clock'} = $pos->{'black_clock'};
402                 delete $pos_calculating->{'white_clock_target'};
403                 delete $pos_calculating->{'black_clock_target'};
404                 output_json(1);
405
406                 # Ask the engine to stop; we will throw away its data until it
407                 # sends us "bestmove", signaling the end of it.
408                 $engine->{'stopping'} = 1;
409                 uciprint($engine, "stop");
410         }
411
412         # It's wrong to just give the FEN (the move history is useful,
413         # and per the UCI spec, we should really have sent "ucinewgame"),
414         # but it's easier, and it works around a Stockfish repetition issue.
415         if ($engine->{'chess960'} != $pos->{'chess960'}) {
416                 uciprint($engine, "setoption name UCI_Chess960 value " . ($pos->{'chess960'} ? 'true' : 'false'));
417                 $engine->{'chess960'} = $pos->{'chess960'};
418         }
419         uciprint($engine, "position fen " . $pos->fen());
420         uciprint($engine, "go infinite");
421         $pos_calculating = $pos;
422
423         if (defined($engine2)) {
424                 if (defined($pos_calculating_second_engine)) {
425                         $engine2->{'stopping'} = 1;
426                         uciprint($engine2, "stop");
427                 }
428                 if ($engine2->{'chess960'} != $pos->{'chess960'}) {
429                         uciprint($engine2, "setoption name UCI_Chess960 value " . ($pos->{'chess960'} ? 'true' : 'false'));
430                         $engine2->{'chess960'} = $pos->{'chess960'};
431                 }
432                 uciprint($engine2, "position fen " . $pos->fen());
433                 uciprint($engine2, "go infinite");
434                 $pos_calculating_second_engine = $pos;
435                 $engine2->{'info'} = {};
436         }
437
438         $engine->{'info'} = {};
439         $last_move = time;
440
441         schedule_tb_lookup();
442
443         # 
444         # Output a command every move to note that we're
445         # still paying attention -- this is a good tradeoff,
446         # since if no move has happened in the last half
447         # hour, the analysis/relay has most likely stopped
448         # and we should stop hogging server resources.
449         #
450         if (defined($t)) {
451                 $t->cmd("date");
452         }
453 }
454
455 sub parse_infos {
456         my ($engine, @x) = @_;
457         my $mpv = '';
458
459         my $info = $engine->{'info'};
460
461         # Search for "multipv" first of all, since e.g. Stockfish doesn't put it first.
462         for my $i (0..$#x - 1) {
463                 if ($x[$i] eq 'multipv') {
464                         $mpv = $x[$i + 1];
465                         next;
466                 }
467         }
468
469         while (scalar @x > 0) {
470                 if ($x[0] eq 'multipv') {
471                         # Dealt with above
472                         shift @x;
473                         shift @x;
474                         next;
475                 }
476                 if ($x[0] eq 'currmove' || $x[0] eq 'currmovenumber' || $x[0] eq 'cpuload') {
477                         my $key = shift @x;
478                         my $value = shift @x;
479                         $info->{$key} = $value;
480                         next;
481                 }
482                 if ($x[0] eq 'depth' || $x[0] eq 'seldepth' || $x[0] eq 'hashfull' ||
483                     $x[0] eq 'time' || $x[0] eq 'nodes' || $x[0] eq 'nps' ||
484                     $x[0] eq 'tbhits') {
485                         my $key = shift @x;
486                         my $value = shift @x;
487                         $info->{$key . $mpv} = $value;
488                         next;
489                 }
490                 if ($x[0] eq 'score') {
491                         shift @x;
492
493                         delete $info->{'score_cp' . $mpv};
494                         delete $info->{'score_mate' . $mpv};
495
496                         while ($x[0] eq 'cp' || $x[0] eq 'mate') {
497                                 if ($x[0] eq 'cp') {
498                                         shift @x;
499                                         $info->{'score_cp' . $mpv} = shift @x;
500                                 } elsif ($x[0] eq 'mate') {
501                                         shift @x;
502                                         $info->{'score_mate' . $mpv} = shift @x;
503                                 } else {
504                                         shift @x;
505                                 }
506                         }
507                         next;
508                 }
509                 if ($x[0] eq 'pv') {
510                         $info->{'pv' . $mpv} = [ @x[1..$#x] ];
511                         last;
512                 }
513                 if ($x[0] eq 'string' || $x[0] eq 'UCI_AnalyseMode' || $x[0] eq 'setting' || $x[0] eq 'contempt') {
514                         last;
515                 }
516
517                 #print "unknown info '$x[0]', trying to recover...\n";
518                 #shift @x;
519                 die "Unknown info '" . join(',', @x) . "'";
520
521         }
522 }
523
524 sub parse_ids {
525         my ($engine, @x) = @_;
526
527         while (scalar @x > 0) {
528                 if ($x[0] eq 'name') {
529                         my $value = join(' ', @x);
530                         $engine->{'id'}{'author'} = $value;
531                         last;
532                 }
533
534                 # unknown
535                 shift @x;
536         }
537 }
538
539 sub prettyprint_pv_no_cache {
540         my ($board, @pvs) = @_;
541
542         if (scalar @pvs == 0 || !defined($pvs[0])) {
543                 return ();
544         }
545
546         my $pv = shift @pvs;
547         my ($from_row, $from_col, $to_row, $to_col, $promo) = parse_uci_move($pv);
548         my ($pretty, $nb) = $board->prettyprint_move($from_row, $from_col, $to_row, $to_col, $promo);
549         return ( $pretty, prettyprint_pv_no_cache($nb, @pvs) );
550 }
551
552 sub prettyprint_pv {
553         my ($pos, @pvs) = @_;
554
555         my $cachekey = join('', @pvs);
556         if (exists($pos->{'prettyprint_cache'}{$cachekey})) {
557                 return @{$pos->{'prettyprint_cache'}{$cachekey}};
558         } else {
559                 my @res = prettyprint_pv_no_cache($pos->{'board'}, @pvs);
560                 $pos->{'prettyprint_cache'}{$cachekey} = \@res;
561                 return @res;
562         }
563 }
564
565 my %tbprobe_cache = ();
566
567 sub complete_using_tbprobe {
568         my ($pos, $info, $mpv) = @_;
569
570         # We need Fathom installed to do standalone TB probes.
571         return if (!defined($remoteglotconf::fathom_cmdline));
572
573         # If we already have a mate, don't bother; in some cases, it would even be
574         # better than a tablebase score.
575         return if defined($info->{'score_mate' . $mpv});
576
577         # If we have a draw or near-draw score, there's also not much interesting
578         # we could add from a tablebase. We only really want mates.
579         return if ($info->{'score_cp' . $mpv} >= -12250 && $info->{'score_cp' . $mpv} <= 12250);
580
581         # Run through the PV until we are at a 6-man position.
582         # TODO: We could in theory only have 5-man data.
583         my @pv = @{$info->{'pv' . $mpv}};
584         my $key = $pos->fen() . " " . join('', @pv);
585         my @moves = ();
586         if (exists($tbprobe_cache{$key})) {
587                 @moves = @{$tbprobe_cache{$key}};
588         } else {
589                 if ($mpv ne '') {
590                         # Force doing at least one move of the PV.
591                         my $move = shift @pv;
592                         push @moves, $move;
593                         $pos = $pos->make_move(parse_uci_move($move));
594                 }
595
596                 while ($pos->num_pieces() > 6 && $#pv > -1) {
597                         my $move = shift @pv;
598                         push @moves, $move;
599                         $pos = $pos->make_move(parse_uci_move($move));
600                 }
601
602                 return if ($pos->num_pieces() > 6);
603
604                 my $fen = $pos->fen();
605                 my $pgn_text = `fathom --path=/srv/syzygy "$fen"`;
606                 my $pgn = Chess::PGN::Parse->new(undef, $pgn_text);
607                 return if (!defined($pgn) || !$pgn->read_game() || ($pgn->result ne '0-1' && $pgn->result ne '1-0'));
608                 $pgn->quick_parse_game;
609                 $info->{'pv' . $mpv} = \@moves;
610
611                 # Splice the PV from the tablebase onto what we have so far.
612                 for my $move (@{$pgn->moves}) {
613                         last if $move eq '#';
614                         my $uci_move;
615                         ($pos, $uci_move) = $pos->make_pretty_move($move);
616                         push @moves, $uci_move;
617                 }
618
619                 $tbprobe_cache{$key} = \@moves;
620         }
621
622         $info->{'pv' . $mpv} = \@moves;
623
624         my $matelen = int((1 + scalar @moves) / 2);
625         if ((scalar @moves) % 2 == 0) {
626                 $info->{'score_mate' . $mpv} = -$matelen;
627         } else {
628                 $info->{'score_mate' . $mpv} = $matelen;
629         }
630 }
631
632 sub output {
633         #return;
634
635         return if (!defined($pos_calculating));
636
637         # Don't update too often.
638         my $age = Time::HiRes::tv_interval($latest_update);
639         if ($age < $remoteglotconf::update_max_interval) {
640                 my $wait = $remoteglotconf::update_max_interval + 0.01 - $age;
641                 $output_timer = AnyEvent->timer(after => $wait, cb => \&output);
642                 return;
643         }
644         
645         my $info = $engine->{'info'};
646
647         #
648         # If we have tablebase data from a previous lookup, replace the
649         # engine data with the data from the tablebase.
650         #
651         my $fen = $pos_calculating->fen();
652         if (exists($tb_cache{$fen})) {
653                 for my $key (qw(pv score_cp score_mate nodes nps depth seldepth tbhits)) {
654                         delete $info->{$key . '1'};
655                         delete $info->{$key};
656                 }
657                 $info->{'nodes'} = 0;
658                 $info->{'nps'} = 0;
659                 $info->{'depth'} = 0;
660                 $info->{'seldepth'} = 0;
661                 $info->{'tbhits'} = 0;
662
663                 my $t = $tb_cache{$fen};
664                 my $pv = $t->{'pv'};
665                 my $matelen = int((1 + $t->{'score'}) / 2);
666                 if ($t->{'result'} eq '1/2-1/2') {
667                         $info->{'score_cp'} = 0;
668                 } elsif ($t->{'result'} eq '1-0') {
669                         if ($pos_calculating->{'toplay'} eq 'B') {
670                                 $info->{'score_mate'} = -$matelen;
671                         } else {
672                                 $info->{'score_mate'} = $matelen;
673                         }
674                 } else {
675                         if ($pos_calculating->{'toplay'} eq 'B') {
676                                 $info->{'score_mate'} = $matelen;
677                         } else {
678                                 $info->{'score_mate'} = -$matelen;
679                         }
680                 }
681                 $info->{'pv'} = $pv;
682                 $info->{'tablebase'} = 1;
683         } else {
684                 $info->{'tablebase'} = 0;
685         }
686         
687         #
688         # Some programs _always_ report MultiPV, even with only one PV.
689         # In this case, we simply use that data as if MultiPV was never
690         # specified.
691         #
692         if (exists($info->{'pv1'}) && !exists($info->{'pv2'})) {
693                 for my $key (qw(pv score_cp score_mate nodes nps depth seldepth tbhits)) {
694                         if (exists($info->{$key . '1'})) {
695                                 $info->{$key} = $info->{$key . '1'};
696                         } else {
697                                 delete $info->{$key};
698                         }
699                 }
700         }
701         
702         #
703         # Check the PVs first. if they're invalid, just wait, as our data
704         # is most likely out of sync. This isn't a very good solution, as
705         # it can frequently miss stuff, but it's good enough for most users.
706         #
707         eval {
708                 my $dummy;
709                 if (exists($info->{'pv'})) {
710                         $dummy = prettyprint_pv($pos_calculating, @{$info->{'pv'}});
711                 }
712         
713                 my $mpv = 1;
714                 while (exists($info->{'pv' . $mpv})) {
715                         $dummy = prettyprint_pv($pos_calculating, @{$info->{'pv' . $mpv}});
716                         ++$mpv;
717                 }
718         };
719         if ($@) {
720                 $engine->{'info'} = {};
721                 return;
722         }
723
724         # Now do our own Syzygy tablebase probes to convert scores like +123.45 to mate.
725         if (exists($info->{'pv'})) {
726                 complete_using_tbprobe($pos_calculating, $info, '');
727         }
728
729         my $mpv = 1;
730         while (exists($info->{'pv' . $mpv})) {
731                 complete_using_tbprobe($pos_calculating, $info, $mpv);
732                 ++$mpv;
733         }
734
735         output_screen();
736         output_json(0);
737         $latest_update = [Time::HiRes::gettimeofday];
738 }
739
740 sub output_screen {
741         my $info = $engine->{'info'};
742         my $id = $engine->{'id'};
743
744         my $text = 'Analysis';
745         if ($pos_calculating->{'last_move'} ne 'none') {
746                 if ($pos_calculating->{'toplay'} eq 'W') {
747                         $text .= sprintf ' after %u. ... %s', ($pos_calculating->{'move_num'}-1), $pos_calculating->{'last_move'};
748                 } else {
749                         $text .= sprintf ' after %u. %s', $pos_calculating->{'move_num'}, $pos_calculating->{'last_move'};
750                 }
751                 if (exists($id->{'name'})) {
752                         $text .= ',';
753                 }
754         }
755
756         if (exists($id->{'name'})) {
757                 $text .= " by $id->{'name'}:\n\n";
758         } else {
759                 $text .= ":\n\n";
760         }
761
762         return unless (exists($pos_calculating->{'board'}));
763                 
764         if (exists($info->{'pv1'}) && exists($info->{'pv2'})) {
765                 # multi-PV
766                 my $mpv = 1;
767                 while (exists($info->{'pv' . $mpv})) {
768                         $text .= sprintf "  PV%2u", $mpv;
769                         my $score = short_score($info, $pos_calculating, $mpv);
770                         $text .= "  ($score)" if (defined($score));
771
772                         my $tbhits = '';
773                         if (exists($info->{'tbhits' . $mpv}) && $info->{'tbhits' . $mpv} > 0) {
774                                 if ($info->{'tbhits' . $mpv} == 1) {
775                                         $tbhits = ", 1 tbhit";
776                                 } else {
777                                         $tbhits = sprintf ", %u tbhits", $info->{'tbhits' . $mpv};
778                                 }
779                         }
780
781                         if (exists($info->{'nodes' . $mpv}) && exists($info->{'nps' . $mpv}) && exists($info->{'depth' . $mpv})) {
782                                 $text .= sprintf " (%5u kn, %3u kn/s, %2u ply$tbhits)",
783                                         $info->{'nodes' . $mpv} / 1000, $info->{'nps' . $mpv} / 1000, $info->{'depth' . $mpv};
784                         }
785
786                         $text .= ":\n";
787                         $text .= "  " . join(', ', prettyprint_pv($pos_calculating, @{$info->{'pv' . $mpv}})) . "\n";
788                         $text .= "\n";
789                         ++$mpv;
790                 }
791         } else {
792                 # single-PV
793                 my $score = long_score($info, $pos_calculating, '');
794                 $text .= "  $score\n" if defined($score);
795                 $text .=  "  PV: " . join(', ', prettyprint_pv($pos_calculating, @{$info->{'pv'}}));
796                 $text .=  "\n";
797
798                 if (exists($info->{'nodes'}) && exists($info->{'nps'}) && exists($info->{'depth'})) {
799                         $text .= sprintf "  %u nodes, %7u nodes/sec, depth %u ply",
800                                 $info->{'nodes'}, $info->{'nps'}, $info->{'depth'};
801                 }
802                 if (exists($info->{'seldepth'})) {
803                         $text .= sprintf " (%u selective)", $info->{'seldepth'};
804                 }
805                 if (exists($info->{'tbhits'}) && $info->{'tbhits'} > 0) {
806                         if ($info->{'tbhits'} == 1) {
807                                 $text .= ", one Syzygy hit";
808                         } else {
809                                 $text .= sprintf ", %u Syzygy hits", $info->{'tbhits'};
810                         }
811                 }
812                 $text .= "\n\n";
813         }
814
815         #$text .= book_info($pos_calculating->fen(), $pos_calculating->{'board'}, $pos_calculating->{'toplay'});
816
817         my @refutation_lines = ();
818         if (defined($engine2)) {
819                 for (my $mpv = 1; $mpv < 500; ++$mpv) {
820                         my $info = $engine2->{'info'};
821                         last if (!exists($info->{'pv' . $mpv}));
822                         eval {
823                                 complete_using_tbprobe($pos_calculating_second_engine, $info, $mpv);
824                                 my $pv = $info->{'pv' . $mpv};
825                                 my $pretty_move = join('', prettyprint_pv($pos_calculating_second_engine, $pv->[0]));
826                                 my @pretty_pv = prettyprint_pv($pos_calculating_second_engine, @$pv);
827                                 if (scalar @pretty_pv > 5) {
828                                         @pretty_pv = @pretty_pv[0..4];
829                                         push @pretty_pv, "...";
830                                 }
831                                 my $key = $pretty_move;
832                                 my $line = sprintf("  %-6s %6s %3s  %s",
833                                         $pretty_move,
834                                         short_score($info, $pos_calculating_second_engine, $mpv),
835                                         "d" . $info->{'depth' . $mpv},
836                                         join(', ', @pretty_pv));
837                                 push @refutation_lines, [ $key, $line ];
838                         };
839                 }
840         }
841
842         if ($#refutation_lines >= 0) {
843                 $text .= "Shallow search of all legal moves:\n\n";
844                 for my $line (sort { $a->[0] cmp $b->[0] } @refutation_lines) {
845                         $text .= $line->[1] . "\n";
846                 }
847                 $text .= "\n\n";        
848         }       
849
850         if ($last_text ne $text) {
851                 print "\e[H\e[2J"; # clear the screen
852                 print $text;
853                 $last_text = $text;
854         }
855 }
856
857 sub output_json {
858         my $historic_json_only = shift;
859         my $info = $engine->{'info'};
860
861         my $json = {};
862         $json->{'position'} = $pos_calculating->to_json_hash();
863         $json->{'engine'} = $engine->{'id'};
864         if (defined($remoteglotconf::engine_url)) {
865                 $json->{'engine'}{'url'} = $remoteglotconf::engine_url;
866         }
867         if (defined($remoteglotconf::engine_details)) {
868                 $json->{'engine'}{'details'} = $remoteglotconf::engine_details;
869         }
870         my @grpc_backends = ();
871         if (defined($remoteglotconf::engine_grpc_backend)) {
872                 push @grpc_backends, $remoteglotconf::engine_grpc_backend;
873         }
874         if (defined($remoteglotconf::engine2_grpc_backend)) {
875                 push @grpc_backends, $remoteglotconf::engine2_grpc_backend;
876         }
877         $json->{'internal'}{'grpc_backends'} = \@grpc_backends;
878         if (defined($remoteglotconf::move_source)) {
879                 $json->{'move_source'} = $remoteglotconf::move_source;
880         }
881         if (defined($remoteglotconf::move_source_url)) {
882                 $json->{'move_source_url'} = $remoteglotconf::move_source_url;
883         }
884         $json->{'score'} = score_digest($info, $pos_calculating, '');
885         $json->{'using_lomonosov'} = defined($remoteglotconf::tb_serial_key);
886
887         $json->{'nodes'} = $info->{'nodes'};
888         $json->{'nps'} = $info->{'nps'};
889         $json->{'depth'} = $info->{'depth'};
890         $json->{'tbhits'} = $info->{'tbhits'};
891         $json->{'seldepth'} = $info->{'seldepth'};
892         $json->{'tablebase'} = $info->{'tablebase'};
893         $json->{'pv'} = [ prettyprint_pv($pos_calculating, @{$info->{'pv'}}) ];
894
895         my %refutation_lines = ();
896         my @refutation_lines = ();
897         if (defined($engine2)) {
898                 for (my $mpv = 1; $mpv < 500; ++$mpv) {
899                         my $info = $engine2->{'info'};
900                         my $pretty_move = "";
901                         my @pretty_pv = ();
902                         last if (!exists($info->{'pv' . $mpv}));
903
904                         eval {
905                                 complete_using_tbprobe($pos_calculating, $info, $mpv);
906                                 my $pv = $info->{'pv' . $mpv};
907                                 my $pretty_move = join('', prettyprint_pv($pos_calculating, $pv->[0]));
908                                 my @pretty_pv = prettyprint_pv($pos_calculating, @$pv);
909                                 $refutation_lines{$pretty_move} = {
910                                         depth => $info->{'depth' . $mpv},
911                                         score => score_digest($info, $pos_calculating, $mpv),
912                                         move => $pretty_move,
913                                         pv => \@pretty_pv,
914                                 };
915                         };
916                 }
917         }
918         $json->{'refutation_lines'} = \%refutation_lines;
919
920         # Piece together historic score information, to the degree we have it.
921         if (!$historic_json_only && exists($pos_calculating->{'history'})) {
922                 my %score_history = ();
923
924                 local $dbh->{AutoCommit} = 0;
925                 my $q = $dbh->prepare('SELECT * FROM scores WHERE id=?');
926                 my $pos;
927                 if (exists($pos_calculating->{'start_fen'})) {
928                         $pos = Position->from_fen($pos_calculating->{'start_fen'});
929                 } else {
930                         $pos = Position->start_pos('white', 'black');
931                 }
932                 $pos->{'chess960'} = $pos_calculating->{'chess960'};
933                 my $halfmove_num = 0;
934                 for my $move (@{$pos_calculating->{'history'}}) {
935                         my $id = id_for_pos($pos, $halfmove_num);
936                         my $ref = $dbh->selectrow_hashref($q, undef, $id);
937                         if (defined($ref)) {
938                                 $score_history{$halfmove_num} = [
939                                         $ref->{'score_type'},
940                                         $ref->{'score_value'}
941                                 ];
942                         }
943                         ++$halfmove_num;
944                         ($pos) = $pos->make_pretty_move($move);
945                 }
946                 $q->finish;
947                 $dbh->commit;
948
949                 # If at any point we are missing 10 consecutive moves,
950                 # truncate the history there. This is so we don't get into
951                 # a situation where we e.g. start analyzing at move 45,
952                 # but we have analysis for 1. e4 from some completely different game
953                 # and thus show a huge hole.
954                 my $consecutive_missing = 0;
955                 my $truncate_until = 0;
956                 for (my $i = $halfmove_num; $i --> 0; ) {
957                         if ($consecutive_missing >= 10) {
958                                 delete $score_history{$i};
959                                 next;
960                         }
961                         if (exists($score_history{$i})) {
962                                 $consecutive_missing = 0;
963                         } else {
964                                 ++$consecutive_missing;
965                         }
966                 }
967
968                 $json->{'score_history'} = \%score_history;
969         }
970
971         # Give out a list of other games going on. (Empty is fine.)
972         # TODO: Don't bother reading our own file, the data will be stale anyway.
973         if (!$historic_json_only) {
974                 my @games = ();
975
976                 my $q = $dbh->prepare('SELECT * FROM current_games ORDER BY priority DESC, id');
977                 $q->execute;
978                 while (my $ref = $q->fetchrow_hashref) {
979                         eval {
980                                 my $other_game_contents = File::Slurp::read_file($ref->{'json_path'});
981                                 my $other_game_json = JSON::XS::decode_json($other_game_contents);
982
983                                 die "Missing position" if (!exists($other_game_json->{'position'}));
984                                 my $white = $other_game_json->{'position'}{'player_w'} // die 'Missing white';
985                                 my $black = $other_game_json->{'position'}{'player_b'} // die 'Missing black';
986
987                                 my $game = {
988                                         id => $ref->{'id'},
989                                         name => "$white–$black",
990                                         url => $ref->{'url'},
991                                         hashurl => $ref->{'hash_url'},
992                                 };
993                                 if (defined($other_game_json->{'position'}{'result'})) {
994                                         $game->{'result'} = $other_game_json->{'position'}{'result'};
995                                 } else {
996                                         $game->{'score'} = $other_game_json->{'score'};
997                                 }
998                                 push @games, $game;
999                         };
1000                         if ($@) {
1001                                 warn "Could not add external game " . $ref->{'json_path'} . ": $@";
1002                         }
1003                 }
1004
1005                 if (scalar @games > 0) {
1006                         $json->{'games'} = \@games;
1007                 }
1008         }
1009
1010         my $json_enc = JSON::XS->new;
1011         $json_enc->canonical(1);
1012         my $encoded = $json_enc->encode($json);
1013         unless ($historic_json_only || !defined($remoteglotconf::json_output) ||
1014                 (defined($last_written_json) && $last_written_json eq $encoded)) {
1015                 atomic_set_contents($remoteglotconf::json_output, $encoded);
1016                 $last_written_json = $encoded;
1017         }
1018
1019         if (exists($pos_calculating->{'history'}) &&
1020             defined($remoteglotconf::json_history_dir)) {
1021                 my $id = id_for_pos($pos_calculating);
1022                 my $filename = $remoteglotconf::json_history_dir . "/" . $id . ".json";
1023
1024                 # Overwrite old analysis (assuming it exists at all) if we're
1025                 # using a different engine, or if we've calculated deeper.
1026                 # nodes is used as a tiebreaker. Don't bother about Multi-PV
1027                 # data; it's not that important.
1028                 my ($old_engine, $old_depth, $old_nodes) = get_json_analysis_stats($id);
1029                 my $new_depth = $json->{'depth'} // 0;
1030                 my $new_nodes = $json->{'nodes'} // 0;
1031                 if (!defined($old_engine) ||
1032                     $old_engine ne $json->{'engine'}{'name'} ||
1033                     $new_depth > $old_depth ||
1034                     ($new_depth == $old_depth && $new_nodes >= $old_nodes)) {
1035                         atomic_set_contents($filename, $encoded);
1036                         if (defined($json->{'score'})) {
1037                                 $dbh->do('INSERT INTO scores (id, score_type, score_value, engine, depth, nodes) VALUES (?,?,?,?,?,?) ' .
1038                                          '    ON CONFLICT (id) DO UPDATE SET ' .
1039                                          '        score_type=EXCLUDED.score_type, ' .
1040                                          '        score_value=EXCLUDED.score_value, ' .
1041                                          '        engine=EXCLUDED.engine, ' .
1042                                          '        depth=EXCLUDED.depth, ' .
1043                                          '        nodes=EXCLUDED.nodes',
1044                                         undef,
1045                                         $id, $json->{'score'}[0], $json->{'score'}[1],
1046                                         $json->{'engine'}{'name'}, $new_depth, $new_nodes);
1047                         }
1048                 }
1049         }
1050 }
1051
1052 sub atomic_set_contents {
1053         my ($filename, $contents) = @_;
1054
1055         open my $fh, ">", $filename . ".tmp"
1056                 or return;
1057         print $fh $contents;
1058         close $fh;
1059         rename($filename . ".tmp", $filename);
1060 }
1061
1062 sub id_for_pos {
1063         my ($pos, $halfmove_num) = @_;
1064
1065         $halfmove_num //= scalar @{$pos->{'history'}};
1066         (my $fen = $pos->fen()) =~ tr,/ ,-_,;
1067         return "move$halfmove_num-$fen";
1068 }
1069
1070 sub get_json_analysis_stats {
1071         my $id = shift;
1072         my $ref = $dbh->selectrow_hashref('SELECT * FROM scores WHERE id=?', undef, $id);
1073         if (defined($ref)) {
1074                 return ($ref->{'engine'}, $ref->{'depth'}, $ref->{'nodes'});
1075         } else {
1076                 return ('', 0, 0);
1077         }
1078 }
1079
1080 sub uciprint {
1081         my ($engine, $msg) = @_;
1082         $engine->print($msg);
1083         print UCILOG localtime() . " $engine->{'tag'} => $msg\n";
1084 }
1085
1086 sub short_score {
1087         my ($info, $pos, $mpv) = @_;
1088
1089         my $invert = ($pos->{'toplay'} eq 'B');
1090         if (defined($info->{'score_mate' . $mpv})) {
1091                 if ($invert) {
1092                         return sprintf "M%3d", -$info->{'score_mate' . $mpv};
1093                 } else {
1094                         return sprintf "M%3d", $info->{'score_mate' . $mpv};
1095                 }
1096         } else {
1097                 if (exists($info->{'score_cp' . $mpv})) {
1098                         my $score = $info->{'score_cp' . $mpv} * 0.01;
1099                         if ($score == 0) {
1100                                 if ($info->{'tablebase'}) {
1101                                         return "TB draw";
1102                                 } else {
1103                                         return " 0.00";
1104                                 }
1105                         }
1106                         if ($invert) {
1107                                 $score = -$score;
1108                         }
1109                         return sprintf "%+5.2f", $score;
1110                 }
1111         }
1112
1113         return undef;
1114 }
1115
1116 # Sufficient for computing long_score, short_score, plot_score and
1117 # (with side-to-play information) score_sort_key.
1118 sub score_digest {
1119         my ($info, $pos, $mpv) = @_;
1120
1121         if (defined($info->{'score_mate' . $mpv})) {
1122                 my $mate = $info->{'score_mate' . $mpv};
1123                 if ($pos->{'toplay'} eq 'B') {
1124                         $mate = -$mate;
1125                 }
1126                 return ['m', $mate];
1127         } else {
1128                 if (exists($info->{'score_cp' . $mpv})) {
1129                         my $score = $info->{'score_cp' . $mpv};
1130                         if ($pos->{'toplay'} eq 'B') {
1131                                 $score = -$score;
1132                         }
1133                         if ($score == 0 && $info->{'tablebase'}) {
1134                                 return ['d', undef];
1135                         } else {
1136                                 return ['cp', int($score)];
1137                         }
1138                 }
1139         }
1140
1141         return undef;
1142 }
1143
1144 sub long_score {
1145         my ($info, $pos, $mpv) = @_;
1146
1147         if (defined($info->{'score_mate' . $mpv})) {
1148                 my $mate = $info->{'score_mate' . $mpv};
1149                 if ($pos->{'toplay'} eq 'B') {
1150                         $mate = -$mate;
1151                 }
1152                 if ($mate > 0) {
1153                         return sprintf "White mates in %u", $mate;
1154                 } else {
1155                         return sprintf "Black mates in %u", -$mate;
1156                 }
1157         } else {
1158                 if (exists($info->{'score_cp' . $mpv})) {
1159                         my $score = $info->{'score_cp' . $mpv} * 0.01;
1160                         if ($score == 0) {
1161                                 if ($info->{'tablebase'}) {
1162                                         return "Theoretical draw";
1163                                 } else {
1164                                         return "Score:  0.00";
1165                                 }
1166                         }
1167                         if ($pos->{'toplay'} eq 'B') {
1168                                 $score = -$score;
1169                         }
1170                         return sprintf "Score: %+5.2f", $score;
1171                 }
1172         }
1173
1174         return undef;
1175 }
1176
1177 # For graphs; a single number in centipawns, capped at +/- 500.
1178 sub plot_score {
1179         my ($info, $pos, $mpv) = @_;
1180
1181         my $invert = ($pos->{'toplay'} eq 'B');
1182         if (defined($info->{'score_mate' . $mpv})) {
1183                 my $mate = $info->{'score_mate' . $mpv};
1184                 if ($invert) {
1185                         $mate = -$mate;
1186                 }
1187                 if ($mate > 0) {
1188                         return 500;
1189                 } else {
1190                         return -500;
1191                 }
1192         } else {
1193                 if (exists($info->{'score_cp' . $mpv})) {
1194                         my $score = $info->{'score_cp' . $mpv};
1195                         if ($invert) {
1196                                 $score = -$score;
1197                         }
1198                         $score = 500 if ($score > 500);
1199                         $score = -500 if ($score < -500);
1200                         return int($score);
1201                 }
1202         }
1203
1204         return undef;
1205 }
1206
1207 my %book_cache = ();
1208 sub book_info {
1209         my ($fen, $board, $toplay) = @_;
1210
1211         if (exists($book_cache{$fen})) {
1212                 return $book_cache{$fen};
1213         }
1214
1215         my $ret = `./booklook $fen`;
1216         return "" if ($ret =~ /Not found/ || $ret eq '');
1217
1218         my @moves = ();
1219
1220         for my $m (split /\n/, $ret) {
1221                 my ($move, $annotation, $win, $draw, $lose, $rating, $rating_div) = split /,/, $m;
1222
1223                 my $pmove;
1224                 if ($move eq '')  {
1225                         $pmove = '(current)';
1226                 } else {
1227                         ($pmove) = prettyprint_pv_no_cache($board, $move);
1228                         $pmove .= $annotation;
1229                 }
1230
1231                 my $score;
1232                 if ($toplay eq 'W') {
1233                         $score = 1.0 * $win + 0.5 * $draw + 0.0 * $lose;
1234                 } else {
1235                         $score = 0.0 * $win + 0.5 * $draw + 1.0 * $lose;
1236                 }
1237                 my $n = $win + $draw + $lose;
1238                 
1239                 my $percent;
1240                 if ($n == 0) {
1241                         $percent = "     ";
1242                 } else {
1243                         $percent = sprintf "%4u%%", int(100.0 * $score / $n + 0.5);
1244                 }
1245
1246                 push @moves, [ $pmove, $n, $percent, $rating ];
1247         }
1248
1249         @moves[1..$#moves] = sort { $b->[2] cmp $a->[2] } @moves[1..$#moves];
1250         
1251         my $text = "Book moves:\n\n              Perf.     N     Rating\n\n";
1252         for my $m (@moves) {
1253                 $text .= sprintf "  %-10s %s   %6u    %4s\n", $m->[0], $m->[2], $m->[1], $m->[3]
1254         }
1255
1256         return $text;
1257 }
1258
1259 sub extract_clock {
1260         my ($pgn, $pos) = @_;
1261
1262         # Look for extended PGN clock tags.
1263         my $tags = $pgn->tags;
1264         if (exists($tags->{'WhiteClock'}) && exists($tags->{'BlackClock'})) {
1265                 $pos->{'white_clock'} = hms_to_sec($tags->{'WhiteClock'});
1266                 $pos->{'black_clock'} = hms_to_sec($tags->{'BlackClock'});
1267                 return;
1268         }
1269
1270         # Look for TCEC-style time comments.
1271         my $moves = $pgn->moves;
1272         my $comments = $pgn->comments;
1273         my $last_black_move = int((scalar @$moves) / 2);
1274         my $last_white_move = int((1 + scalar @$moves) / 2);
1275
1276         my $black_key = $last_black_move . "b";
1277         my $white_key = $last_white_move . "w";
1278
1279         if (exists($comments->{$white_key}) &&
1280             exists($comments->{$black_key}) &&
1281             $comments->{$white_key} =~ /(?:tl=|clk )(\d+:\d+:\d+)/ &&
1282             $comments->{$black_key} =~ /(?:tl=|clk )(\d+:\d+:\d+)/) {
1283                 $comments->{$white_key} =~ /(?:tl=|clk )(\d+:\d+:\d+)/;
1284                 $pos->{'white_clock'} = hms_to_sec($1);
1285                 $comments->{$black_key} =~ /(?:tl=|clk )(\d+:\d+:\d+)/;
1286                 $pos->{'black_clock'} = hms_to_sec($1);
1287                 return;
1288         }
1289
1290         delete $pos->{'white_clock'};
1291         delete $pos->{'black_clock'};
1292 }
1293
1294 sub hms_to_sec {
1295         my $hms = shift;
1296         return undef if (!defined($hms));
1297         $hms =~ /(\d+):(\d+):(\d+)/;
1298         return $1 * 3600 + $2 * 60 + $3;
1299 }
1300
1301 sub find_clock_start {
1302         my ($pos, $prev_pos) = @_;
1303
1304         # If the game is over, the clock is stopped.
1305         if (exists($pos->{'result'}) &&
1306             ($pos->{'result'} eq '1-0' ||
1307              $pos->{'result'} eq '1/2-1/2' ||
1308              $pos->{'result'} eq '0-1')) {
1309                 return;
1310         }
1311
1312         # When we don't have any moves, we assume the clock hasn't started yet.
1313         if ($pos->{'move_num'} == 1 && $pos->{'toplay'} eq 'W') {
1314                 if (defined($remoteglotconf::adjust_clocks_before_move)) {
1315                         &$remoteglotconf::adjust_clocks_before_move(\$pos->{'white_clock'}, \$pos->{'black_clock'}, 1, 'W');
1316                 }
1317                 return;
1318         }
1319
1320         # TODO(sesse): Maybe we can get the number of moves somehow else for FICS games.
1321         # The history is needed for id_for_pos.
1322         if (!exists($pos->{'history'})) {
1323                 return;
1324         }
1325
1326         my $id = id_for_pos($pos);
1327         my $clock_info = $dbh->selectrow_hashref('SELECT * FROM clock_info WHERE id=? AND COALESCE(white_clock_target, black_clock_target) >= EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - INTERVAL \'1 day\'));', undef, $id);
1328         if (defined($clock_info)) {
1329                 $pos->{'white_clock'} //= $clock_info->{'white_clock'};
1330                 $pos->{'black_clock'} //= $clock_info->{'black_clock'};
1331                 if ($pos->{'toplay'} eq 'W') {
1332                         $pos->{'white_clock_target'} = $clock_info->{'white_clock_target'};
1333                 } else {
1334                         $pos->{'black_clock_target'} = $clock_info->{'black_clock_target'};
1335                 }
1336                 return;
1337         }
1338
1339         # OK, we haven't seen this position before, so we assume the move
1340         # happened right now.
1341
1342         # See if we should do our own clock management (ie., clock information
1343         # is spurious or non-existent).
1344         if (defined($remoteglotconf::adjust_clocks_before_move)) {
1345                 my $wc = $pos->{'white_clock'} // $prev_pos->{'white_clock'};
1346                 my $bc = $pos->{'black_clock'} // $prev_pos->{'black_clock'};
1347                 if (defined($prev_pos->{'white_clock_target'})) {
1348                         $wc = $prev_pos->{'white_clock_target'} - time;
1349                 }
1350                 if (defined($prev_pos->{'black_clock_target'})) {
1351                         $bc = $prev_pos->{'black_clock_target'} - time;
1352                 }
1353                 &$remoteglotconf::adjust_clocks_before_move(\$wc, \$bc, $pos->{'move_num'}, $pos->{'toplay'});
1354                 $pos->{'white_clock'} = $wc;
1355                 $pos->{'black_clock'} = $bc;
1356         }
1357
1358         my $key = ($pos->{'toplay'} eq 'W') ? 'white_clock' : 'black_clock';
1359         if (!exists($pos->{$key})) {
1360                 # No clock information.
1361                 return;
1362         }
1363         my $time_left = $pos->{$key};
1364         my ($white_clock_target, $black_clock_target);
1365         if ($pos->{'toplay'} eq 'W') {
1366                 $white_clock_target = $pos->{'white_clock_target'} = time + $time_left;
1367         } else {
1368                 $black_clock_target = $pos->{'black_clock_target'} = time + $time_left;
1369         }
1370         local $dbh->{AutoCommit} = 0;
1371         $dbh->do('DELETE FROM clock_info WHERE id=?', undef, $id);
1372         $dbh->do('INSERT INTO clock_info (id, white_clock, black_clock, white_clock_target, black_clock_target) VALUES (?, ?, ?, ?, ?)', undef,
1373                 $id, $pos->{'white_clock'}, $pos->{'black_clock'}, $white_clock_target, $black_clock_target);
1374         $dbh->commit;
1375 }
1376
1377 sub schedule_tb_lookup {
1378         return if (!defined($remoteglotconf::tb_serial_key));
1379         my $pos = $pos_calculating;
1380         return if (exists($tb_cache{$pos->fen()}));
1381
1382         # If there's more than seven pieces, there's not going to be an answer,
1383         # so don't bother.
1384         return if ($pos->num_pieces() > 7);
1385
1386         # Max one at a time. If it's still relevant when it returns,
1387         # schedule_tb_lookup() will be called again.
1388         return if ($tb_lookup_running);
1389
1390         $tb_lookup_running = 1;
1391         my $url = 'http://tb7-api.chessok.com:6904/tasks/addtask?auth.login=' .
1392                 $remoteglotconf::tb_serial_key .
1393                 '&auth.password=aquarium&type=0&fen=' . 
1394                 URI::Escape::uri_escape($pos->fen());
1395         print TBLOG "Downloading $url...\n";
1396         AnyEvent::HTTP::http_get($url, sub {
1397                 handle_tb_lookup_return(@_, $pos, $pos->fen());
1398         });
1399 }
1400
1401 sub handle_tb_lookup_return {
1402         my ($body, $header, $pos, $fen) = @_;
1403         print TBLOG "Response for [$fen]:\n";
1404         print TBLOG $header . "\n\n";
1405         print TBLOG $body . "\n\n";
1406         eval {
1407                 my $response = JSON::XS::decode_json($body);
1408                 if ($response->{'ErrorCode'} != 0) {
1409                         die "Unknown tablebase server error: " . $response->{'ErrorDesc'};
1410                 }
1411                 my $state = $response->{'Response'}{'StateString'};
1412                 if ($state eq 'COMPLETE') {
1413                         my $pgn = Chess::PGN::Parse->new(undef, $response->{'Response'}{'Moves'});
1414                         if (!defined($pgn) || !$pgn->read_game()) {
1415                                 warn "Error in parsing PGN\n";
1416                         } else {
1417                                 $pgn->quick_parse_game;
1418                                 my $pvpos = $pos;
1419                                 my $moves = $pgn->moves;
1420                                 my @uci_moves = ();
1421                                 for my $move (@$moves) {
1422                                         my $uci_move;
1423                                         ($pvpos, $uci_move) = $pvpos->make_pretty_move($move);
1424                                         push @uci_moves, $uci_move;
1425                                 }
1426                                 $tb_cache{$fen} = {
1427                                         result => $pgn->result,
1428                                         pv => \@uci_moves,
1429                                         score => $response->{'Response'}{'Score'},
1430                                 };
1431                                 output();
1432                         }
1433                 } elsif ($state =~ /QUEUED/ || $state =~ /PROCESSING/) {
1434                         # Try again in a second. Note that if we have changed
1435                         # position in the meantime, we might query a completely
1436                         # different position! But that's fine.
1437                 } else {
1438                         die "Unknown response state " . $state;
1439                 }
1440
1441                 # Wait a second before we schedule another one.
1442                 $tb_retry_timer = AnyEvent->timer(after => 1.0, cb => sub {
1443                         $tb_lookup_running = 0;
1444                         schedule_tb_lookup();
1445                 });
1446         };
1447         if ($@) {
1448                 warn "Error in tablebase lookup: $@";
1449
1450                 # Don't try this one again, but don't block new lookups either.
1451                 $tb_lookup_running = 0;
1452         }
1453 }
1454
1455 sub open_engine {
1456         my ($cmdline, $tag, $cb) = @_;
1457         return undef if (!defined($cmdline));
1458         return Engine->open($cmdline, $tag, $cb);
1459 }
1460
1461 sub col_letter_to_num {
1462         return ord(shift) - ord('a');
1463 }
1464
1465 sub row_letter_to_num {
1466         return 7 - (ord(shift) - ord('1'));
1467 }
1468
1469 sub parse_uci_move {
1470         my $move = shift;
1471         my $from_col = col_letter_to_num(substr($move, 0, 1));
1472         my $from_row = row_letter_to_num(substr($move, 1, 1));
1473         my $to_col   = col_letter_to_num(substr($move, 2, 1));
1474         my $to_row   = row_letter_to_num(substr($move, 3, 1));
1475         my $promo    = substr($move, 4, 1);
1476         return ($from_row, $from_col, $to_row, $to_col, $promo);
1477 }