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.)
8 # Copyright 2007 Steinar H. Gunderson <sgunderson@bigfoot.com>
9 # Licensed under the GNU General Public License, version 2.
17 require 'Position.pm';
22 my $server = "freechess.org";
23 my $target = "GMCarlsen";
24 my $engine_cmdline = "'./Deep Rybka 4 SSE42 x64'";
25 my $engine2_cmdline = "./stockfish_13111119_x64_modern_sse42"; # undef for none
26 my $uci_assume_full_compliance = 0; # dangerous :-)
27 my $update_max_interval = 1.0;
37 $SIG{ALRM} = sub { output(); };
38 my $latest_update = undef;
42 open(FICSLOG, ">ficslog.txt")
43 or die "ficslog.txt: $!";
44 print FICSLOG "Log starting.\n";
48 open(UCILOG, ">ucilog.txt")
49 or die "ucilog.txt: $!";
50 print UCILOG "Log starting.\n";
55 # open the chess engine
56 my $engine = open_engine($engine_cmdline, 'E1');
57 my $engine2 = open_engine($engine2_cmdline, 'E2');
60 my ($pos_waiting, $pos_calculating, $pos_calculating_second_engine);
62 uciprint($engine, "setoption name UCI_AnalyseMode value true");
63 # uciprint($engine, "setoption name NalimovPath value /srv/tablebase");
64 uciprint($engine, "setoption name NalimovUsage value Rarely");
65 uciprint($engine, "setoption name Hash value 1024");
66 # uciprint($engine, "setoption name MultiPV value 2");
67 uciprint($engine, "ucinewgame");
69 if (defined($engine2)) {
70 uciprint($engine2, "setoption name UCI_AnalyseMode value true");
71 # uciprint($engine2, "setoption name NalimovPath value /srv/tablebase");
72 uciprint($engine2, "setoption name NalimovUsage value Rarely");
73 uciprint($engine2, "setoption name Hash value 1024");
74 uciprint($engine2, "setoption name Threads value 8");
75 uciprint($engine2, "setoption name MultiPV value 500");
76 uciprint($engine2, "ucinewgame");
79 print "Chess engine ready.\n";
82 my $t = Net::Telnet->new(Timeout => 10, Prompt => '/fics% /');
83 $t->input_log(\*FICSLOG);
85 $t->print("SesseBOT");
86 $t->waitfor('/Press return to enter the server/');
90 $t->cmd("set shout 0");
91 $t->cmd("set seek 0");
92 $t->cmd("set style 12");
93 $t->cmd("observe $target");
96 print "FICS ready.\n";
100 vec($rin, fileno($engine->{'read'}), 1) = 1;
101 if (defined($engine2)) {
102 vec($rin, fileno($engine2->{'read'}), 1) = 1;
104 vec($rin, fileno($t), 1) = 1;
106 my ($nfound, $timeleft) = select($rout=$rin, undef, undef, 5.0);
110 my $line = $t->getline(Timeout => 0, errmode => 'return');
111 last if (!defined($line));
115 if ($line =~ /^<12> /) {
116 my $pos = Position->new($line);
118 # if this is already in the queue, ignore it
119 next if (defined($pos_waiting) && $pos->fen() eq $pos_waiting->fen());
121 # if we're already chewing on this and there's nothing else in the queue,
123 next if (!defined($pos_waiting) && defined($pos_calculating) &&
124 $pos->fen() eq $pos_calculating->fen());
126 # if we're already thinking on something, stop and wait for the engine
128 if (defined($pos_calculating)) {
129 if (!defined($pos_waiting)) {
130 uciprint($engine, "stop");
132 if ($uci_assume_full_compliance) {
135 uciprint($engine, "position fen " . $pos->fen());
136 uciprint($engine, "go infinite");
137 $pos_calculating = $pos;
140 # it's wrong just to give the FEN (the move history is useful,
141 # and per the UCI spec, we should really have sent "ucinewgame"),
143 uciprint($engine, "position fen " . $pos->fen());
144 uciprint($engine, "go infinite");
145 $pos_calculating = $pos;
148 if (defined($engine2)) {
149 if (defined($pos_calculating_second_engine)) {
150 uciprint($engine2, "stop");
152 uciprint($engine2, "position fen " . $pos->fen());
153 uciprint($engine2, "go infinite");
154 $pos_calculating_second_engine = $pos;
156 $engine2->{'info'} = {};
159 $engine->{'info'} = {};
163 # Output a command every move to note that we're
164 # still paying attention -- this is a good tradeoff,
165 # since if no move has happened in the last half
166 # hour, the analysis/relay has most likely stopped
167 # and we should stop hogging server resources.
171 if ($line =~ /^([A-Za-z]+)(?:\([A-Z]+\))* tells you: (.*)$/) {
172 my ($who, $msg) = ($1, $2);
174 next if (grep { $_ eq $who } (@masters) == 0);
176 if ($msg =~ /^fics (.*?)$/) {
177 $t->cmd("tell $who Executing '$1' on FICS.");
179 } elsif ($msg =~ /^uci (.*?)$/) {
180 $t->cmd("tell $who Sending '$1' to the engine.");
181 print { $engine->{'write'} } "$1\n";
183 $t->cmd("tell $who Couldn't understand '$msg', sorry.");
186 #print "FICS: [$line]\n";
190 # any fun on the UCI channel?
191 if ($nfound > 0 && vec($rout, fileno($engine->{'read'}), 1) == 1) {
192 my @lines = read_lines($engine);
193 for my $line (@lines) {
194 next if $line =~ /(upper|lower)bound/;
195 handle_uci($engine, $line, 1);
201 if (defined($engine2) && $nfound > 0 && vec($rout, fileno($engine2->{'read'}), 1) == 1) {
202 my @lines = read_lines($engine2);
203 for my $line (@lines) {
204 next if $line =~ /(upper|lower)bound/;
205 handle_uci($engine2, $line, 0);
216 my ($engine, $line, $primary) = @_;
220 $line =~ s/ / /g; # Sometimes needed for Zappa Mexico
221 print UCILOG localtime() . " $engine->{'tag'} <= $line\n";
222 if ($line =~ /^info/) {
223 my (@infos) = split / /, $line;
226 parse_infos($engine, @infos);
228 if ($line =~ /^id/) {
229 my (@ids) = split / /, $line;
232 parse_ids($engine, @ids);
234 if ($line =~ /^bestmove/) {
236 return if (!$uci_assume_full_compliance);
237 if (defined($pos_waiting)) {
238 uciprint($engine, "position fen " . $pos_waiting->fen());
239 uciprint($engine, "go infinite");
241 $pos_calculating = $pos_waiting;
242 $pos_waiting = undef;
245 $engine2->{'info'} = {};
246 my $pos = $pos_waiting // $pos_calculating;
247 uciprint($engine2, "position fen " . $pos->fen());
248 uciprint($engine2, "go infinite");
249 $pos_calculating_second_engine = $pos;
255 my ($engine, @x) = @_;
258 my $info = $engine->{'info'};
260 # Search for "multipv" first of all, since e.g. Stockfish doesn't put it first.
261 for my $i (0..$#x - 1) {
262 if ($x[$i] =~ 'multipv') {
268 while (scalar @x > 0) {
269 if ($x[0] =~ 'multipv') {
275 if ($x[0] =~ /^(currmove|currmovenumber|cpuload)$/) {
277 my $value = shift @x;
278 $info->{$key} = $value;
281 if ($x[0] =~ /^(depth|seldepth|hashfull|time|nodes|nps|tbhits)$/) {
283 my $value = shift @x;
284 $info->{$key . $mpv} = $value;
287 if ($x[0] eq 'score') {
290 delete $info->{'score_cp' . $mpv};
291 delete $info->{'score_mate' . $mpv};
293 while ($x[0] =~ /^(cp|mate|lowerbound|upperbound)$/) {
296 $info->{'score_cp' . $mpv} = shift @x;
297 } elsif ($x[0] eq 'mate') {
299 $info->{'score_mate' . $mpv} = shift @x;
307 $info->{'pv' . $mpv} = [ @x[1..$#x] ];
310 if ($x[0] eq 'string' || $x[0] eq 'UCI_AnalyseMode' || $x[0] eq 'setting' || $x[0] eq 'contempt') {
314 #print "unknown info '$x[0]', trying to recover...\n";
316 die "Unknown info '" . join(',', @x) . "'";
322 my ($engine, @x) = @_;
324 while (scalar @x > 0) {
325 if ($x[0] =~ /^(name|author)$/) {
327 my $value = join(' ', @x);
328 $engine->{'id'}{$key} = $value;
338 my ($board, @pvs) = @_;
340 if (scalar @pvs == 0 || !defined($pvs[0])) {
345 my $from_col = col_letter_to_num(substr($pv, 0, 1));
346 my $from_row = row_letter_to_num(substr($pv, 1, 1));
347 my $to_col = col_letter_to_num(substr($pv, 2, 1));
348 my $to_row = row_letter_to_num(substr($pv, 3, 1));
349 my $promo = substr($pv, 4, 1);
351 my $nb = $board->make_move($from_row, $from_col, $to_row, $to_col, $promo);
352 my $piece = $board->[$from_row][$from_col];
355 die "Invalid move $pv";
358 # white short castling
359 if ($pv eq 'e1g1' && $piece eq 'K') {
360 return ('0-0', prettyprint_pv($nb, @pvs));
363 # white long castling
364 if ($pv eq 'e1c1' && $piece eq 'K') {
365 return ('0-0-0', prettyprint_pv($nb, @pvs));
368 # black short castling
369 if ($pv eq 'e8g8' && $piece eq 'k') {
370 return ('0-0', prettyprint_pv($nb, @pvs));
373 # black long castling
374 if ($pv eq 'e8c8' && $piece eq 'k') {
375 return ('0-0-0', prettyprint_pv($nb, @pvs));
380 # check if the from-piece is a pawn
381 if (lc($piece) eq 'p') {
383 if ($from_col != $to_col) {
384 $pretty = substr($pv, 0, 1) . 'x' . substr($pv, 2, 2);
386 $pretty = substr($pv, 2, 2);
388 if (length($pv) == 5) {
391 $pretty .= uc(substr($pv, 4, 1));
394 $piece = substr($pv, 4, 1);
396 $piece = uc(substr($pv, 4, 1));
401 $pretty = uc($piece);
403 # see how many of these pieces could go here, in all
407 next unless ($board->[$row][$col] eq $piece);
408 ++$num_total if ($board->can_reach($piece, $row, $col, $to_row, $to_col));
412 # see how many of these pieces from the given row could go here
415 next unless ($board->[$from_row][$col] eq $piece);
416 ++$num_row if ($board->can_reach($piece, $from_row, $col, $to_row, $to_col));
419 # and same for columns
422 next unless ($board->[$row][$from_col] eq $piece);
423 ++$num_col if ($board->can_reach($piece, $row, $from_col, $to_row, $to_col));
426 # see if we need to disambiguate
427 if ($num_total > 1) {
429 $pretty .= substr($pv, 0, 1);
430 } elsif ($num_row == 1) {
431 $pretty .= substr($pv, 1, 1);
433 $pretty .= substr($pv, 0, 2);
438 if ($board->[$to_row][$to_col] ne '-') {
442 $pretty .= substr($pv, 2, 2);
445 if ($nb->in_mate()) {
447 } elsif ($nb->in_check() ne 'none') {
450 return ($pretty, prettyprint_pv($nb, @pvs));
456 return if (!defined($pos_calculating));
458 # Don't update too often.
459 my $age = Time::HiRes::tv_interval($latest_update);
460 if ($age < $update_max_interval) {
461 Time::HiRes::alarm($update_max_interval + 0.01 - $age);
465 my $info = $engine->{'info'};
468 # Some programs _always_ report MultiPV, even with only one PV.
469 # In this case, we simply use that data as if MultiPV was never
472 if (exists($info->{'pv1'}) && !exists($info->{'pv2'})) {
473 for my $key (qw(pv score_cp score_mate nodes nps depth seldepth tbhits)) {
474 if (exists($info->{$key . '1'})) {
475 $info->{$key} = $info->{$key . '1'};
481 # Check the PVs first. if they're invalid, just wait, as our data
482 # is most likely out of sync. This isn't a very good solution, as
483 # it can frequently miss stuff, but it's good enough for most users.
487 if (exists($info->{'pv'})) {
488 $dummy = prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv'}});
492 while (exists($info->{'pv' . $mpv})) {
493 $dummy = prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv' . $mpv}});
498 $engine->{'info'} = {};
504 $latest_update = [Time::HiRes::gettimeofday];
508 my $info = $engine->{'info'};
509 my $id = $engine->{'id'};
511 my $text = 'Analysis';
512 if ($pos_calculating->{'last_move'} ne 'none') {
513 if ($pos_calculating->{'toplay'} eq 'W') {
514 $text .= sprintf ' after %u. ... %s', ($pos_calculating->{'move_num'}-1), $pos_calculating->{'last_move'};
516 $text .= sprintf ' after %u. %s', $pos_calculating->{'move_num'}, $pos_calculating->{'last_move'};
518 if (exists($id->{'name'})) {
523 if (exists($id->{'name'})) {
524 $text .= " by $id->{'name'}:\n\n";
529 return unless (exists($pos_calculating->{'board'}));
531 if (exists($info->{'pv1'}) && exists($info->{'pv2'})) {
534 while (exists($info->{'pv' . $mpv})) {
535 $text .= sprintf " PV%2u", $mpv;
536 my $score = short_score($info, $pos_calculating, $mpv);
537 $text .= " ($score)" if (defined($score));
540 if (exists($info->{'tbhits' . $mpv}) && $info->{'tbhits' . $mpv} > 0) {
541 if ($info->{'tbhits' . $mpv} == 1) {
542 $tbhits = ", 1 tbhit";
544 $tbhits = sprintf ", %u tbhits", $info->{'tbhits' . $mpv};
548 if (exists($info->{'nodes' . $mpv}) && exists($info->{'nps' . $mpv}) && exists($info->{'depth' . $mpv})) {
549 $text .= sprintf " (%5u kn, %3u kn/s, %2u ply$tbhits)",
550 $info->{'nodes' . $mpv} / 1000, $info->{'nps' . $mpv} / 1000, $info->{'depth' . $mpv};
554 $text .= " " . join(', ', prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv' . $mpv}})) . "\n";
560 my $score = long_score($info, $pos_calculating, '');
561 $text .= " $score\n" if defined($score);
562 $text .= " PV: " . join(', ', prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv'}}));
565 if (exists($info->{'nodes'}) && exists($info->{'nps'}) && exists($info->{'depth'})) {
566 $text .= sprintf " %u nodes, %7u nodes/sec, depth %u ply",
567 $info->{'nodes'}, $info->{'nps'}, $info->{'depth'};
569 if (exists($info->{'seldepth'})) {
570 $text .= sprintf " (%u selective)", $info->{'seldepth'};
572 if (exists($info->{'tbhits'}) && $info->{'tbhits'} > 0) {
573 if ($info->{'tbhits'} == 1) {
574 $text .= ", one Syzygy hit";
576 $text .= sprintf ", %u Syzygy hits", $info->{'tbhits'};
582 #$text .= book_info($pos_calculating->fen(), $pos_calculating->{'board'}, $pos_calculating->{'toplay'});
584 my @refutation_lines = ();
585 if (defined($engine2)) {
586 for (my $mpv = 1; $mpv < 500; ++$mpv) {
587 my $info = $engine2->{'info'};
588 last if (!exists($info->{'pv' . $mpv}));
590 my $pv = $info->{'pv' . $mpv};
592 my $pretty_move = join('', prettyprint_pv($pos_calculating_second_engine->{'board'}, $pv->[0]));
593 my @pretty_pv = prettyprint_pv($pos_calculating_second_engine->{'board'}, @$pv);
594 if (scalar @pretty_pv > 5) {
595 @pretty_pv = @pretty_pv[0..4];
596 push @pretty_pv, "...";
598 my $key = $pretty_move;
599 my $line = sprintf(" %-6s %6s %3s %s",
601 short_score($info, $pos_calculating_second_engine, $mpv, 0),
602 "d" . $info->{'depth' . $mpv},
603 join(', ', @pretty_pv));
604 push @refutation_lines, [ $key, $line ];
609 if ($#refutation_lines >= 0) {
610 $text .= "Shallow search of all legal moves:\n\n";
611 for my $line (sort { $a->[0] cmp $b->[0] } @refutation_lines) {
612 $text .= $line->[1] . "\n";
617 if ($last_text ne $text) {
618 print "
\e[H
\e[2J"; # clear the screen
625 my $info = $engine->{'info'};
628 $json->{'position'} = $pos_calculating->to_json_hash();
629 $json->{'id'} = $engine->{'id'};
630 $json->{'score'} = long_score($info, $pos_calculating, '');
632 $json->{'nodes'} = $info->{'nodes'};
633 $json->{'nps'} = $info->{'nps'};
634 $json->{'depth'} = $info->{'depth'};
635 $json->{'tbhits'} = $info->{'tbhits'};
636 $json->{'seldepth'} = $info->{'seldepth'};
638 # single-PV only for now
639 $json->{'pv_uci'} = $info->{'pv'};
640 $json->{'pv_pretty'} = [ prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv'}}) ];
642 my %refutation_lines = ();
643 my @refutation_lines = ();
644 if (defined($engine2)) {
645 for (my $mpv = 1; $mpv < 500; ++$mpv) {
646 my $info = $engine2->{'info'};
647 my $pretty_move = "";
649 last if (!exists($info->{'pv' . $mpv}));
652 my $pv = $info->{'pv' . $mpv};
653 my $pretty_move = join('', prettyprint_pv($pos_calculating->{'board'}, $pv->[0]));
654 my @pretty_pv = prettyprint_pv($pos_calculating->{'board'}, @$pv);
655 $refutation_lines{$pv->[0]} = {
656 sort_key => $pretty_move,
657 depth => $info->{'depth' . $mpv},
658 score_sort_key => score_sort_key($info, $pos_calculating, $mpv, 0),
659 pretty_score => short_score($info, $pos_calculating, $mpv, 0),
660 pretty_move => $pretty_move,
661 pv_pretty => \@pretty_pv,
663 $refutation_lines{$pv->[0]}->{'pv_uci'} = $pv;
667 $json->{'refutation_lines'} = \%refutation_lines;
669 open my $fh, ">/srv/analysis.sesse.net/www/analysis.json.tmp"
671 print $fh JSON::XS::encode_json($json);
673 rename("/srv/analysis.sesse.net/www/analysis.json.tmp", "/srv/analysis.sesse.net/www/analysis.json");
677 my ($engine, $msg) = @_;
678 print { $engine->{'write'} } "$msg\n";
679 print UCILOG localtime() . " $engine->{'tag'} => $msg\n";
683 my ($info, $pos, $mpv, $invert) = @_;
686 if ($pos->{'toplay'} eq 'B') {
690 if (defined($info->{'score_mate' . $mpv})) {
692 return sprintf "M%3d", -$info->{'score_mate' . $mpv};
694 return sprintf "M%3d", $info->{'score_mate' . $mpv};
697 if (exists($info->{'score_cp' . $mpv})) {
698 my $score = $info->{'score_cp' . $mpv} * 0.01;
705 return sprintf "%+5.2f", $score;
713 my ($info, $pos, $mpv, $invert) = @_;
715 if (defined($info->{'score_mate' . $mpv})) {
717 return 99999 - $info->{'score_mate' . $mpv};
719 return -(99999 - $info->{'score_mate' . $mpv});
722 if (exists($info->{'score_cp' . $mpv})) {
723 my $score = $info->{'score_cp' . $mpv};
735 my ($info, $pos, $mpv) = @_;
737 if (defined($info->{'score_mate' . $mpv})) {
738 my $mate = $info->{'score_mate' . $mpv};
739 if ($pos->{'toplay'} eq 'B') {
743 return sprintf "White mates in %u", $mate;
745 return sprintf "Black mates in %u", -$mate;
748 if (exists($info->{'score_cp' . $mpv})) {
749 my $score = $info->{'score_cp' . $mpv} * 0.01;
751 return "Score: 0.00";
753 if ($pos->{'toplay'} eq 'B') {
756 return sprintf "Score: %+5.2f", $score;
765 my ($fen, $board, $toplay) = @_;
767 if (exists($book_cache{$fen})) {
768 return $book_cache{$fen};
771 my $ret = `./booklook $fen`;
772 return "" if ($ret =~ /Not found/ || $ret eq '');
776 for my $m (split /\n/, $ret) {
777 my ($move, $annotation, $win, $draw, $lose, $rating, $rating_div) = split /,/, $m;
781 $pmove = '(current)';
783 ($pmove) = prettyprint_pv($board, $move);
784 $pmove .= $annotation;
788 if ($toplay eq 'W') {
789 $score = 1.0 * $win + 0.5 * $draw + 0.0 * $lose;
791 $score = 0.0 * $win + 0.5 * $draw + 1.0 * $lose;
793 my $n = $win + $draw + $lose;
799 $percent = sprintf "%4u%%", int(100.0 * $score / $n + 0.5);
802 push @moves, [ $pmove, $n, $percent, $rating ];
805 @moves[1..$#moves] = sort { $b->[2] cmp $a->[2] } @moves[1..$#moves];
807 my $text = "Book moves:\n\n Perf. N Rating\n\n";
809 $text .= sprintf " %-10s %s %6u %4s\n", $m->[0], $m->[2], $m->[1], $m->[3]
816 my ($cmdline, $tag) = @_;
818 return undef if (!defined($cmdline));
820 my ($uciread, $uciwrite);
821 my $pid = IPC::Open2::open2($uciread, $uciwrite, $cmdline);
833 uciprint($engine, "uci");
838 handle_uci($engine, $_);
848 # Read until we've got a full line -- if the engine sends part of
849 # a line and then stops we're pretty much hosed, but that should
852 while ($engine->{'readbuf'} !~ /\n/) {
854 my $ret = sysread $engine->{'read'}, $tmp, 4096;
856 if (!defined($ret)) {
858 die "error in reading from the UCI engine: $!";
859 } elsif ($ret == 0) {
860 die "EOF from UCI engine";
863 $engine->{'readbuf'} .= $tmp;
868 while ($engine->{'readbuf'} =~ s/^([^\n]*)\n//) {
876 sub col_letter_to_num {
877 return ord(shift) - ord('a');
880 sub row_letter_to_num {
881 return 7 - (ord(shift) - ord('1'));