Initial commit of GNU Go v3.8.
[sgk-go] / interface / gtp_examples / twogtp.pike
CommitLineData
7eeb782e
AT
1#! /usr/bin/env pike
2
3/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
4 * This is GNU Go, a Go program. Contact gnugo@gnu.org, or see *
5 * http://www.gnu.org/software/gnugo/ for more information. *
6 * *
7 * Copyright 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008 and *
8 * 2009 by the Free Software Foundation. *
9 * *
10 * This program is free software; you can redistribute it and/or *
11 * modify it under the terms of the GNU General Public License as *
12 * published by the Free Software Foundation - version 3 or *
13 * (at your option) any later version. *
14 * *
15 * This program is distributed in the hope that it will be useful, *
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
18 * GNU General Public License in file COPYING for more details. *
19 * *
20 * You should have received a copy of the GNU General Public *
21 * License along with this program; if not, write to the Free *
22 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, *
23 * Boston, MA 02111, USA. *
24\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
25
26#define DUMP_GTP_PIPES 0
27
28
29int
30maximal_fixed_handicap(int board_size)
31{
32 if (board_size < 7)
33 return 0;
34 if (board_size % 2 == 1 && board_size >= 9)
35 return 9;
36 return 4;
37}
38
39
40float
41result_to_float(string result)
42{
43 if (result[..1] == "W+")
44 return (float) result[2..];
45 if (result[..1] == "B+")
46 return - (float) result[2..];
47 return (float) result;
48}
49
50
51string
52arrange_values_nicely(array(string) values, string delimiter)
53{
54 string result = "";
55 for (int k = 0; k < sizeof(values); k++) {
56 if (k > 0 && k % 12 == 0)
57 result += "\n";
58 result += delimiter + values[k];
59 }
60
61 return result + "\n";
62}
63
64
65string
66list_sgf_positions(array(array(string)) positions, array(string) sgf_properties)
67{
68 string result = "";
69 for (int k = 0; k < sizeof(positions); k++) {
70 if (sizeof(positions[k]))
71 result += sgf_properties[k] + arrange_values_nicely(positions[k], "");
72 }
73
74 return result;
75}
76
77
78string
79nice_time(float _time)
80{
81 int time = (int) (_time * 10);
82#ifdef __AUTO_BIGNUM__
83 return sprintf("%d:%02d.%d", time / 600, (time % 600) / 10, time % 10);
84#else
85 return sprintf("%d:%02d", time / 600, (time % 600) / 10);
86#endif
87}
88
89
90int
91is_numeric(string str)
92{
93 str = String.trim_all_whites(str);
94 if (str[0] == '+')
95 str = str[1..];
96
97 return ((string) ((int) str)) == str;
98}
99
100
101class GtpServer {
102 int server_is_up;
103 int board_size;
104 private Stdio.File file_out;
105 private Stdio.FILE file_in;
106 int protocol_version;
107 string color;
108 string capitalized_color;
109 string command_line;
110 string full_engine_name;
111 string random_seed;
112 private float main_time;
113 private float byo_yomi_time;
114 private int byo_yomi_stones;
115 private float time_left;
116 private int stones_left;
117 float total_used_time;
118 private array(string) statistics;
119 private array(string) reset;
120 private array totals;
121
122
123 void
124 create(string _command_line, array(string) _statistics, array(string) _reset,
125 string _color,
126 float _main_time, float _byo_yomi_time, int _byo_yomi_stones)
127 {
128 file_out = Stdio.File();
129 file_in = Stdio.FILE();
130 set_color(_color);
131 command_line = _command_line;
132
133 array error = catch {
134 Process.create_process(command_line / " ",
135 ([ "stdin" : file_out->pipe(),
136 "stdout" : file_in->pipe() ]));
137 };
138
139 if (error) {
140 werror(error[0]);
141 werror("Command line was `%s'.\n", command_line);
142 destruct(this_object());
143 }
144 else {
145 array error = catch {
146 array answer = send_command("protocol_version");
147 protocol_version = answer[0] ? 1 : (int) answer[1];
148 full_engine_name = get_full_engine_name();
149 server_is_up = 1;
150 };
151 if (error) {
152 werror("Engine `%s' crashed at startup.\nPerhaps command line is wrong.\n",
153 command_line);
154 destruct(this_object());
155 }
156 else {
157 main_time = _main_time;
158 byo_yomi_time = _byo_yomi_time;
159 byo_yomi_stones = _byo_yomi_stones;
160 total_used_time = 0.0;
161
162 statistics = _statistics;
163 reset = _reset;
164 totals = ({ 0 }) * sizeof(statistics);
165 }
166 }
167 }
168
169
170 void
171 set_color(string _color)
172 {
173 color = _color;
174 capitalized_color = String.capitalize(color);
175 }
176
177
178 void
179 restart_if_crashed()
180 {
181 if (!server_is_up) {
182 werror("Restarting engine `%s' playing %s.\n", command_line, color);
183 Process.create_process(command_line / " ",
184 ([ "stdin" : file_out->pipe(),
185 "stdout" : file_in->pipe() ]));
186 server_is_up = 1;
187 }
188 }
189
190
191 array
192 send_command(string command)
193 {
194#if DUMP_GTP_PIPES
195 werror("[%s%s] %s\n",
196 full_engine_name ? full_engine_name + ", " : "", color, command);
197#endif
198
199 command = String.trim_all_whites(command);
200 sscanf(command, "%[0-9]", string id);
201 if (command[0] == '#' || command == id)
202 return ({ 0, "" });
203
204 file_out->write("%s\n", command);
205 string response = file_in->gets();
206 if (!response) {
207 server_is_up = 0;
208 error("Engine `%s' playing %s crashed!", command_line, color);
209 }
210
211#if DUMP_GTP_PIPES
212 werror("%s\n", response);
213#endif
214
215 array result;
216 int id_length = strlen(id);
217 if (response && response[..id_length] == "=" + id)
218 result = ({ 0, response[id_length + 1 ..] });
219 else if (response && response[..id_length] == "?" + id)
220 result = ({ 1, response[id_length + 1 ..] });
221 else
222 result = ({ -1, response });
223
224 result[1] = String.trim_all_whites(result[1]);
225 while (1) {
226 response = file_in->gets();
227
228#if DUMP_GTP_PIPES
229 werror("%s\n", response);
230#endif
231
232 if (response == "") {
233 if (result[0] < 0) {
234 werror("Warning, unrecognized response to command `%s':\n", command);
235 werror("%s\n", result[1]);
236 }
237 return result;
238 }
239 result[1] += "\n" + response;
240 }
241 }
242
243
244 int
245 is_known_command(string command)
246 {
247 array answer = send_command("known_command " + command);
248 return !answer[0] && answer[1] == "true";
249 }
250
251
252 string
253 get_full_engine_name()
254 {
255 return send_command("name")[1] + " " + send_command("version")[1];
256 }
257
258
259 void
260 set_board_size(int _board_size)
261 {
262 board_size = _board_size;
263 send_command("boardsize " + board_size);
264 if (protocol_version >= 2)
265 send_command("clear_board");
266 random_seed = get_random_seed();
267 }
268
269
270 array(string)
271 fixed_handicap(int handicap)
272 {
273 array answer = send_command("fixed_handicap " + handicap);
274 return answer[0] ? ({}) : answer[1] / " ";
275 }
276
277
278 array(string)
279 place_free_handicap(int handicap)
280 {
281 array answer = send_command("place_free_handicap " + handicap);
282 return answer[0] ? ({}) : answer[1] / " ";
283 }
284
285
286 void
287 set_free_handicap(array(string) stones)
288 {
289 send_command("set_free_handicap " + (stones * " "));
290 }
291
292
293 array(string)
294 set_handicap(int handicap, string handicap_mode,
295 GtpServer opponent, GtpServer arbiter)
296 {
297 if (handicap == 0)
298 return ({});
299
300 if (handicap_mode == "free") {
301 array(string) stones = place_free_handicap(handicap);
302 opponent->set_free_handicap(stones);
303 if (arbiter)
304 arbiter->set_free_handicap(stones);
305
306 return stones;
307 }
308 else {
309 opponent->fixed_handicap(handicap);
310 if (arbiter)
311 arbiter->fixed_handicap(handicap);
312
313 return fixed_handicap(handicap);
314 }
315 }
316
317
318 float
319 get_komi()
320 {
321 return (float) (send_command("get_komi")[1]);
322 }
323
324
325 void
326 set_komi(float komi)
327 {
328 send_command("komi " + komi);
329 }
330
331
332 array
333 load_sgf(string sgf_file_name, int|string load_up_to)
334 {
335 array answer = send_command(sprintf("loadsgf %s %s", sgf_file_name,
336 (string) load_up_to));
337 board_size = (int) send_command("query_boardsize")[1];
338 return answer;
339 }
340
341
342 void
343 reset_engine()
344 {
345 foreach (reset, string reset_command)
346 send_command(reset_command);
347
348 initialize_time_control();
349 }
350
351
352 void
353 initialize_time_control()
354 {
355 if (main_time < 0.0) {
356 time_left = 0.0;
357 return;
358 }
359
360 if (main_time > 0.0) {
361 time_left = main_time;
362 stones_left = 0;
363 }
364 else {
365 time_left = byo_yomi_time;
366 stones_left = byo_yomi_stones;
367 }
368
369 send_command(sprintf("time_settings %d %d %d", (int) main_time,
370 (int) byo_yomi_time, byo_yomi_stones));
371 }
372
373
374 string
375 generate_move(int use_time_control)
376 {
377 string result;
378 int time_notch = 0;
379
380 if (use_time_control) {
381 if (main_time >= 0.0) {
382 send_command(sprintf("time_left %s %d %d", color,
383 (int) time_left, stones_left));
384 }
385
386#ifdef __AUTO_BIGNUM__
387 time_notch = gethrtime();
388#else
389 time_notch = time();
390#endif
391 }
392
393 if (protocol_version >= 2)
394 result = send_command("genmove " + color)[1];
395 else
396 result = send_command("genmove_" + color)[1];
397
398 if (use_time_control) {
399#ifdef __AUTO_BIGNUM__
400 time_left -= (gethrtime() - time_notch) / 1.0e6;
401#else
402 time_left -= time() - time_notch;
403#endif
404 if (main_time >= 0.0) {
405 if (time_left < 0.0) {
406 if (stones_left > 0)
407 return "time";
408 else {
409 total_used_time += main_time;
410 time_left += byo_yomi_time;
411 stones_left = byo_yomi_stones;
412 if (time_left < 0.0)
413 return "time";
414 }
415 }
416
417 if (stones_left > 0 && --stones_left == 0) {
418 total_used_time += byo_yomi_time - time_left;
419 time_left = byo_yomi_time;
420 stones_left = byo_yomi_stones;
421 }
422 }
423 }
424
425 return result;
426 }
427
428
429 string
430 get_time_left()
431 {
432 if (main_time < 0.0)
433 return "";
434
435 if (time_left < 0)
436 return "lost on time";
437
438 if (stones_left <= 0) {
439 return sprintf("main time: %s / %s",
440 nice_time(time_left), nice_time(main_time));
441 }
442
443 return sprintf("byo-yomi time: %s / %s, stones: %d / %d",
444 nice_time(time_left), nice_time(byo_yomi_time),
445 stones_left, byo_yomi_stones);
446 }
447
448
449 void
450 finalize_time_control()
451 {
452 if (main_time < 0.0)
453 total_used_time += -time_left;
454 else if (stones_left > 0)
455 total_used_time += byo_yomi_time - time_left;
456 else
457 total_used_time += main_time - time_left;
458 }
459
460
461 void
462 play(string color, string move)
463 {
464 if (protocol_version >= 2)
465 send_command(sprintf("play %s %s", color, move));
466 else
467 send_command(sprintf("%s %s", color, move));
468 }
469
470
471 string
472 get_random_seed()
473 {
474 array answer = send_command("get_random_seed");
475 return answer[0] ? "unknown" : answer[1];
476 }
477
478
479 void
480 set_random_seed(int|string seed)
481 {
482 random_seed = (string) seed;
483 send_command("set_random_seed " + random_seed);
484 }
485
486
487 array(string)
488 list_stones(string color)
489 {
490 return send_command("list_stones " + color)[1] / " ";
491 }
492
493
494 array(array(string))
495 get_position_as_sgf()
496 {
497 if (!is_known_command("list_stones"))
498 return ({});
499 return ({ map(list_stones("white"), move_to_sgf_notation),
500 map(list_stones("black"), move_to_sgf_notation) });
501 }
502
503
504 array(string)
505 final_status_list(string status)
506 {
507 array result = send_command("final_status_list " + status);
508 return result[0] ? ({}) : ((result[1] / "\n") * " ") / " " - ({""});
509 }
510
511
512 array(array(string))
513 get_territory_as_sgf()
514 {
515 if (!is_known_command("final_status_list"))
516 return ({});
517
518 array(array(string)) result
519 = ({ map(final_status_list("white_territory"), move_to_sgf_notation),
520 map(final_status_list("black_territory"), move_to_sgf_notation) });
521 if (result[0] == ({}) && result[1] == ({}))
522 return ({});
523
524 if (is_known_command("color")) {
525 array(string) dead_stones = final_status_list("dead");
526 foreach (dead_stones, string stone) {
527 switch (send_command("color " + stone)[1]) {
528 case "black": result[0] += ({ move_to_sgf_notation(stone) }); break;
529 case "white": result[1] += ({ move_to_sgf_notation(stone) }); break;
530 }
531 }
532 }
533
534 return result;
535 }
536
537
538 string
539 show_board()
540 {
541 array answer = send_command("showboard");
542 if (answer[0])
543 return "\n";
544 if (answer[1] != "" && answer[1][0] == '\n')
545 return answer[1][1..] + "\n";
546 return answer[1] + "\n";
547 }
548
549
550 void
551 print_statistics(int dont_print_numerics)
552 {
553 for (int k = 0; k < sizeof(statistics); k++) {
554 array command_result = send_command(statistics[k]);
555 if (!command_result[0]) {
556 if (is_numeric(command_result[1])) {
557 if (totals[k] != "")
558 totals[k] += (int) command_result[1];
559
560 if (dont_print_numerics)
561 continue;
562 }
563 else
564 totals[k] = "";
565
566 write("%s (%s) statistic `%s': %s\n", full_engine_name, color,
567 statistics[k], command_result[1]);
568 }
569 else {
570 werror("Couldn't acquire statistic `%s': engine failed with message \"%s\"\n",
571 statistics[k], command_result[1]);
572 }
573 }
574 }
575
576
577 void
578 print_statistic_totals()
579 {
580 int first_total = 1;
581 for (int k = 0; k < sizeof(statistics); k++) {
582 if (totals[k] != "") {
583 if (first_total) {
584 write("\n%s (%s) statistics totals:\n", full_engine_name, color);
585 first_total = 0;
586 }
587
588 write("`%s' total: %d\n", statistics[k], totals[k]);
589 }
590 }
591 }
592
593
594 string
595 final_score()
596 {
597 array answer = send_command("final_score");
598 return answer[0] ? "?" : answer[1];
599 }
600
601
602 string
603 cpu_time()
604 {
605 if (is_known_command("cputime"))
606 return send_command("cputime")[1];
607 return "";
608 }
609
610
611 void
612 quit()
613 {
614 send_command("quit");
615 }
616
617
618 string
619 move_to_sgf_notation(string coordinates)
620 {
621 coordinates = lower_case(coordinates);
622 if (coordinates == "pass")
623 return "[]";
624
625 int y = board_size - ((int) coordinates[1..]);
626 int x = coordinates[0] - 'a';
627 if (x > 'i' - 'a')
628 x--;
629
630 return sprintf("[%c%c]", 'a' + x, 'a' + y);
631 }
632};
633
634
635class GtpGame {
636 private GtpServer white;
637 private GtpServer black;
638 private GtpServer arbiter;
639 private int verbose;
640 private int black_to_play;
641 private string sgf_header;
642 private array(array(string)) territory;
643 private int totals_only;
644
645
646 void
647 create(string command_line_white, string command_line_black,
648 string command_line_arbiter,
649 array(string) statistics_white, array(string) statistics_black,
650 array(string) reset_white, array(string) reset_black,
651 int _totals_only, int _verbose,
652 float main_time, float byo_yomi_time, int byo_yomi_stones)
653 {
654 verbose = _verbose;
655 totals_only = _totals_only;
656 white = GtpServer(command_line_white, statistics_white, reset_white,
657 "white", main_time, byo_yomi_time, byo_yomi_stones);
658 if (white) {
659 black = GtpServer(command_line_black, statistics_black, reset_black,
660 "black", main_time, byo_yomi_time, byo_yomi_stones);
661
662 if (black && command_line_arbiter != "") {
663 arbiter = GtpServer(command_line_arbiter, ({}), ({}), "arbiter",
664 -1.0, 0.0, 0);
665 }
666 else
667 arbiter = UNDEFINED;
668 }
669
670 if (!white || !black || (command_line_arbiter != "" && !arbiter))
671 destruct(this_object());
672 }
673
674
675 void
676 swap_engines()
677 {
678 GtpServer temp = white;
679 white = black;
680 black = temp;
681 white->set_color("white");
682 black->set_color("black");
683 }
684
685
686 void
687 start_new_game(int board_size, int handicap, string handicap_mode,
688 float komi)
689 {
690 white->set_board_size(board_size);
691 black->set_board_size(board_size);
692 if (arbiter)
693 arbiter->set_board_size(board_size);
694
695 array(string) stones = black->set_handicap(handicap, handicap_mode,
696 white, arbiter);
697 stones = map(stones, black->move_to_sgf_notation);
698
699 white->set_komi(komi);
700 black->set_komi(komi);
701 if (arbiter)
702 arbiter->set_komi(komi);
703
704 black_to_play = (handicap == 0);
705 sgf_header = sprintf("(;\nGM[1]FF[4]\nSZ[%d]HA[%d]KM[%.1f]\n",
706 board_size, handicap, komi);
707 if (handicap)
708 sgf_header += "AB" + arrange_values_nicely(stones, "");
709 }
710
711
712 array(string)
713 play(string|int sgf_file_name)
714 {
715 array(string) sgf_moves = ({});
716 array(array(string)) move_history = ({});
717 string special_win = "";
718 GtpServer player;
719 GtpServer opponent;
720
721 if (verbose)
722 werror("\nBeginning a new game.\n");
723
724 white->reset_engine();
725 black->reset_engine();
726 territory = UNDEFINED;
727
728 array error = catch {
729 int passes = 0;
730 while (1) {
731 player = black_to_play ? black : white;
732 opponent = black_to_play ? white : black;
733
734 string move = player->generate_move(1);
735 string move_lower_case = lower_case(move);
736
737 if (move_lower_case == "resign") {
738 if (verbose)
739 werror(player->capitalized_color + " resigns!\n");
740
741 special_win = sprintf("%c+Resign", opponent->capitalized_color[0]);
742 break;
743 }
744
745 if (move_lower_case == "time") {
746 if (verbose)
747 werror(player->capitalized_color + " loses on time!\n");
748
749 special_win = sprintf("%c+Time", opponent->capitalized_color[0]);
750 break;
751 }
752
753 opponent->play(player->color, move);
754 sgf_moves += ({ sprintf("%c%s", player->capitalized_color[0],
755 player->move_to_sgf_notation(move)) });
756 if (arbiter)
757 move_history += ({ ({ player->color, move }) });
758
759 if (move_lower_case == "pass") {
760 if (verbose)
761 werror("play " + player->capitalized_color + " pass\n");
762
763 if (++passes == 2)
764 break;
765 }
766 else {
767 if (verbose) {
768 string time_left = " (" + player->get_time_left() + ")";
769 if (time_left == " ()")
770 time_left = "";
771
772 werror("play %s %s%s\n", player->capitalized_color,
773 move, time_left);
774 }
775
776 passes = 0;
777 }
778
779 if (verbose >= 2) {
780 string board = white->show_board();
781 if (board == "")
782 board = black->show_board();
783 werror("%s\n", board);
784 }
785 black_to_play = !black_to_play;
786
787 if (sgf_file_name)
788 write_sgf_file(sgf_file_name, sgf_moves, ({ "Void" }));
789 }
790 };
791
792 white->finalize_time_control();
793 black->finalize_time_control();
794
795 array(string) result;
796 if (error) {
797 result = ({ "Void", error[0] });
798 if (sgf_file_name)
799 werror("The game will be saved in file `%s'.\n", sgf_file_name);
800
801 white->restart_if_crashed();
802 black->restart_if_crashed();
803 if (arbiter)
804 arbiter->restart_if_crashed();
805 }
806 else {
807 if (special_win == "") {
808 result = ({ white->final_score(), black->final_score() });
809
810 if (result[1] == "?" || result[0] == result[1])
811 result = ({ result[0] });
812 else if (result[0] == "?")
813 result = ({ result[1] });
814
815 territory = player->get_territory_as_sgf();
816 if (territory == ({}))
817 territory = opponent->get_territory_as_sgf();
818
819 if (arbiter && (sizeof(result) == 2 || result[0] == "?")) {
820 foreach (move_history, array(string) move)
821 arbiter->play(move[0], move[1]);
822 result = ({ arbiter->final_score() });
823
824 if (territory == ({}))
825 territory = arbiter->get_territory_as_sgf();
826 }
827 }
828 else
829 result = ({ special_win });
830 }
831
832 if (sgf_file_name)
833 write_sgf_file(sgf_file_name, sgf_moves, result);
834 return result;
835 }
836
837
838 int
839 init_endgame_contest(string endgame_file_name, int endgame_moves)
840 {
841 array(string) sgf_nodes;
842 array error = catch {
843 sgf_nodes = Stdio.read_file(endgame_file_name) / ";";
844 };
845 if (error) {
846 werror(error[0]);
847 return 0;
848 }
849
850 Regexp move = Regexp("\\<([BW]\\[[a-z][a-z]\\])");
851 array(string) moves = ({});
852 foreach (sgf_nodes, string sgf_node) {
853 array move_groups = move->split(sgf_node);
854 if (move_groups)
855 moves += ({ move_groups[0] });
856 }
857
858 int load_up_to = max(sizeof(moves) - endgame_moves + 1, 1);
859 array white_answer = white->load_sgf(endgame_file_name, load_up_to);
860 array black_answer = black->load_sgf(endgame_file_name, load_up_to);
861 array arbiter_answer = (arbiter
862 ? arbiter->load_sgf(endgame_file_name, load_up_to)
863 : ({0}));
864
865 if (white_answer[0] || black_answer[0] || arbiter_answer[0]) {
866 werror("File `%s' might be corrupt. Engines refuse to load it.\n",
867 endgame_file_name);
868 return 0;
869 }
870
871 sgf_header = sprintf("(;\nGM[1]FF[4]\nSZ[%d]KM[%.1f]\n"
872 "C[Original game: `%s' loaded up to move %d]\n",
873 white->board_size, white->get_komi(),
874 endgame_file_name, load_up_to);
875 array(array(string)) stones = white->get_position_as_sgf();
876 if (!stones)
877 stones = black->get_position_as_sgf();
878 if (!stones && arbiter)
879 stones = arbiter->get_position_as_sgf();
880 if (!stones) {
881 stones = ({ ({}), ({}) });
882 moves = moves[..load_up_to - 2];
883 foreach (moves, string move)
884 stones[move[0] == 'B'] += ({ move[1..] });
885 }
886
887 sgf_header += list_sgf_positions(stones, ({ "AW", "AB" }));
888
889 white->set_random_seed(0);
890 black->set_random_seed(0);
891
892 black_to_play = (black_answer[1] == "black");
893 return load_up_to;
894 }
895
896
897 void
898 reinit_endgame_contest(string endgame_file_name, int load_up_to)
899 {
900 swap_engines();
901 white->load_sgf(endgame_file_name, load_up_to);
902 array black_answer = black->load_sgf(endgame_file_name, load_up_to);
903
904 if (arbiter)
905 arbiter->load_sgf(endgame_file_name, load_up_to);
906
907 white->set_random_seed(0);
908 black->set_random_seed(0);
909 black_to_play = (black_answer[1] == "black");
910 }
911
912
913 void
914 write_sgf_file(string sgf_file_name, array(string) sgf_moves,
915 array(string) result)
916 {
917 string sgf_data = sprintf("PW[%s (random seed %s)]\nPB[%s (random seed %s)]\n"
918 "RU[Japanese]\nRE[%s]\n",
919 white->full_engine_name, white->random_seed,
920 black->full_engine_name, black->random_seed,
921 result[0]);
922 if (sizeof(result) > 1) {
923 sgf_data += sprintf("GC[%s]\n", result[0] == "Void" ? result[1] :
924 sprintf("Engines disagreed on results:\n"
925 "White claimed %s\nBlack claimed %s\n",
926 result[0], result[1]));
927 }
928
929 sgf_data += arrange_values_nicely(sgf_moves, ";");
930 if (sizeof(result) == 1) {
931 if (territory)
932 sgf_data += ";" + list_sgf_positions(territory, ({ "TW", "TB" }));
933 }
934 else if (result[0] == "Void")
935 sgf_data += ";C[" + result[1] + "]\n";
936 sgf_data += ")\n";
937
938 array error = catch {
939 Stdio.write_file(sgf_file_name, sgf_header + sgf_data);
940 };
941
942 if (error)
943 werror(error[0]);
944 }
945
946
947 void
948 print_time_report()
949 {
950 string cpu_time_white = white->cpu_time();
951 string cpu_time_black = black->cpu_time();
952 if (cpu_time_white != "")
953 write("White: %ss CPU time.\n", cpu_time_white);
954 if (cpu_time_black != "")
955 write("Black: %ss CPU time.\n", cpu_time_black);
956
957 write("\nTime control report (wall move generation time):\n");
958 write(" White: %s\n", nice_time(white->total_used_time));
959 write(" Black: %s\n", nice_time(black->total_used_time));
960 }
961
962
963 void
964 print_statistics()
965 {
966 white->print_statistics(totals_only);
967 black->print_statistics(totals_only);
968 }
969
970
971 void
972 print_statistic_totals()
973 {
974 white->print_statistic_totals();
975 black->print_statistic_totals();
976 }
977
978
979 void
980 finalize()
981 {
982 white->quit();
983 black->quit();
984 }
985}
986
987
988void
989run_twogtp_match(GtpGame game, int num_games, int board_size, int handicap,
990 string handicap_mode, int adjust_handicap, float komi,
991 int verbose, string|int sgf_base, int skip_games)
992{
993 int white_wins = 0;
994 int black_wins = 0;
995 int jigos = 0;
996 int result_unknown = 0;
997 int disagreed_games = 0;
998 int last_streak = 0;
999 int last_to_win = '0';
1000 array(array(string)) results = ({});
1001
1002 for (int k = skip_games; k < skip_games + num_games; k++) {
1003 game->start_new_game(board_size, handicap, handicap_mode, komi);
1004 array(string) result
1005 = game->play(sgf_base ? sprintf("%s%03d.sgf", sgf_base, k + 1) : 0);
1006
1007 write("Game %d: %s\n", k + 1, result * " ");
1008 game->print_statistics();
1009 results += ({result});
1010
1011 if (sizeof(result) == 1 || (result[0][0] == result[1][0])) {
1012 switch (result[0][0]) {
1013 case 'W':
1014 case 'B':
1015 if (result[0][0] == 'W')
1016 white_wins++;
1017 else
1018 black_wins++;
1019
1020 if (result[0][0] == last_to_win)
1021 last_streak++;
1022 else {
1023 last_to_win = result[0][0];
1024 last_streak = 1;
1025 }
1026 break;
1027
1028 case '0': jigos++; break;
1029
1030 default:
1031 result_unknown++;
1032 last_to_win = '0';
1033 }
1034 }
1035 else {
1036 result_unknown++;
1037 last_to_win = '0';
1038 }
1039
1040 if (adjust_handicap && last_streak == adjust_handicap) {
1041 if (result[0][0] == 'W') {
1042 if (handicap_mode == "free"
1043 || handicap < maximal_fixed_handicap(board_size)) {
1044 if (++handicap == 1)
1045 handicap = 2;
1046 write("White wins too often. Increasing handicap to %d.\n", handicap);
1047 }
1048 }
1049 else {
1050 if (handicap == 0) {
1051 handicap = 2;
1052 if (handicap_mode == "fixed")
1053 handicap = min(maximal_fixed_handicap(board_size), 2);
1054 game->swap_engines();
1055 write("Black looks stronger than white. Swapping colors and setting handicap to %d.\n",
1056 handicap);
1057 }
1058 else {
1059 if (--handicap == 1)
1060 handicap = 0;
1061 write("Black wins too often. Decreasing handicap to %d.\n",
1062 handicap);
1063 }
1064 }
1065
1066 last_streak = 0;
1067 }
1068
1069 if (sizeof(result) == 2 && result[0] != "Void")
1070 disagreed_games++;
1071 }
1072
1073 if (verbose) {
1074 write("\n");
1075 for (int k = 0; k < num_games; k++)
1076 write("Game %d: %s\n", skip_games + k + 1, results[k] * " ");
1077 }
1078
1079 write("\nTotal %d game(s).\nWhite won %d. Black won %d.",
1080 num_games, white_wins, black_wins);
1081 if (jigos)
1082 write(" %d jigos.", jigos);
1083 if (result_unknown)
1084 write(" Results of %d game(s) are unknown.", result_unknown);
1085 write("\n");
1086 if (disagreed_games)
1087 write("Engines disagreed on results of %d game(s).\n", disagreed_games);
1088
1089 game->print_time_report();
1090 game->print_statistic_totals();
1091 game->finalize();
1092}
1093
1094
1095void
1096endgame_contest(GtpGame game, int endgame_moves, array(string) endgame_files,
1097 int verbose, string|int sgf_base, int skip_games)
1098{
1099 array(string) differences = ({});
1100 for (int k = skip_games; k < sizeof(endgame_files); k++) {
1101 int load_up_to = game->init_endgame_contest(endgame_files[k], endgame_moves);
1102 if (load_up_to) {
1103 if (verbose)
1104 werror("Replaying game `%s'.\n", endgame_files[k]);
1105
1106 array(string) result1
1107 = game->play(sgf_base ? sprintf("%s%03d_1.sgf", sgf_base, k + 1) : 0);
1108 game->print_statistics();
1109 game->reinit_endgame_contest(endgame_files[k], load_up_to);
1110
1111 array(string) result2
1112 = game->play(sgf_base ? sprintf("%s%03d_2.sgf", sgf_base, k + 1) : 0);
1113 game->print_statistics();
1114 game->swap_engines();
1115
1116 write("%s: ", endgame_files[k]);
1117 if (sizeof(result1) > 1 || sizeof(result2) > 1) {
1118 write("can't determine difference, engines disagreed on results.\n");
1119 write("\t%s\n", result1 * " ");
1120 write("\t%s\n", result2 * " ");
1121 differences += ({ "unknown" });
1122 }
1123 else {
1124 string difference = sprintf("%+.1f", (result_to_float(result1[0])
1125 - result_to_float(result2[0])));
1126 if (difference == "+0.0") {
1127 write("same result: %s\n", result1[0]);
1128 differences += ({ "0" });
1129 }
1130 else {
1131 write("%s %s; difference %s.\n", result1[0], result2[0], difference);
1132 differences += ({ difference });
1133 }
1134 }
1135 }
1136 }
1137
1138 int white_wins = 0;
1139 int black_wins = 0;
1140 write("\n");
1141 foreach (differences, string difference) {
1142 write(difference + "\n");
1143 if (difference[0] == '+')
1144 white_wins++;
1145 else if (difference[0] == '-')
1146 black_wins++;
1147 }
1148
1149 write("\nTotal %d game(s) replayed. White won %d. Black won %d.\n",
1150 sizeof(differences), white_wins, black_wins);
1151 game->print_time_report();
1152 game->print_statistic_totals();
1153 game->finalize();
1154}
1155
1156
1157string help_message =
1158 "Usage: %s [OPTION]... [FILE]...\n\n"
1159 "Runs either a match or endgame contest between two GTP engines.\n"
1160 "`--white' and `--black' options are mandatory.\n\n"
1161 "Options:\n"
1162 " -w, --white=COMMAND_LINE\n"
1163 " -b, --black=COMMAND_LINE command lines to run the two engines with.\n\n"
1164 " -A, --arbiter=COMMAND_LINE command line to run arbiter--program that will\n"
1165 " score disputed games--with.\n"
1166 " --help display this help and exit.\n"
1167 " --help-statistics display help on statistics options and exit.\n"
1168 " -v, --verbose=LEVEL 1 - print moves, 2 and higher - draw boards.\n"
1169 " --no-sgf do not create SGF game recods.\n"
1170 " --sgf-base=FILENAME create SGF files with FILENAME as base (default\n"
1171 " is `twogtp' or `endgame' depending on mode).\n"
1172 " -m, --match runs a match between the engines (the default).\n"
1173 " -e, --endgame=MOVES runs an endgame contest instead of a match.\n"
1174 " -c, --continue continue a match or endgame contest.\n\n"
1175 "Options valid only in match mode:\n"
1176 " -g, --games=GAMES number of games in the match (one by default).\n"
1177 " -s, --board-size=SIZE the board size for the match (default is 19).\n"
1178 " -h, --handicap=STONES fixed handicap for the black player.\n"
1179 " -f, --free-handicap=STONES free handicap for the black player.\n"
1180 " -a, --adjust-handicap=LENGTH use simple adjusting scheme: change handicap\n"
1181 " by 1 after LENGTH wins in a row.\n"
1182 " -k, --komi=KOMI the komi to use.\n\n"
1183 "Time control options:\n"
1184 " -t, --main-time=TIME main time for a game (default is forever with\n"
1185 " no byo-yomi or zero otherwise).\n"
1186 " -B, --byo-yomi-time=TIME byo-yomi time for a game (zero by default).\n"
1187 " TIMEs are in minutes and can be fractional.\n"
1188 " -S, --byo-yomi-stones=STONES stones to be played in a byo-yomi period\n"
1189 " (default is 25).\n\n"
1190 "Default is no handicap. Komi defaults to 5.5 with no handicap or 0.5 with\n"
1191 "nonzero handicap. Note that `--adjust-handicap' option not only can change\n"
1192 "handicap, but can also swap engines' colors if black appears stronger.\n\n"
1193 "FILEs are only used in endgame contest mode. They must be non-branched SGF\n"
1194 "game records. In endgame contest mode the FILEs are loaded into the engines\n"
1195 "excluding last non-pass MOVES specified with `--endgame' option. For each of\n"
1196 "the FILEs two games are played with alternating colors and the difference in\n"
1197 "results is determined.\n\n"
1198 "Option `--continue' allows to have a continuous set of game records for\n"
1199 "several script runs. It restarts a match or endgame contest skipping all\n"
1200 "games for which game records already exist. In case of an endgame contest\n"
1201 "it also skips appropriate number of FILEs.\n";
1202
1203string help_statistics_message =
1204 "Engine statistics options:\n"
1205 " --statistics=COMMANDS\n"
1206 " --statistics-white=COMMANDS\n"
1207 " --statistics-black=COMMANDS COMMANDS is a semicolon separated list of GTP\n"
1208 " commands to be executed after each game; if\n"
1209 " engines' responses appear to be numeric, totals\n"
1210 " are printed after all games are played.\n"
1211 " --reset=COMMANDS\n"
1212 " --reset-white=COMMANDS\n"
1213 " --reset-black=COMMANDS semicolon separated list of GTP commands needed\n"
1214 " to reset statistics before each game.\n\n"
1215 " --totals-only don't print numeric statistics after each game.\n"
1216 "Note that you can use `--statistics' and `--reset' options to acquire similar\n"
1217 "statistics from both engines (provided they both understand the commands). If\n"
1218 "you use color-specific options together with common options, both command lists\n"
1219 "are used as one would expect.\n";
1220
1221
1222int
1223main(int argc, array(string) argv)
1224{
1225 string hint = sprintf("Try `%s --help' for more information.\n",
1226 basename(argv[0]));
1227
1228 if (Getopt.find_option(argv, UNDEFINED, "help")) {
1229 write(help_message, basename(argv[0]));
1230 return 0;
1231 }
1232
1233 if (Getopt.find_option(argv, UNDEFINED, "help-statistics")) {
1234 write(help_statistics_message);
1235 return 0;
1236 }
1237
1238 string white = Getopt.find_option(argv, "w", "white", UNDEFINED, "");
1239 if (white == "") {
1240 werror("White player is not specified.\n" + hint);
1241 return 1;
1242 }
1243
1244 string black = Getopt.find_option(argv, "b", "black", UNDEFINED, "");
1245 if (black == "") {
1246 werror("Black player is not specified.\n" + hint);
1247 return 1;
1248 }
1249
1250 string arbiter = Getopt.find_option(argv, "A", "arbiter", UNDEFINED, "");
1251
1252 int verbose = (int) Getopt.find_option(argv, "v", "verbose",
1253 UNDEFINED, "0");
1254 Getopt.find_option(argv, "m", "match");
1255 int endgame_moves = (int) Getopt.find_option(argv, "e", "endgame",
1256 UNDEFINED, "0");
1257 int mode = (endgame_moves > 0);
1258
1259 string|int sgf_base = 0;
1260 if (!Getopt.find_option(argv, UNDEFINED, "no-sgf")) {
1261 sgf_base = Getopt.find_option(argv, UNDEFINED, "sgf-base",
1262 UNDEFINED, mode ? "endgame" : "twogtp");
1263 }
1264 else {
1265 if (Getopt.find_option(argv, UNDEFINED, "sgf-base"))
1266 werror("Warning: `--no-sgf' option specified, `--sgf-base' has no effect");
1267 }
1268
1269 int skip_games = Getopt.find_option(argv, "c", "continue");
1270 if (skip_games) {
1271 for (skip_games = 0; ; skip_games++) {
1272 if (!Stdio.is_file(sprintf("%s%03d%s.sgf", sgf_base, skip_games + 1,
1273 mode ? "_1" : "")))
1274 break;
1275 }
1276 }
1277
1278 float main_time = (float) Getopt.find_option(argv, "t", "main-time",
1279 UNDEFINED, "0") * 60.0;
1280 float byo_yomi_time = (float) Getopt.find_option(argv, "B", "byo-yomi-time",
1281 UNDEFINED, "0") * 60.0;
1282 if (main_time == 0.0 && byo_yomi_time <= 0.0)
1283 main_time = -1.0;
1284 int byo_yomi_stones = (int) Getopt.find_option(argv, "S", "byo-yomi-stones",
1285 UNDEFINED, "25");
1286 byo_yomi_stones = max(byo_yomi_stones, 1);
1287
1288 string|int statistics_value
1289 = Getopt.find_option(argv, UNDEFINED, "statistics", UNDEFINED, "");
1290 string|int statistics_white_value
1291 = Getopt.find_option(argv, UNDEFINED, "statistics-white", UNDEFINED, "");
1292 string|int statistics_black_value
1293 = Getopt.find_option(argv, UNDEFINED, "statistics-black", UNDEFINED, "");
1294
1295 array(string) statistics_white = ({});
1296 array(string) statistics_black = ({});
1297
1298 if (statistics_value && statistics_value != "") {
1299 statistics_white = map(statistics_value / ";", String.trim_all_whites);
1300 statistics_black = map(statistics_value / ";", String.trim_all_whites);
1301 }
1302
1303 if (statistics_white_value && statistics_white_value != "") {
1304 statistics_white |= map(statistics_white_value / ";",
1305 String.trim_all_whites);
1306 }
1307
1308 if (statistics_black_value && statistics_black_value != "") {
1309 statistics_black |= map(statistics_black_value / ";",
1310 String.trim_all_whites);
1311 }
1312
1313 string|int reset_value
1314 = Getopt.find_option(argv, UNDEFINED, "reset", UNDEFINED, "");
1315 string|int reset_white_value
1316 = Getopt.find_option(argv, UNDEFINED, "reset-white", UNDEFINED, "");
1317 string|int reset_black_value
1318 = Getopt.find_option(argv, UNDEFINED, "reset-black", UNDEFINED, "");
1319
1320 array(string) reset_white = ({});
1321 array(string) reset_black = ({});
1322
1323 if (reset_value && reset_value != "") {
1324 reset_white = map(reset_value / ";", String.trim_all_whites);
1325 reset_black = map(reset_value / ";", String.trim_all_whites);
1326 }
1327
1328 if (reset_white_value && reset_white_value != "")
1329 reset_white |= map(reset_white_value / ";", String.trim_all_whites);
1330
1331 if (reset_black_value && reset_black_value != "")
1332 reset_black |= map(reset_black_value / ";", String.trim_all_whites);
1333
1334 int totals_only = (Getopt.find_option(argv, UNDEFINED, "totals-only") != 0);
1335
1336 if (!mode) {
1337 string handicap_mode = "fixed";
1338 int handicap = 0;
1339
1340 int games = (int) Getopt.find_option(argv, "g", "games", UNDEFINED, "1");
1341 games = max(games, 1);
1342 int board_size = (int) Getopt.find_option(argv, "s", "board-size",
1343 UNDEFINED, "19");
1344 if (board_size < 1 || 25 < board_size) {
1345 werror("GTP only supports boards with size from 1 to 25.\n");
1346 return 1;
1347 }
1348
1349 int fixed_handicap = (int) Getopt.find_option(argv, "h", "handicap",
1350 UNDEFINED, "-1");
1351
1352 int free_handicap = (int) Getopt.find_option(argv, "f", "free-handicap",
1353 UNDEFINED, "-1");
1354
1355 if (fixed_handicap >= 0 && free_handicap >= 0) {
1356 werror("Fixed and free handicaps are mutually exclusive.\n" + hint);
1357 return 1;
1358 }
1359
1360 if (fixed_handicap >= 0) {
1361 int maximum = maximal_fixed_handicap(board_size);
1362 if (fixed_handicap > maximum) {
1363 write("Maximal allowed handicap for board size %d is %d.\n",
1364 board_size, maximum);
1365 return 1;
1366 }
1367
1368 handicap = fixed_handicap;
1369 handicap_mode = "fixed";
1370 }
1371 else if (free_handicap >= 0) {
1372 handicap = free_handicap;
1373 handicap_mode = "free";
1374 }
1375
1376 if (handicap == 1) {
1377 werror("Warning: handicap 1 is not allowed, falling back on handicap 0.\n");
1378 handicap = 0;
1379 }
1380
1381 int adjust_handicap = (int) Getopt.find_option(argv, "a", "adjust-handicap",
1382 UNDEFINED, "0");
1383 adjust_handicap = max(adjust_handicap, 0);
1384
1385 float komi = handicap ? 0.5 : 5.5;
1386 komi = (float) Getopt.find_option(argv, "k", "komi",
1387 UNDEFINED, (string) komi);
1388
1389 if (sizeof(Getopt.get_args(argv)) != 1) {
1390 werror("Unrecognized input in command line.\n" + hint);
1391 return 1;
1392 }
1393
1394
1395 GtpGame game = GtpGame(white, black, arbiter,
1396 statistics_white, statistics_black,
1397 reset_white, reset_black, totals_only,
1398 verbose, main_time, byo_yomi_time, byo_yomi_stones);
1399 if (game) {
1400 run_twogtp_match(game, games, board_size, handicap, handicap_mode,
1401 adjust_handicap, komi, verbose, sgf_base, skip_games);
1402 }
1403 }
1404 else {
1405 array(string) endgame_files = Getopt.get_args(argv)[1..];
1406 if (sizeof(endgame_files) == 0) {
1407 werror("No SGF files specified for endgame contest.\n" + hint);
1408 return 1;
1409 }
1410
1411 GtpGame game = GtpGame(white, black, arbiter,
1412 statistics_white, statistics_black,
1413 reset_white, reset_black, totals_only,
1414 verbose, main_time, byo_yomi_time, byo_yomi_stones);
1415 if (game) {
1416 endgame_contest(game, endgame_moves, endgame_files,
1417 verbose, sgf_base, skip_games);
1418 }
1419 }
1420}