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 ($pretty, $nb) = $board->prettyprint_move($from_row, $from_col, $to_row, $to_col, $promo);
352 return ($pretty, prettyprint_pv($nb, @pvs));
358 return if (!defined($pos_calculating));
360 # Don't update too often.
361 my $age = Time::HiRes::tv_interval($latest_update);
362 if ($age < $update_max_interval) {
363 Time::HiRes::alarm($update_max_interval + 0.01 - $age);
367 my $info = $engine->{'info'};
370 # Some programs _always_ report MultiPV, even with only one PV.
371 # In this case, we simply use that data as if MultiPV was never
374 if (exists($info->{'pv1'}) && !exists($info->{'pv2'})) {
375 for my $key (qw(pv score_cp score_mate nodes nps depth seldepth tbhits)) {
376 if (exists($info->{$key . '1'})) {
377 $info->{$key} = $info->{$key . '1'};
383 # Check the PVs first. if they're invalid, just wait, as our data
384 # is most likely out of sync. This isn't a very good solution, as
385 # it can frequently miss stuff, but it's good enough for most users.
389 if (exists($info->{'pv'})) {
390 $dummy = prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv'}});
394 while (exists($info->{'pv' . $mpv})) {
395 $dummy = prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv' . $mpv}});
400 $engine->{'info'} = {};
406 $latest_update = [Time::HiRes::gettimeofday];
410 my $info = $engine->{'info'};
411 my $id = $engine->{'id'};
413 my $text = 'Analysis';
414 if ($pos_calculating->{'last_move'} ne 'none') {
415 if ($pos_calculating->{'toplay'} eq 'W') {
416 $text .= sprintf ' after %u. ... %s', ($pos_calculating->{'move_num'}-1), $pos_calculating->{'last_move'};
418 $text .= sprintf ' after %u. %s', $pos_calculating->{'move_num'}, $pos_calculating->{'last_move'};
420 if (exists($id->{'name'})) {
425 if (exists($id->{'name'})) {
426 $text .= " by $id->{'name'}:\n\n";
431 return unless (exists($pos_calculating->{'board'}));
433 if (exists($info->{'pv1'}) && exists($info->{'pv2'})) {
436 while (exists($info->{'pv' . $mpv})) {
437 $text .= sprintf " PV%2u", $mpv;
438 my $score = short_score($info, $pos_calculating, $mpv);
439 $text .= " ($score)" if (defined($score));
442 if (exists($info->{'tbhits' . $mpv}) && $info->{'tbhits' . $mpv} > 0) {
443 if ($info->{'tbhits' . $mpv} == 1) {
444 $tbhits = ", 1 tbhit";
446 $tbhits = sprintf ", %u tbhits", $info->{'tbhits' . $mpv};
450 if (exists($info->{'nodes' . $mpv}) && exists($info->{'nps' . $mpv}) && exists($info->{'depth' . $mpv})) {
451 $text .= sprintf " (%5u kn, %3u kn/s, %2u ply$tbhits)",
452 $info->{'nodes' . $mpv} / 1000, $info->{'nps' . $mpv} / 1000, $info->{'depth' . $mpv};
456 $text .= " " . join(', ', prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv' . $mpv}})) . "\n";
462 my $score = long_score($info, $pos_calculating, '');
463 $text .= " $score\n" if defined($score);
464 $text .= " PV: " . join(', ', prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv'}}));
467 if (exists($info->{'nodes'}) && exists($info->{'nps'}) && exists($info->{'depth'})) {
468 $text .= sprintf " %u nodes, %7u nodes/sec, depth %u ply",
469 $info->{'nodes'}, $info->{'nps'}, $info->{'depth'};
471 if (exists($info->{'seldepth'})) {
472 $text .= sprintf " (%u selective)", $info->{'seldepth'};
474 if (exists($info->{'tbhits'}) && $info->{'tbhits'} > 0) {
475 if ($info->{'tbhits'} == 1) {
476 $text .= ", one Syzygy hit";
478 $text .= sprintf ", %u Syzygy hits", $info->{'tbhits'};
484 #$text .= book_info($pos_calculating->fen(), $pos_calculating->{'board'}, $pos_calculating->{'toplay'});
486 my @refutation_lines = ();
487 if (defined($engine2)) {
488 for (my $mpv = 1; $mpv < 500; ++$mpv) {
489 my $info = $engine2->{'info'};
490 last if (!exists($info->{'pv' . $mpv}));
492 my $pv = $info->{'pv' . $mpv};
494 my $pretty_move = join('', prettyprint_pv($pos_calculating_second_engine->{'board'}, $pv->[0]));
495 my @pretty_pv = prettyprint_pv($pos_calculating_second_engine->{'board'}, @$pv);
496 if (scalar @pretty_pv > 5) {
497 @pretty_pv = @pretty_pv[0..4];
498 push @pretty_pv, "...";
500 my $key = $pretty_move;
501 my $line = sprintf(" %-6s %6s %3s %s",
503 short_score($info, $pos_calculating_second_engine, $mpv, 0),
504 "d" . $info->{'depth' . $mpv},
505 join(', ', @pretty_pv));
506 push @refutation_lines, [ $key, $line ];
511 if ($#refutation_lines >= 0) {
512 $text .= "Shallow search of all legal moves:\n\n";
513 for my $line (sort { $a->[0] cmp $b->[0] } @refutation_lines) {
514 $text .= $line->[1] . "\n";
519 if ($last_text ne $text) {
520 print "
\e[H
\e[2J"; # clear the screen
527 my $info = $engine->{'info'};
530 $json->{'position'} = $pos_calculating->to_json_hash();
531 $json->{'id'} = $engine->{'id'};
532 $json->{'score'} = long_score($info, $pos_calculating, '');
534 $json->{'nodes'} = $info->{'nodes'};
535 $json->{'nps'} = $info->{'nps'};
536 $json->{'depth'} = $info->{'depth'};
537 $json->{'tbhits'} = $info->{'tbhits'};
538 $json->{'seldepth'} = $info->{'seldepth'};
540 # single-PV only for now
541 $json->{'pv_uci'} = $info->{'pv'};
542 $json->{'pv_pretty'} = [ prettyprint_pv($pos_calculating->{'board'}, @{$info->{'pv'}}) ];
544 my %refutation_lines = ();
545 my @refutation_lines = ();
546 if (defined($engine2)) {
547 for (my $mpv = 1; $mpv < 500; ++$mpv) {
548 my $info = $engine2->{'info'};
549 my $pretty_move = "";
551 last if (!exists($info->{'pv' . $mpv}));
554 my $pv = $info->{'pv' . $mpv};
555 my $pretty_move = join('', prettyprint_pv($pos_calculating->{'board'}, $pv->[0]));
556 my @pretty_pv = prettyprint_pv($pos_calculating->{'board'}, @$pv);
557 $refutation_lines{$pv->[0]} = {
558 sort_key => $pretty_move,
559 depth => $info->{'depth' . $mpv},
560 score_sort_key => score_sort_key($info, $pos_calculating, $mpv, 0),
561 pretty_score => short_score($info, $pos_calculating, $mpv, 0),
562 pretty_move => $pretty_move,
563 pv_pretty => \@pretty_pv,
565 $refutation_lines{$pv->[0]}->{'pv_uci'} = $pv;
569 $json->{'refutation_lines'} = \%refutation_lines;
571 open my $fh, ">/srv/analysis.sesse.net/www/analysis.json.tmp"
573 print $fh JSON::XS::encode_json($json);
575 rename("/srv/analysis.sesse.net/www/analysis.json.tmp", "/srv/analysis.sesse.net/www/analysis.json");
579 my ($engine, $msg) = @_;
580 print { $engine->{'write'} } "$msg\n";
581 print UCILOG localtime() . " $engine->{'tag'} => $msg\n";
585 my ($info, $pos, $mpv, $invert) = @_;
588 if ($pos->{'toplay'} eq 'B') {
592 if (defined($info->{'score_mate' . $mpv})) {
594 return sprintf "M%3d", -$info->{'score_mate' . $mpv};
596 return sprintf "M%3d", $info->{'score_mate' . $mpv};
599 if (exists($info->{'score_cp' . $mpv})) {
600 my $score = $info->{'score_cp' . $mpv} * 0.01;
607 return sprintf "%+5.2f", $score;
615 my ($info, $pos, $mpv, $invert) = @_;
617 if (defined($info->{'score_mate' . $mpv})) {
619 return 99999 - $info->{'score_mate' . $mpv};
621 return -(99999 - $info->{'score_mate' . $mpv});
624 if (exists($info->{'score_cp' . $mpv})) {
625 my $score = $info->{'score_cp' . $mpv};
637 my ($info, $pos, $mpv) = @_;
639 if (defined($info->{'score_mate' . $mpv})) {
640 my $mate = $info->{'score_mate' . $mpv};
641 if ($pos->{'toplay'} eq 'B') {
645 return sprintf "White mates in %u", $mate;
647 return sprintf "Black mates in %u", -$mate;
650 if (exists($info->{'score_cp' . $mpv})) {
651 my $score = $info->{'score_cp' . $mpv} * 0.01;
653 return "Score: 0.00";
655 if ($pos->{'toplay'} eq 'B') {
658 return sprintf "Score: %+5.2f", $score;
667 my ($fen, $board, $toplay) = @_;
669 if (exists($book_cache{$fen})) {
670 return $book_cache{$fen};
673 my $ret = `./booklook $fen`;
674 return "" if ($ret =~ /Not found/ || $ret eq '');
678 for my $m (split /\n/, $ret) {
679 my ($move, $annotation, $win, $draw, $lose, $rating, $rating_div) = split /,/, $m;
683 $pmove = '(current)';
685 ($pmove) = prettyprint_pv($board, $move);
686 $pmove .= $annotation;
690 if ($toplay eq 'W') {
691 $score = 1.0 * $win + 0.5 * $draw + 0.0 * $lose;
693 $score = 0.0 * $win + 0.5 * $draw + 1.0 * $lose;
695 my $n = $win + $draw + $lose;
701 $percent = sprintf "%4u%%", int(100.0 * $score / $n + 0.5);
704 push @moves, [ $pmove, $n, $percent, $rating ];
707 @moves[1..$#moves] = sort { $b->[2] cmp $a->[2] } @moves[1..$#moves];
709 my $text = "Book moves:\n\n Perf. N Rating\n\n";
711 $text .= sprintf " %-10s %s %6u %4s\n", $m->[0], $m->[2], $m->[1], $m->[3]
718 my ($cmdline, $tag) = @_;
720 return undef if (!defined($cmdline));
722 my ($uciread, $uciwrite);
723 my $pid = IPC::Open2::open2($uciread, $uciwrite, $cmdline);
735 uciprint($engine, "uci");
740 handle_uci($engine, $_);
750 # Read until we've got a full line -- if the engine sends part of
751 # a line and then stops we're pretty much hosed, but that should
754 while ($engine->{'readbuf'} !~ /\n/) {
756 my $ret = sysread $engine->{'read'}, $tmp, 4096;
758 if (!defined($ret)) {
760 die "error in reading from the UCI engine: $!";
761 } elsif ($ret == 0) {
762 die "EOF from UCI engine";
765 $engine->{'readbuf'} .= $tmp;
770 while ($engine->{'readbuf'} =~ s/^([^\n]*)\n//) {
778 sub col_letter_to_num {
779 return ord(shift) - ord('a');
782 sub row_letter_to_num {
783 return 7 - (ord(shift) - ord('1'));