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