]> git.sesse.net Git - remoteglot/blob - remoteglot.pl
39fa629304b429840583dfe22cbe414b25284153
[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 <sgunderson@bigfoot.com>
9 # Licensed under the GNU General Public License, version 2.
10 #
11
12 use Net::Telnet;
13 use FileHandle;
14 use IPC::Open2;
15 use Time::HiRes;
16 use JSON::XS;
17 use strict;
18 use warnings;
19
20 # Configuration
21 my $server = "freechess.org";
22 my $target = "GMCarlsen";
23 my $engine_cmdline = "'./Deep Rybka 4 SSE42 x64'";
24 my $engine2_cmdline = "./stockfish_13111119_x64_modern_sse42";
25 my $telltarget = undef;   # undef to be silent
26 my @tell_intervals = (5, 20, 60, 120, 240, 480, 960);  # after each move
27 my $uci_assume_full_compliance = 0;                    # dangerous :-)
28 my $update_max_interval = 2.0;
29 my $second_engine_start_depth = 8;
30 my @masters = (
31         'Sesse',
32         'Sessse',
33         'Sesssse',
34         'greatestguns',
35         'beuki'
36 );
37
38 # Program starts here
39 $SIG{ALRM} = sub { output(); };
40 my $latest_update = undef;
41
42 $| = 1;
43
44 open(FICSLOG, ">ficslog.txt")
45         or die "ficslog.txt: $!";
46 print FICSLOG "Log starting.\n";
47 select(FICSLOG);
48 $| = 1;
49
50 open(UCILOG, ">ucilog.txt")
51         or die "ucilog.txt: $!";
52 print UCILOG "Log starting.\n";
53 select(UCILOG);
54 $| = 1;
55 select(STDOUT);
56
57 # open the chess engine
58 my $engine = open_engine($engine_cmdline, 'E1');
59 my $engine2 = open_engine($engine2_cmdline, 'E2');
60 my ($last_move, $last_tell);
61 my $last_text = '';
62 my $last_told_text = '';
63 my ($pos_waiting, $pos_calculating, $pos_calculating_second_engine);
64
65 uciprint($engine, "setoption name UCI_AnalyseMode value true");
66 # uciprint($engine, "setoption name NalimovPath value /srv/tablebase");
67 uciprint($engine, "setoption name NalimovUsage value Rarely");
68 uciprint($engine, "setoption name Hash value 1024");
69 # uciprint($engine, "setoption name MultiPV value 2");
70 uciprint($engine, "ucinewgame");
71
72 uciprint($engine2, "setoption name UCI_AnalyseMode value true");
73 # uciprint($engine2, "setoption name NalimovPath value /srv/tablebase");
74 uciprint($engine2, "setoption name NalimovUsage value Rarely");
75 uciprint($engine2, "setoption name Hash value 1024");
76 uciprint($engine2, "setoption name Threads value 8");
77 uciprint($engine2, "setoption name MultiPV value 500");
78 uciprint($engine2, "ucinewgame");
79
80 print "Chess engine ready.\n";
81
82 # now talk to FICS
83 my $t = Net::Telnet->new(Timeout => 10, Prompt => '/fics% /');
84 $t->input_log(\*FICSLOG);
85 $t->open($server);
86 $t->print("SesseBOT");
87 $t->waitfor('/Press return to enter the server/');
88 $t->cmd("");
89
90 # set some options
91 $t->cmd("set shout 0");
92 $t->cmd("set seek 0");
93 $t->cmd("set style 12");
94 $t->cmd("observe $target");
95
96 # main loop
97 print "FICS ready.\n";
98 while (1) {
99         my $rin = '';
100         my $rout;
101         vec($rin, fileno($engine->{'read'}), 1) = 1;
102         vec($rin, fileno($engine2->{'read'}), 1) = 1;
103         vec($rin, fileno($t), 1) = 1;
104
105         my ($nfound, $timeleft) = select($rout=$rin, undef, undef, 5.0);
106         my $sleep = 1.0;
107
108         while (1) {
109                 my $line = $t->getline(Timeout => 0, errmode => 'return');
110                 last if (!defined($line));
111
112                 chomp $line;
113                 $line =~ tr/\r//d;
114                 if ($line =~ /^<12> /) {
115                         my $pos = style12_to_pos($line);
116                         
117                         # if this is already in the queue, ignore it
118                         next if (defined($pos_waiting) && $pos->{'fen'} eq $pos_waiting->{'fen'});
119
120                         # if we're already chewing on this and there's nothing else in the queue,
121                         # also ignore it
122                         next if (!defined($pos_waiting) && defined($pos_calculating) &&
123                                  $pos->{'fen'} eq $pos_calculating->{'fen'});
124
125                         # if we're already thinking on something, stop and wait for the engine
126                         # to approve
127                         if (defined($pos_calculating)) {
128                                 if (!defined($pos_waiting)) {
129                                         uciprint($engine, "stop");
130                                 }
131                                 if ($uci_assume_full_compliance) {
132                                         $pos_waiting = $pos;
133                                 } else {
134                                         uciprint($engine, "position fen " . $pos->{'fen'});
135                                         uciprint($engine, "go infinite");
136                                         $pos_calculating = $pos;
137                                 }
138                         } else {
139                                 # it's wrong just to give the FEN (the move history is useful,
140                                 # and per the UCI spec, we should really have sent "ucinewgame"),
141                                 # but it's easier
142                                 uciprint($engine, "position fen " . $pos->{'fen'});
143                                 uciprint($engine, "go infinite");
144                                 $pos_calculating = $pos;
145                         }
146
147                         if (defined($pos_calculating_second_engine)) {
148                                 uciprint($engine2, "stop");
149                         } else {
150                                 uciprint($engine2, "position fen " . $pos->{'fen'});
151                                 uciprint($engine2, "go infinite");
152                                 $pos_calculating_second_engine = $pos;
153                         }
154
155                         $engine->{'info'} = {};
156                         $engine2->{'info'} = {};
157                         $last_move = time;
158
159                         # 
160                         # Output a command every move to note that we're
161                         # still paying attention -- this is a good tradeoff,
162                         # since if no move has happened in the last half
163                         # hour, the analysis/relay has most likely stopped
164                         # and we should stop hogging server resources.
165                         #
166                         $t->cmd("date");
167                 }
168                 if ($line =~ /^([A-Za-z]+)(?:\([A-Z]+\))* tells you: (.*)$/) {
169                         my ($who, $msg) = ($1, $2);
170
171                         next if (grep { $_ eq $who } (@masters) == 0);
172         
173                         if ($msg =~ /^fics (.*?)$/) {
174                                 $t->cmd("tell $who Executing '$1' on FICS.");
175                                 $t->cmd($1);
176                         } elsif ($msg =~ /^uci (.*?)$/) {
177                                 $t->cmd("tell $who Sending '$1' to the engine.");
178                                 print { $engine->{'write'} } "$1\n";
179                         } else {
180                                 $t->cmd("tell $who Couldn't understand '$msg', sorry.");
181                         }
182                 }
183                 #print "FICS: [$line]\n";
184                 $sleep = 0;
185         }
186         
187         # any fun on the UCI channel?
188         if ($nfound > 0 && vec($rout, fileno($engine->{'read'}), 1) == 1) {
189                 my @lines = read_lines($engine);
190                 for my $line (@lines) {
191                         next if $line =~ /(upper|lower)bound/;
192                         handle_uci($engine, $line, 1);
193                 }
194                 $sleep = 0;
195
196                 output();
197         }
198         if ($nfound > 0 && vec($rout, fileno($engine2->{'read'}), 1) == 1) {
199                 my @lines = read_lines($engine2);
200                 for my $line (@lines) {
201                         next if $line =~ /(upper|lower)bound/;
202                         handle_uci($engine2, $line, 0);
203                 }
204                 $sleep = 0;
205
206                 output();
207         }
208
209         sleep $sleep;
210 }
211
212 sub handle_uci {
213         my ($engine, $line, $primary) = @_;
214
215         chomp $line;
216         $line =~ tr/\r//d;
217         $line =~ s/  / /g;  # Sometimes needed for Zappa Mexico
218         print UCILOG localtime() . " $engine->{'tag'} <= $line\n";
219         if ($line =~ /^info/) {
220                 my (@infos) = split / /, $line;
221                 shift @infos;
222
223                 parse_infos($engine, @infos);
224         }
225         if ($line =~ /^id/) {
226                 my (@ids) = split / /, $line;
227                 shift @ids;
228
229                 parse_ids($engine, @ids);
230         }
231         if ($line =~ /^bestmove/) {
232                 if ($primary) {
233                         return if (!$uci_assume_full_compliance);
234                         if (defined($pos_waiting)) {
235                                 uciprint($engine, "position fen " . $pos_waiting->{'fen'});
236                                 uciprint($engine, "go infinite");
237
238                                 $pos_calculating = $pos_waiting;
239                                 $pos_waiting = undef;
240                         }
241                 } else {
242                         my $pos = $pos_waiting // $pos_calculating;
243                         uciprint($engine2, "position fen " . $pos->{'fen'});
244                         uciprint($engine2, "go infinite");
245                         $pos_calculating_second_engine = $pos;
246                 }
247         }
248 }
249
250 sub parse_infos {
251         my ($engine, @x) = @_;
252         my $mpv = '';
253
254         my $info = $engine->{'info'};
255
256         # Search for "multipv" first of all, since e.g. Stockfish doesn't put it first.
257         for my $i (0..$#x - 1) {
258                 if ($x[$i] =~ 'multipv') {
259                         $mpv = $x[$i + 1];
260                         next;
261                 }
262         }
263
264         while (scalar @x > 0) {
265                 if ($x[0] =~ 'multipv') {
266                         # Dealt with above
267                         shift @x;
268                         shift @x;
269                         next;
270                 }
271                 if ($x[0] =~ /^(currmove|currmovenumber|cpuload)$/) {
272                         my $key = shift @x;
273                         my $value = shift @x;
274                         $info->{$key} = $value;
275                         next;
276                 }
277                 if ($x[0] =~ /^(depth|seldepth|hashfull|time|nodes|nps|tbhits)$/) {
278                         my $key = shift @x;
279                         my $value = shift @x;
280                         $info->{$key . $mpv} = $value;
281                         next;
282                 }
283                 if ($x[0] eq 'score') {
284                         shift @x;
285
286                         delete $info->{'score_cp' . $mpv};
287                         delete $info->{'score_mate' . $mpv};
288
289                         while ($x[0] =~ /^(cp|mate|lowerbound|upperbound)$/) {
290                                 if ($x[0] eq 'cp') {
291                                         shift @x;
292                                         $info->{'score_cp' . $mpv} = shift @x;
293                                 } elsif ($x[0] eq 'mate') {
294                                         shift @x;
295                                         $info->{'score_mate' . $mpv} = shift @x;
296                                 } else {
297                                         shift @x;
298                                 }
299                         }
300                         next;
301                 }
302                 if ($x[0] eq 'pv') {
303                         $info->{'pv' . $mpv} = [ @x[1..$#x] ];
304                         last;
305                 }
306                 if ($x[0] eq 'string' || $x[0] eq 'UCI_AnalyseMode' || $x[0] eq 'setting' || $x[0] eq 'contempt') {
307                         last;
308                 }
309
310                 #print "unknown info '$x[0]', trying to recover...\n";
311                 #shift @x;
312                 die "Unknown info '" . join(',', @x) . "'";
313
314         }
315 }
316
317 sub parse_ids {
318         my ($engine, @x) = @_;
319
320         while (scalar @x > 0) {
321                 if ($x[0] =~ /^(name|author)$/) {
322                         my $key = shift @x;
323                         my $value = join(' ', @x);
324                         $engine->{'id'}{$key} = $value;
325                         last;
326                 }
327
328                 # unknown
329                 shift @x;
330         }
331 }
332
333 sub style12_to_pos {
334         my $str = shift;
335         my %pos = ();
336         my (@x) = split / /, $str;
337         
338         $pos{'board'} = [ @x[1..8] ];
339         $pos{'toplay'} = $x[9];
340         $pos{'ep_file_num'} = $x[10];
341         $pos{'white_castle_k'} = $x[11];
342         $pos{'white_castle_q'} = $x[12];
343         $pos{'black_castle_k'} = $x[13];
344         $pos{'black_castle_q'} = $x[14];
345         $pos{'time_to_100move_rule'} = $x[15];
346         $pos{'move_num'} = $x[26];
347         $pos{'last_move'} = $x[29];
348         $pos{'fen'} = make_fen(\%pos);
349
350         return \%pos;
351 }
352
353 sub make_fen {
354         my $pos = shift;
355
356         # the board itself
357         my (@board) = @{$pos->{'board'}};
358         for my $rank (0..7) {
359                 $board[$rank] =~ s/(-+)/length($1)/ge;
360         }
361         my $fen = join('/', @board);
362
363         # white/black to move
364         $fen .= " ";
365         $fen .= lc($pos->{'toplay'});
366
367         # castling
368         my $castling = "";
369         $castling .= "K" if ($pos->{'white_castle_k'} == 1);
370         $castling .= "Q" if ($pos->{'white_castle_q'} == 1);
371         $castling .= "k" if ($pos->{'black_castle_k'} == 1);
372         $castling .= "q" if ($pos->{'black_castle_q'} == 1);
373         $castling = "-" if ($castling eq "");
374         # $castling = "-"; # chess960
375         $fen .= " ";
376         $fen .= $castling;
377
378         # en passant
379         my $ep = "-";
380         if ($pos->{'ep_file_num'} != -1) {
381                 my $col = $pos->{'ep_file_num'};
382                 my $nep = (qw(a b c d e f g h))[$col];
383
384                 if ($pos->{'toplay'} eq 'B') {
385                         $nep .= "3";
386                 } else {
387                         $nep .= "6";
388                 }
389
390                 #
391                 # Showing the en passant square when actually no capture can be made
392                 # seems to confuse at least Rybka. Thus, check if there's actually
393                 # a pawn of the opposite side that can do the en passant move, and if
394                 # not, just lie -- it doesn't matter anyway. I'm unsure what's the
395                 # "right" thing as per the standard, though.
396                 #
397                 if ($pos->{'toplay'} eq 'B') {
398                         $ep = $nep if ($col > 0 && substr($pos->{'board'}[4], $col-1, 1) eq 'p');
399                         $ep = $nep if ($col < 7 && substr($pos->{'board'}[4], $col+1, 1) eq 'p');
400                 } else {
401                         $ep = $nep if ($col > 0 && substr($pos->{'board'}[3], $col-1, 1) eq 'P');
402                         $ep = $nep if ($col < 7 && substr($pos->{'board'}[3], $col+1, 1) eq 'P');
403                 }
404         }
405         $fen .= " ";
406         $fen .= $ep;
407
408         # half-move clock
409         $fen .= " ";
410         $fen .= $pos->{'time_to_100move_rule'};
411
412         # full-move clock
413         $fen .= " ";
414         $fen .= $pos->{'move_num'};
415
416         return $fen;
417 }
418
419 sub make_move {
420         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
421         my $move = move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo);
422         my $piece = substr($board->[$from_row], $from_col, 1);
423         my @nb = @$board;
424
425         if ($piece eq '-') {
426                 die "Invalid move $move";
427         }
428
429         # white short castling
430         if ($move eq 'e1g1' && $piece eq 'K') {
431                 # king
432                 substr($nb[7], 4, 1, '-');
433                 substr($nb[7], 6, 1, $piece);
434                 
435                 # rook
436                 substr($nb[7], 7, 1, '-');
437                 substr($nb[7], 5, 1, 'R');
438                                 
439                 return \@nb;
440         }
441
442         # white long castling
443         if ($move eq 'e1c1' && $piece eq 'K') {
444                 # king
445                 substr($nb[7], 4, 1, '-');
446                 substr($nb[7], 2, 1, $piece);
447                 
448                 # rook
449                 substr($nb[7], 0, 1, '-');
450                 substr($nb[7], 3, 1, 'R');
451                                 
452                 return \@nb;
453         }
454
455         # black short castling
456         if ($move eq 'e8g8' && $piece eq 'k') {
457                 # king
458                 substr($nb[0], 4, 1, '-');
459                 substr($nb[0], 6, 1, $piece);
460                 
461                 # rook
462                 substr($nb[0], 7, 1, '-');
463                 substr($nb[0], 5, 1, 'r');
464                                 
465                 return \@nb;
466         }
467
468         # black long castling
469         if ($move eq 'e8c8' && $piece eq 'k') {
470                 # king
471                 substr($nb[0], 4, 1, '-');
472                 substr($nb[0], 2, 1, $piece);
473                 
474                 # rook
475                 substr($nb[0], 0, 1, '-');
476                 substr($nb[0], 3, 1, 'r');
477                                 
478                 return \@nb;
479         }
480
481         # check if the from-piece is a pawn
482         if (lc($piece) eq 'p') {
483                 # attack?
484                 if ($from_col != $to_col) {
485                         # en passant?
486                         if (substr($board->[$to_row], $to_col, 1) eq '-') {
487                                 if ($piece eq 'p') {
488                                         substr($nb[$to_row + 1], $to_col, 1, '-');
489                                 } else {
490                                         substr($nb[$to_row - 1], $to_col, 1, '-');
491                                 }
492                         }
493                 } else {
494                         if ($promo ne '') {
495                                 if ($piece eq 'p') {
496                                         $piece = $promo;
497                                 } else {
498                                         $piece = uc($promo);
499                                 }
500                         }
501                 }
502         }
503
504         # update the board
505         substr($nb[$from_row], $from_col, 1, '-');
506         substr($nb[$to_row], $to_col, 1, $piece);
507
508         return \@nb;
509 }
510
511 sub prettyprint_pv {
512         my ($board, @pvs) = @_;
513
514         if (scalar @pvs == 0 || !defined($pvs[0])) {
515                 return ();
516         }
517
518         my $pv = shift @pvs;
519         my $from_col = col_letter_to_num(substr($pv, 0, 1));
520         my $from_row = row_letter_to_num(substr($pv, 1, 1));
521         my $to_col   = col_letter_to_num(substr($pv, 2, 1));
522         my $to_row   = row_letter_to_num(substr($pv, 3, 1));
523         my $promo    = substr($pv, 4, 1);
524
525         my $nb = make_move($board, $from_row, $from_col, $to_row, $to_col, $promo);
526         my $piece = substr($board->[$from_row], $from_col, 1);
527
528         if ($piece eq '-') {
529                 die "Invalid move $pv";
530         }
531
532         # white short castling
533         if ($pv eq 'e1g1' && $piece eq 'K') {
534                 return ('0-0', prettyprint_pv($nb, @pvs));
535         }
536
537         # white long castling
538         if ($pv eq 'e1c1' && $piece eq 'K') {
539                 return ('0-0-0', prettyprint_pv($nb, @pvs));
540         }
541
542         # black short castling
543         if ($pv eq 'e8g8' && $piece eq 'k') {
544                 return ('0-0', prettyprint_pv($nb, @pvs));
545         }
546
547         # black long castling
548         if ($pv eq 'e8c8' && $piece eq 'k') {
549                 return ('0-0-0', prettyprint_pv($nb, @pvs));
550         }
551
552         my $pretty;
553
554         # check if the from-piece is a pawn
555         if (lc($piece) eq 'p') {
556                 # attack?
557                 if ($from_col != $to_col) {
558                         $pretty = substr($pv, 0, 1) . 'x' . substr($pv, 2, 2);
559                 } else {
560                         $pretty = substr($pv, 2, 2);
561
562                         if (length($pv) == 5) {
563                                 # promotion
564                                 $pretty .= "=";
565                                 $pretty .= uc(substr($pv, 4, 1));
566
567                                 if ($piece eq 'p') {
568                                         $piece = substr($pv, 4, 1);
569                                 } else {
570                                         $piece = uc(substr($pv, 4, 1));
571                                 }
572                         }
573                 }
574         } else {
575                 $pretty = uc($piece);
576
577                 # see how many of these pieces could go here, in all
578                 my $num_total = 0;
579                 for my $col (0..7) {
580                         for my $row (0..7) {
581                                 next unless (substr($board->[$row], $col, 1) eq $piece);
582                                 ++$num_total if (can_reach($board, $piece, $row, $col, $to_row, $to_col));
583                         }
584                 }
585
586                 # see how many of these pieces from the given row could go here
587                 my $num_row = 0;
588                 for my $col (0..7) {
589                         next unless (substr($board->[$from_row], $col, 1) eq $piece);
590                         ++$num_row if (can_reach($board, $piece, $from_row, $col, $to_row, $to_col));
591                 }
592                 
593                 # and same for columns
594                 my $num_col = 0;
595                 for my $row (0..7) {
596                         next unless (substr($board->[$row], $from_col, 1) eq $piece);
597                         ++$num_col if (can_reach($board, $piece, $row, $from_col, $to_row, $to_col));
598                 }
599                 
600                 # see if we need to disambiguate
601                 if ($num_total > 1) {
602                         if ($num_col == 1) {
603                                 $pretty .= substr($pv, 0, 1);
604                         } elsif ($num_row == 1) {
605                                 $pretty .= substr($pv, 1, 1);
606                         } else {
607                                 $pretty .= substr($pv, 0, 2);
608                         }
609                 }
610
611                 # attack?
612                 if (substr($board->[$to_row], $to_col, 1) ne '-') {
613                         $pretty .= 'x';
614                 }
615
616                 $pretty .= substr($pv, 2, 2);
617         }
618
619         if (in_mate($nb)) {
620                 $pretty .= '#';
621         } elsif (in_check($nb) ne 'none') {
622                 $pretty .= '+';
623         }
624         return ($pretty, prettyprint_pv($nb, @pvs));
625 }
626
627 sub output {
628         #return;
629
630         return if (!defined($pos_calculating));
631
632         # Don't update too often.
633         my $age = Time::HiRes::tv_interval($latest_update);
634         if ($age < $update_max_interval) {
635                 Time::HiRes::alarm($update_max_interval + 0.01 - $age);
636                 return;
637         }
638         
639         my $info = $engine->{'info'};
640         
641         #
642         # Some programs _always_ report MultiPV, even with only one PV.
643         # In this case, we simply use that data as if MultiPV was never
644         # specified.
645         #
646         if (exists($info->{'pv1'}) && !exists($info->{'pv2'})) {
647                 for my $key (qw(pv score_cp score_mate nodes nps depth seldepth tbhits)) {
648                         if (exists($info->{$key . '1'})) {
649                                 $info->{$key} = $info->{$key . '1'};
650                         }
651                 }
652         }
653         
654         #
655         # Check the PVs first. if they're invalid, just wait, as our data
656         # is most likely out of sync. This isn't a very good solution, as
657         # it can frequently miss stuff, but it's good enough for most users.
658         #
659         eval {
660                 my $dummy;
661                 if (exists($info->{'pv'})) {
662                         $dummy = prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv'}});
663                 }
664         
665                 my $mpv = 1;
666                 while (exists($info->{'pv' . $mpv})) {
667                         $dummy = prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv' . $mpv}});
668                         ++$mpv;
669                 }
670         };
671         if ($@) {
672                 $engine->{'info'} = {};
673                 return;
674         }
675
676         output_screen();
677         output_json();
678         $latest_update = [Time::HiRes::gettimeofday];
679 }
680
681 sub output_screen {
682         my $info = $engine->{'info'};
683         my $id = $engine->{'id'};
684
685         my $text = 'Analysis';
686         if ($pos_calculating->{'last_move'} ne 'none') {
687                 if ($pos_calculating->{'toplay'} eq 'W') {
688                         $text .= sprintf ' after %u. ... %s', ($pos_calculating->{'move_num'}-1), $pos_calculating->{'last_move'};
689                 } else {
690                         $text .= sprintf ' after %u. %s', $pos_calculating->{'move_num'}, $pos_calculating->{'last_move'};
691                 }
692                 if (exists($id->{'name'})) {
693                         $text .= ',';
694                 }
695         }
696
697         if (exists($id->{'name'})) {
698                 $text .= " by $id->{'name'}:\n\n";
699         } else {
700                 $text .= ":\n\n";
701         }
702
703         return unless (exists($pos_calculating->{'board'}));
704                 
705         if (exists($info->{'pv1'}) && exists($info->{'pv2'})) {
706                 # multi-PV
707                 my $mpv = 1;
708                 while (exists($info->{'pv' . $mpv})) {
709                         $text .= sprintf "  PV%2u", $mpv;
710                         my $score = short_score($info, $pos_calculating, $mpv);
711                         $text .= "  ($score)" if (defined($score));
712
713                         my $tbhits = '';
714                         if (exists($info->{'tbhits' . $mpv}) && $info->{'tbhits' . $mpv} > 0) {
715                                 if ($info->{'tbhits' . $mpv} == 1) {
716                                         $tbhits = ", 1 tbhit";
717                                 } else {
718                                         $tbhits = sprintf ", %u tbhits", $info->{'tbhits' . $mpv};
719                                 }
720                         }
721
722                         if (exists($info->{'nodes' . $mpv}) && exists($info->{'nps' . $mpv}) && exists($info->{'depth' . $mpv})) {
723                                 $text .= sprintf " (%5u kn, %3u kn/s, %2u ply$tbhits)",
724                                         $info->{'nodes' . $mpv} / 1000, $info->{'nps' . $mpv} / 1000, $info->{'depth' . $mpv};
725                         }
726
727                         $text .= ":\n";
728                         $text .= "  " . join(', ', prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv' . $mpv}})) . "\n";
729                         $text .= "\n";
730                         ++$mpv;
731                 }
732         } else {
733                 # single-PV
734                 my $score = long_score($info, $pos_calculating, '');
735                 $text .= "  $score\n" if defined($score);
736                 $text .=  "  PV: " . join(', ', prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv'}}));
737                 $text .=  "\n";
738
739                 if (exists($info->{'nodes'}) && exists($info->{'nps'}) && exists($info->{'depth'})) {
740                         $text .= sprintf "  %u nodes, %7u nodes/sec, depth %u ply",
741                                 $info->{'nodes'}, $info->{'nps'}, $info->{'depth'};
742                 }
743                 if (exists($info->{'seldepth'})) {
744                         $text .= sprintf " (%u selective)", $info->{'seldepth'};
745                 }
746                 if (exists($info->{'tbhits'}) && $info->{'tbhits'} > 0) {
747                         if ($info->{'tbhits'} == 1) {
748                                 $text .= ", one Nalimov hit";
749                         } else {
750                                 $text .= sprintf ", %u Nalimov hits", $info->{'tbhits'};
751                         }
752                 }
753                 $text .= "\n\n";
754         }
755
756         #$text .= book_info($pos_calculating->{'fen'}, $pos_calculating->{'board'}, $pos_calculating->{'toplay'});
757
758         my @refutation_lines = ();
759         for (my $mpv = 1; $mpv < 500; ++$mpv) {
760                 my $info = $engine2->{'info'};
761                 last if (!exists($info->{'pv' . $mpv}));
762                 eval {
763                         my $pv = $info->{'pv' . $mpv};
764
765                         my $pretty_move = join('', prettyprint_pv($pos_calculating_second_engine->{'board'}, $pv->[0]));
766                         my @pretty_pv = prettyprint_pv($pos_calculating_second_engine->{'board'}, @$pv);
767                         if (scalar @pretty_pv > 5) {
768                                 @pretty_pv = @pretty_pv[0..4];
769                                 push @pretty_pv, "...";
770                         }
771                         my $key = $pretty_move;
772                         my $line = sprintf("  %-6s %6s %3s  %s",
773                                 $pretty_move,
774                                 short_score($info, $pos_calculating_second_engine, $mpv, 1),
775                                 "d" . $info->{'depth' . $mpv},
776                                 join(', ', @pretty_pv));
777                         push @refutation_lines, [ $key, $line ];
778                 };
779         }
780
781         if ($#refutation_lines >= 0) {
782                 $text .= "Shallow search of all legal moves:\n\n";
783                 for my $line (sort { $a->[0] cmp $b->[0] } @refutation_lines) {
784                         $text .= $line->[1] . "\n";
785                 }
786                 $text .= "\n\n";        
787         }       
788
789         if ($last_text ne $text) {
790                 print "\e[H\e[2J"; # clear the screen
791                 print $text;
792                 $last_text = $text;
793         }
794
795         # Now construct the tell text, if any
796         return if (!defined($telltarget));
797
798         my $tell_text = '';
799
800         if (exists($id->{'name'})) {
801                 $tell_text .= "Analysis by $id->{'name'} -- see http://analysis.sesse.net/ for more information\n";
802         } else {
803                 $tell_text .= "Computer analysis -- http://analysis.sesse.net/ for more information\n";
804         }
805
806         if (exists($info->{'pv1'}) && exists($info->{'pv2'})) {
807                 # multi-PV
808                 my $mpv = 1;
809                 while (exists($info->{'pv' . $mpv})) {
810                         $tell_text .= sprintf "  PV%2u", $mpv;
811                         my $score = short_score($info, $pos_calculating, $mpv);
812                         $tell_text .= "  ($score)" if (defined($score));
813
814                         if (exists($info->{'depth' . $mpv})) {
815                                 $tell_text .= sprintf " (%2u ply)", $info->{'depth' . $mpv};
816                         }
817
818                         $tell_text .= ": ";
819                         $tell_text .= join(', ', prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv' . $mpv}}));
820                         $tell_text .= "\n";
821                         ++$mpv;
822                 }
823         } else {
824                 # single-PV
825                 my $score = long_score($info, $pos_calculating, '');
826                 $tell_text .= "  $score\n" if defined($score);
827                 $tell_text .= "  PV: " . join(', ', prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv'}}));
828                 if (exists($info->{'depth'})) {
829                         $tell_text .= sprintf " (depth %u ply)", $info->{'depth'};
830                 }
831                 $tell_text .=  "\n";
832         }
833
834         # see if a new tell is called for -- it is if the delay has expired _and_
835         # this is not simply a repetition of the last one
836         if ($last_told_text ne $tell_text) {
837                 my $now = time;
838                 for my $iv (@tell_intervals) {
839                         last if ($now - $last_move < $iv);
840                         next if ($last_tell - $last_move >= $iv);
841
842                         for my $line (split /\n/, $tell_text) {
843                                 $t->print("tell $telltarget [$target] $line");
844                         }
845
846                         $last_told_text = $text;
847                         $last_tell = $now;
848
849                         last;
850                 }
851         }
852 }
853
854 sub output_json {
855         my $info = $engine->{'info'};
856
857         my $json = {};
858         $json->{'position'} = $pos_calculating;
859         $json->{'id'} = $engine->{'id'};
860         $json->{'score'} = long_score($info, $pos_calculating, '');
861
862         $json->{'nodes'} = $info->{'nodes'};
863         $json->{'nps'} = $info->{'nps'};
864         $json->{'depth'} = $info->{'depth'};
865         $json->{'tbhits'} = $info->{'tbhits'};
866         $json->{'seldepth'} = $info->{'seldepth'};
867
868         # single-PV only for now
869         $json->{'pv_uci'} = $info->{'pv'};
870         $json->{'pv_pretty'} = [ prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv'}}) ];
871
872         my %refutation_lines = ();
873         my @refutation_lines = ();
874         for (my $mpv = 1; $mpv < 500; ++$mpv) {
875                 my $info = $engine2->{'info'};
876                 my $pretty_move = "";
877                 my @pretty_pv = ();
878                 last if (!exists($info->{'pv' . $mpv}));
879
880                 eval {
881                         my $pv = $info->{'pv' . $mpv};
882                         my $pretty_move = join('', prettyprint_pv($pos_calculating->{'board'}, $pv->[0]));
883                         my @pretty_pv = prettyprint_pv($pos_calculating->{'board'}, @$pv);
884                         $refutation_lines{$pv->[0]} = {
885                                 sort_key => $pretty_move,
886                                 depth => $info->{'depth' . $mpv},
887                                 score_sort_key => score_sort_key($info, $pos_calculating, $mpv, 0),
888                                 pretty_score => short_score($info, $pos_calculating, $mpv, 0),
889                                 pretty_move => $pretty_move,
890                                 pv_pretty => \@pretty_pv,
891                         };
892                         $refutation_lines{$pv->[0]}->{'pv_uci'} = $pv;
893                 };
894         }
895         $json->{'refutation_lines'} = \%refutation_lines;
896
897         open my $fh, ">analysis.json.tmp"
898                 or return;
899         print $fh JSON::XS::encode_json($json);
900         close $fh;
901         rename("analysis.json.tmp", "analysis.json");   
902 }
903
904 sub find_kings {
905         my $board = shift;
906         my ($wkr, $wkc, $bkr, $bkc);
907
908         for my $row (0..7) {
909                 for my $col (0..7) {
910                         my $piece = substr($board->[$row], $col, 1);
911                         if ($piece eq 'K') {
912                                 ($wkr, $wkc) = ($row, $col);
913                         } elsif ($piece eq 'k') {
914                                 ($bkr, $bkc) = ($row, $col);
915                         }
916                 }
917         }
918
919         return ($wkr, $wkc, $bkr, $bkc);
920 }
921
922 sub in_mate {
923         my $board = shift;
924         my $check = in_check($board);
925         return 0 if ($check eq 'none');
926
927         # try all possible moves for the side in check
928         for my $row (0..7) {
929                 for my $col (0..7) {
930                         my $piece = substr($board->[$row], $col, 1);
931                         next if ($piece eq '-');
932
933                         if ($check eq 'white') {
934                                 next if ($piece eq lc($piece));
935                         } else {
936                                 next if ($piece eq uc($piece));
937                         }
938
939                         for my $dest_row (0..7) {
940                                 for my $dest_col (0..7) {
941                                         next if ($row == $dest_row && $col == $dest_col);
942                                         next unless (can_reach($board, $piece, $row, $col, $dest_row, $dest_col));
943
944                                         my @nb = @$board;
945                                         substr($nb[$row], $col, 1, '-');
946                                         substr($nb[$dest_row], $dest_col, 1, $piece);
947
948                                         my $new_check = in_check(\@nb);
949                                         return 0 if ($new_check ne $check && $new_check ne 'both');
950                                 }
951                         }
952                 }
953         }
954
955         # nothing to do; mate
956         return 1;
957 }
958
959 sub in_check {
960         my $board = shift;
961         my ($black_check, $white_check) = (0, 0);
962
963         my ($wkr, $wkc, $bkr, $bkc) = find_kings($board);
964
965         # check all pieces for the possibility of threatening the two kings
966         for my $row (0..7) {
967                 for my $col (0..7) {
968                         my $piece = substr($board->[$row], $col, 1);
969                         next if ($piece eq '-');
970                 
971                         if (uc($piece) eq $piece) {
972                                 # white piece
973                                 $black_check = 1 if (can_reach($board, $piece, $row, $col, $bkr, $bkc));
974                         } else {
975                                 # black piece
976                                 $white_check = 1 if (can_reach($board, $piece, $row, $col, $wkr, $wkc));
977                         }
978                 }
979         }
980
981         if ($black_check && $white_check) {
982                 return 'both';
983         } elsif ($black_check) {
984                 return 'black';
985         } elsif ($white_check) {
986                 return 'white';
987         } else {
988                 return 'none';
989         }
990 }
991
992 sub can_reach {
993         my ($board, $piece, $from_row, $from_col, $to_row, $to_col) = @_;
994         
995         # can't eat your own piece
996         my $dest_piece = substr($board->[$to_row], $to_col, 1);
997         if ($dest_piece ne '-') {
998                 return 0 if (($piece eq lc($piece)) == ($dest_piece eq lc($dest_piece)));
999         }
1000
1001         if (lc($piece) eq 'k') {
1002                 return (abs($from_row - $to_row) <= 1 && abs($from_col - $to_col) <= 1);
1003         }
1004         if (lc($piece) eq 'r') {
1005                 return 0 unless ($from_row == $to_row || $from_col == $to_col);
1006
1007                 # check that there's a clear passage
1008                 if ($from_row == $to_row) {
1009                         if ($from_col > $to_col) {
1010                                 ($to_col, $from_col) = ($from_col, $to_col);
1011                         }
1012
1013                         for my $c (($from_col+1)..($to_col-1)) {
1014                                 my $middle_piece = substr($board->[$to_row], $c, 1);
1015                                 return 0 if ($middle_piece ne '-');     
1016                         }
1017
1018                         return 1;
1019                 } else {
1020                         if ($from_row > $to_row) {
1021                                 ($to_row, $from_row) = ($from_row, $to_row);
1022                         }
1023
1024                         for my $r (($from_row+1)..($to_row-1)) {
1025                                 my $middle_piece = substr($board->[$r], $to_col, 1);
1026                                 return 0 if ($middle_piece ne '-');     
1027                         }
1028
1029                         return 1;
1030                 }
1031         }
1032         if (lc($piece) eq 'b') {
1033                 return 0 unless (abs($from_row - $to_row) == abs($from_col - $to_col));
1034
1035                 my $dr = ($to_row - $from_row) / abs($to_row - $from_row);
1036                 my $dc = ($to_col - $from_col) / abs($to_col - $from_col);
1037
1038                 my $r = $from_row + $dr;
1039                 my $c = $from_col + $dc;
1040
1041                 while ($r != $to_row) {
1042                         my $middle_piece = substr($board->[$r], $c, 1);
1043                         return 0 if ($middle_piece ne '-');
1044                         
1045                         $r += $dr;
1046                         $c += $dc;
1047                 }
1048
1049                 return 1;
1050         }
1051         if (lc($piece) eq 'n') {
1052                 my $diff_r = abs($from_row - $to_row);
1053                 my $diff_c = abs($from_col - $to_col);
1054                 return 1 if ($diff_r == 2 && $diff_c == 1);
1055                 return 1 if ($diff_r == 1 && $diff_c == 2);
1056                 return 0;
1057         }
1058         if ($piece eq 'q') {
1059                 return (can_reach($board, 'r', $from_row, $from_col, $to_row, $to_col) ||
1060                         can_reach($board, 'b', $from_row, $from_col, $to_row, $to_col));
1061         }
1062         if ($piece eq 'Q') {
1063                 return (can_reach($board, 'R', $from_row, $from_col, $to_row, $to_col) ||
1064                         can_reach($board, 'B', $from_row, $from_col, $to_row, $to_col));
1065         }
1066
1067         # TODO: en passant
1068         if ($piece eq 'p') {
1069                 # black pawn
1070                 if ($to_col == $from_col && $to_row == $from_row + 1) {
1071                         return ($dest_piece eq '-');
1072                 }
1073                 if ($to_col == $from_col && $from_row == 1 && $to_row == 3) {
1074                         my $middle_piece = substr($board->[2], $to_col, 1);
1075                         return ($dest_piece eq '-' && $middle_piece eq '-');
1076                 }
1077                 if (abs($to_col - $from_col) == 1 && $to_row == $from_row + 1) {
1078                         return ($dest_piece ne '-');
1079                 }
1080                 return 0;
1081         }
1082         if ($piece eq 'P') {
1083                 # white pawn
1084                 if ($to_col == $from_col && $to_row == $from_row - 1) {
1085                         return ($dest_piece eq '-');
1086                 }
1087                 if ($to_col == $from_col && $from_row == 6 && $to_row == 4) {
1088                         my $middle_piece = substr($board->[5], $to_col, 1);
1089                         return ($dest_piece eq '-' && $middle_piece eq '-');
1090                 }
1091                 if (abs($to_col - $from_col) == 1 && $to_row == $from_row - 1) {
1092                         return ($dest_piece ne '-');
1093                 }
1094                 return 0;
1095         }
1096         
1097         # unknown piece
1098         return 0;
1099 }
1100
1101 sub uciprint {
1102         my ($engine, $msg) = @_;
1103         print { $engine->{'write'} } "$msg\n";
1104         print UCILOG localtime() . " $engine->{'tag'} => $msg\n";
1105 }
1106
1107 sub short_score {
1108         my ($info, $pos, $mpv, $invert) = @_;
1109
1110         $invert //= 0;
1111         if ($pos->{'toplay'} eq 'B') {
1112                 $invert = !$invert;
1113         }
1114
1115         if (defined($info->{'score_mate' . $mpv})) {
1116                 if ($invert) {
1117                         return sprintf "M%3d", -$info->{'score_mate' . $mpv};
1118                 } else {
1119                         return sprintf "M%3d", $info->{'score_mate' . $mpv};
1120                 }
1121         } else {
1122                 if (exists($info->{'score_cp' . $mpv})) {
1123                         my $score = $info->{'score_cp' . $mpv} * 0.01;
1124                         if ($score == 0) {
1125                                 return " 0.00";
1126                         }
1127                         if ($invert) {
1128                                 $score = -$score;
1129                         }
1130                         return sprintf "%+5.2f", $score;
1131                 }
1132         }
1133
1134         return undef;
1135 }
1136
1137 sub score_sort_key {
1138         my ($info, $pos, $mpv, $invert) = @_;
1139
1140         if (defined($info->{'score_mate' . $mpv})) {
1141                 if ($invert) {
1142                         return -(99999 - $info->{'score_mate' . $mpv});
1143                 } else {
1144                         return 99999 - $info->{'score_mate' . $mpv};
1145                 }
1146         } else {
1147                 if (exists($info->{'score_cp' . $mpv})) {
1148                         my $score = $info->{'score_cp' . $mpv};
1149                         if ($invert) {
1150                                 $score = -$score;
1151                         }
1152                         return $score;
1153                 }
1154         }
1155
1156         return undef;
1157 }
1158
1159 sub long_score {
1160         my ($info, $pos, $mpv) = @_;
1161
1162         if (defined($info->{'score_mate' . $mpv})) {
1163                 my $mate = $info->{'score_mate' . $mpv};
1164                 if ($pos->{'toplay'} eq 'B') {
1165                         $mate = -$mate;
1166                 }
1167                 if ($mate > 0) {
1168                         return sprintf "White mates in %u", $mate;
1169                 } else {
1170                         return sprintf "Black mates in %u", -$mate;
1171                 }
1172         } else {
1173                 if (exists($info->{'score_cp' . $mpv})) {
1174                         my $score = $info->{'score_cp' . $mpv} * 0.01;
1175                         if ($pos->{'toplay'} eq 'B') {
1176                                 $score = -$score;
1177                         }
1178                         return sprintf "Score: %+5.2f", $score;
1179                 }
1180         }
1181
1182         return undef;
1183 }
1184
1185 my %book_cache = ();
1186 sub book_info {
1187         my ($fen, $board, $toplay) = @_;
1188
1189         if (exists($book_cache{$fen})) {
1190                 return $book_cache{$fen};
1191         }
1192
1193         my $ret = `./booklook $fen`;
1194         return "" if ($ret =~ /Not found/ || $ret eq '');
1195
1196         my @moves = ();
1197
1198         for my $m (split /\n/, $ret) {
1199                 my ($move, $annotation, $win, $draw, $lose, $rating, $rating_div) = split /,/, $m;
1200
1201                 my $pmove;
1202                 if ($move eq '')  {
1203                         $pmove = '(current)';
1204                 } else {
1205                         ($pmove) = prettyprint_pv($board, $move);
1206                         $pmove .= $annotation;
1207                 }
1208
1209                 my $score;
1210                 if ($toplay eq 'W') {
1211                         $score = 1.0 * $win + 0.5 * $draw + 0.0 * $lose;
1212                 } else {
1213                         $score = 0.0 * $win + 0.5 * $draw + 1.0 * $lose;
1214                 }
1215                 my $n = $win + $draw + $lose;
1216                 
1217                 my $percent;
1218                 if ($n == 0) {
1219                         $percent = "     ";
1220                 } else {
1221                         $percent = sprintf "%4u%%", int(100.0 * $score / $n + 0.5);
1222                 }
1223
1224                 push @moves, [ $pmove, $n, $percent, $rating ];
1225         }
1226
1227         @moves[1..$#moves] = sort { $b->[2] cmp $a->[2] } @moves[1..$#moves];
1228         
1229         my $text = "Book moves:\n\n              Perf.     N     Rating\n\n";
1230         for my $m (@moves) {
1231                 $text .= sprintf "  %-10s %s   %6u    %4s\n", $m->[0], $m->[2], $m->[1], $m->[3]
1232         }
1233
1234         return $text;
1235 }
1236
1237 sub open_engine {
1238         my ($cmdline, $tag) = @_;
1239         my ($uciread, $uciwrite);
1240         my $pid = IPC::Open2::open2($uciread, $uciwrite, $cmdline);
1241
1242         my $engine = {
1243                 pid => $pid,
1244                 read => $uciread,
1245                 readbuf => '',
1246                 write => $uciwrite,
1247                 info => {},
1248                 ids => {},
1249                 tag => $tag,
1250         };
1251
1252         uciprint($engine, "uci");
1253
1254         # gobble the options
1255         while (<$uciread>) {
1256                 /uciok/ && last;
1257                 handle_uci($engine, $_);
1258         }
1259         
1260         return $engine;
1261 }
1262
1263 sub read_lines {
1264         my $engine = shift;
1265
1266         # 
1267         # Read until we've got a full line -- if the engine sends part of
1268         # a line and then stops we're pretty much hosed, but that should
1269         # never happen.
1270         #
1271         while ($engine->{'readbuf'} !~ /\n/) {
1272                 my $tmp;
1273                 my $ret = sysread $engine->{'read'}, $tmp, 4096;
1274
1275                 if (!defined($ret)) {
1276                         next if ($!{EINTR});
1277                         die "error in reading from the UCI engine: $!";
1278                 } elsif ($ret == 0) {
1279                         die "EOF from UCI engine";
1280                 }
1281
1282                 $engine->{'readbuf'} .= $tmp;
1283         }
1284
1285         # Blah.
1286         my @lines = ();
1287         while ($engine->{'readbuf'} =~ s/^([^\n]*)\n//) {
1288                 my $line = $1;
1289                 $line =~ tr/\r\n//d;
1290                 push @lines, $line;
1291         }
1292         return @lines;
1293 }
1294
1295 sub col_letter_to_num {
1296         return ord(shift) - ord('a');
1297 }
1298
1299 sub row_letter_to_num {
1300         return 7 - (ord(shift) - ord('1'));
1301 }
1302
1303 sub move_to_uci_notation {
1304         my ($from_row, $from_col, $to_row, $to_col, $promo) = @_;
1305         $promo //= "";
1306         return sprintf("%c%d%c%d%s", ord('a') + $from_col, 8 - $from_row, ord('a') + $to_col, 8 - $to_row, $promo);
1307 }