Initial commit of GNU Go v3.8.
[sgk-go] / interface / gtp_examples / twogtp.py
CommitLineData
7eeb782e
AT
1#! /usr/bin/env python
2
3# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
4# This program is distributed with GNU Go, a Go program. #
5# #
6# Write gnugo@gnu.org or see http://www.gnu.org/software/gnugo/ #
7# for more information. #
8# #
9# Copyright 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, #
10# 2008 and 2009 by the Free Software Foundation. #
11# #
12# This program is free software; you can redistribute it and/or #
13# modify it under the terms of the GNU General Public License #
14# as published by the Free Software Foundation - version 3, #
15# or (at your option) any later version. #
16# #
17# This program is distributed in the hope that it will be #
18# useful, but WITHOUT ANY WARRANTY; without even the implied #
19# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR #
20# PURPOSE. See the GNU General Public License in file COPYING #
21# for more details. #
22# #
23# You should have received a copy of the GNU General Public #
24# License along with this program; if not, write to the Free #
25# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, #
26# Boston, MA 02111, USA. #
27# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
28
29from getopt import *
30import popen2
31import sys
32import string
33import re
34
35
36debug = 0
37
38
39def coords_to_sgf(size, board_coords):
40 global debug
41
42 board_coords = string.lower(board_coords)
43 if board_coords == "pass":
44 return ""
45 if debug:
46 print "Coords: <" + board_coords + ">"
47 letter = board_coords[0]
48 digits = board_coords[1:]
49 if letter > "i":
50 sgffirst = chr(ord(letter) - 1)
51 else:
52 sgffirst = letter
53 sgfsecond = chr(ord("a") + int(size) - int(digits))
54 return sgffirst + sgfsecond
55
56
57
58class GTP_connection:
59
60 #
61 # Class members:
62 # outfile File to write to
63 # infile File to read from
64
65 def __init__(self, command):
66 try:
67 infile, outfile = popen2.popen2(command)
68 except:
69 print "popen2 failed"
70 sys.exit(1)
71 self.infile = infile
72 self.outfile = outfile
73
74 def exec_cmd(self, cmd):
75 global debug
76
77 if debug:
78 sys.stderr.write("GTP command: " + cmd + "\n")
79 self.outfile.write(cmd + "\n\n")
80 self.outfile.flush()
81 result = ""
82 line = self.infile.readline()
83 while line != "\n":
84 result = result + line
85 line = self.infile.readline()
86 if debug:
87 sys.stderr.write("Reply: " + line + "\n")
88
89 # Remove trailing newline from the result
90 if result[-1] == "\n":
91 result = result[:-1]
92
93 if len(result) == 0:
94 return "ERROR: len = 0"
95 if (result[0] == "?"):
96 return "ERROR: GTP Command failed: " + result[2:]
97 if (result[0] == "="):
98 return result[2:]
99 return "ERROR: Unrecognized answer: " + result
100
101
102class GTP_player:
103
104 # Class members:
105 # connection GTP_connection
106
107 def __init__(self, command):
108 self.connection = GTP_connection(command)
109 protocol_version = self.connection.exec_cmd("protocol_version")
110 if protocol_version[:5] != "ERROR":
111 self.protocol_version = protocol_version
112 else:
113 self.protocol_version = "1"
114
115 def is_known_command(self, command):
116 return self.connection.exec_cmd("known_command " + command) == "true"
117
118 def genmove(self, color):
119 if color[0] in ["b", "B"]:
120 command = "black"
121 elif color[0] in ["w", "W"]:
122 command = "white"
123 if self.protocol_version == "1":
124 command = "genmove_" + command
125 else:
126 command = "genmove " + command
127
128 return self.connection.exec_cmd(command)
129
130 def black(self, move):
131 if self.protocol_version == "1":
132 self.connection.exec_cmd("black " + move)
133 else:
134 self.connection.exec_cmd("play black " + move)
135
136 def white(self, move):
137 if self.protocol_version == "1":
138 self.connection.exec_cmd("white " + move)
139 else:
140 self.connection.exec_cmd("play white " + move)
141
142 def komi(self, komi):
143 self.connection.exec_cmd("komi " + komi)
144
145 def boardsize(self, size):
146 self.connection.exec_cmd("boardsize " + size)
147 if self.protocol_version != "1":
148 self.connection.exec_cmd("clear_board")
149
150 def handicap(self, handicap, handicap_type):
151 if handicap_type == "fixed":
152 result = self.connection.exec_cmd("fixed_handicap %d" % (handicap))
153 else:
154 result = self.connection.exec_cmd("place_free_handicap %d"
155 % (handicap))
156
157 return string.split(result, " ")
158
159 def loadsgf(self, endgamefile, move_number):
160 self.connection.exec_cmd(string.join(["loadsgf", endgamefile,
161 str(move_number)]))
162
163 def list_stones(self, color):
164 return string.split(self.connection.exec_cmd("list_stones " + color), " ")
165
166 def quit(self):
167 return self.connection.exec_cmd("quit")
168
169 def showboard(self):
170 board = self.connection.exec_cmd("showboard")
171 if board and (board[0] == "\n"):
172 board = board[1:]
173 return board
174
175 def get_random_seed(self):
176 result = self.connection.exec_cmd("get_random_seed")
177 if result[:5] == "ERROR":
178 return "unknown"
179 return result
180
181 def set_random_seed(self, seed):
182 self.connection.exec_cmd("set_random_seed " + seed)
183
184 def get_program_name(self):
185 return self.connection.exec_cmd("name") + " " + \
186 self.connection.exec_cmd("version")
187
188 def final_score(self):
189 return self.connection.exec_cmd("final_score")
190
191 def score(self):
192 return self.final_score(self)
193
194 def cputime(self):
195 if (self.is_known_command("cputime")):
196 return self.connection.exec_cmd("cputime")
197 else:
198 return "0"
199
200
201class GTP_game:
202
203 # Class members:
204 # whiteplayer GTP_player
205 # blackplayer GTP_player
206 # size int
207 # komi float
208 # handicap int
209 # handicap_type string
210 # handicap_stones int
211 # moves list of string
212 # resultw
213 # resultb
214
215 def __init__(self, whitecommand, blackcommand, size, komi, handicap,
216 handicap_type, endgamefile):
217 self.whiteplayer = GTP_player(whitecommand)
218 self.blackplayer = GTP_player(blackcommand)
219 self.size = size
220 self.komi = komi
221 self.handicap = handicap
222 self.handicap_type = handicap_type
223 self.endgamefile = endgamefile
224 self.sgffilestart = ""
225 if endgamefile != "":
226 self.init_endgame_contest_game()
227 else:
228 self.sgffilestart = ""
229
230 def init_endgame_contest_game(self):
231 infile = open(self.endgamefile)
232 if not infile:
233 print "Couldn't read " + self.endgamefile
234 sys.exit(2)
235 sgflines = infile.readlines()
236 infile.close
237 size = re.compile("SZ\[[0-9]+\]")
238 move = re.compile(";[BW]\[[a-z]{0,2}\]")
239 sgf_start = []
240 for line in sgflines:
241 match = size.search(line)
242 if match:
243 self.size = match.group()[3:-1]
244 match = move.search(line)
245 while match:
246 sgf_start.append("A" + match.group()[1:])
247 line = line[match.end():]
248 match = move.search(line)
249 self.endgame_start = len(sgf_start) - endgame_start_at
250 self.sgffilestart = ";" + string.join(
251 sgf_start[:self.endgame_start-1], "") + "\n"
252 if self.endgame_start % 2 == 0:
253 self.first_to_play = "W"
254 else:
255 self.first_to_play = "B"
256
257 def get_position_from_engine(self, engine):
258 black_stones = engine.list_stones("black")
259 white_stones = engine.list_stones("white")
260 self.sgffilestart = ";"
261 if len(black_stones) > 0:
262 self.sgffilestart += "AB"
263 for stone in black_stones:
264 self.sgffilestart += "[%s]" % coords_to_sgf(self.size, stone)
265 self.sgffilestart += "\n"
266 if len(white_stones) > 0:
267 self.sgffilestart += "AW"
268 for stone in white_stones:
269 self.sgffilestart += "[%s]" % coords_to_sgf(self.size, stone)
270 self.sgffilestart += "\n"
271
272 def writesgf(self, sgffilename):
273 "Write the game to an SGF file after a game"
274
275 size = self.size
276 outfile = open(sgffilename, "w")
277 if not outfile:
278 print "Couldn't create " + sgffilename
279 return
280 black_name = self.blackplayer.get_program_name()
281 white_name = self.whiteplayer.get_program_name()
282 black_seed = self.blackplayer.get_random_seed()
283 white_seed = self.whiteplayer.get_random_seed()
284 handicap = self.handicap
285 komi = self.komi
286 result = self.resultw
287
288 outfile.write("(;GM[1]FF[4]RU[Japanese]SZ[%s]HA[%s]KM[%s]RE[%s]\n" %
289 (size, handicap, komi, result))
290 outfile.write("PW[%s (random seed %s)]PB[%s (random seed %s)]\n" %
291 (white_name, white_seed, black_name, black_seed))
292 outfile.write(self.sgffilestart)
293
294 if handicap > 1:
295 outfile.write("AB");
296 for stone in self.handicap_stones:
297 outfile.write("[%s]" %(coords_to_sgf(size, stone)))
298 outfile.write("PL[W]\n")
299
300 to_play = self.first_to_play
301
302 for move in self.moves:
303 sgfmove = coords_to_sgf(size, move)
304 outfile.write(";%s[%s]\n" % (to_play, sgfmove))
305 if to_play == "B":
306 to_play = "W"
307 else:
308 to_play = "B"
309 outfile.write(")\n")
310 outfile.close
311
312 def set_handicap(self, handicap):
313 self.handicap = handicap
314
315 def swap_players(self):
316 temp = self.whiteplayer
317 self.whiteplayer = self.blackplayer
318 self.blackplayer = temp
319
320 def play(self, sgffile):
321 "Play a game"
322 global verbose
323
324 if verbose >= 1:
325 print "Setting boardsize and komi for black\n"
326 self.blackplayer.boardsize(self.size)
327 self.blackplayer.komi(self.komi)
328
329 if verbose >= 1:
330 print "Setting boardsize and komi for white\n"
331 self.whiteplayer.boardsize(self.size)
332 self.whiteplayer.komi(self.komi)
333
334 self.handicap_stones = []
335
336 if self.endgamefile == "":
337 if self.handicap < 2:
338 self.first_to_play = "B"
339 else:
340 self.handicap_stones = self.blackplayer.handicap(self.handicap, self.handicap_type)
341 for stone in self.handicap_stones:
342 self.whiteplayer.black(stone)
343 self.first_to_play = "W"
344 else:
345 self.blackplayer.loadsgf(self.endgamefile, self.endgame_start)
346 self.blackplayer.set_random_seed("0")
347 self.whiteplayer.loadsgf(self.endgamefile, self.endgame_start)
348 self.whiteplayer.set_random_seed("0")
349 if self.blackplayer.is_known_command("list_stones"):
350 self.get_position_from_engine(self.blackplayer)
351 elif self.whiteplayer.is_known_command("list_stones"):
352 self.get_position_from_engine(self.whiteplayer)
353
354 to_play = self.first_to_play
355
356 self.moves = []
357 passes = 0
358 won_by_resignation = ""
359 while passes < 2:
360 if to_play == "B":
361 move = self.blackplayer.genmove("black")
362 if move[:5] == "ERROR":
363 # FIXME: write_sgf
364 sys.exit(1)
365
366 if move[:6] == "resign":
367 if verbose >= 1:
368 print "Black resigns"
369 won_by_resignation = "W+Resign"
370 break
371 else:
372 self.moves.append(move)
373 if string.lower(move[:4]) == "pass":
374 passes = passes + 1
375 if verbose >= 1:
376 print "Black passes"
377 else:
378 passes = 0
379 self.whiteplayer.black(move)
380 if verbose >= 1:
381 print "Black plays " + move
382 to_play = "W"
383 else:
384 move = self.whiteplayer.genmove("white")
385 if move[:5] == "ERROR":
386 # FIXME: write_sgf
387 sys.exit(1)
388
389 if move[:6] == "resign":
390 if verbose >= 1:
391 print "White resigns"
392 won_by_resignation = "B+Resign"
393 break
394 else:
395 self.moves.append(move)
396 if string.lower(move[:4]) == "pass":
397 passes = passes + 1
398 if verbose >= 1:
399 print "White passes"
400 else:
401 passes = 0
402 self.blackplayer.white(move)
403 if verbose >= 1:
404 print "White plays " + move
405 to_play = "B"
406
407 if verbose >= 2:
408 print self.whiteplayer.showboard() + "\n"
409
410 if won_by_resignation == "":
411 self.resultw = self.whiteplayer.final_score()
412 self.resultb = self.blackplayer.final_score()
413 else:
414 self.resultw = won_by_resignation;
415 self.resultb = won_by_resignation;
416 # if self.resultb == self.resultw:
417 # print "Result: ", self.resultw
418 # else:
419 # print "Result according to W: ", self.resultw
420 # print "Result according to B: ", self.resultb
421 # FIXME: $self->writesgf($sgffile) if defined $sgffile;
422 if sgffile != "":
423 self.writesgf(sgffile)
424
425 def result(self):
426 return (self.resultw, self.resultb)
427
428 def cputime(self):
429 cputime = {}
430 cputime["white"] = self.whiteplayer.cputime()
431 cputime["black"] = self.blackplayer.cputime()
432 return cputime
433
434 def quit(self):
435 self.blackplayer.quit()
436 self.whiteplayer.quit()
437
438
439class GTP_match:
440
441 # Class members:
442 # black
443 # white
444 # size
445 # komi
446 # handicap
447 # handicap_type
448
449 def __init__(self, whitecommand, blackcommand, size, komi, handicap,
450 handicap_type, streak_length, endgamefilelist):
451 self.white = whitecommand
452 self.black = blackcommand
453 self.size = size
454 self.komi = komi
455 self.handicap = handicap
456 self.handicap_type = handicap_type
457 self.streak_length = streak_length
458 self.endgamefilelist = endgamefilelist
459
460 def endgame_contest(self, sgfbase):
461 results = []
462 i = 1
463 for endgamefile in self.endgamefilelist:
464 game1 = GTP_game(self.white, self.black, self.size, self.komi,
465 0, "", endgamefile)
466 game2 = GTP_game(self.black, self.white, self.size, self.komi,
467 0, "", endgamefile)
468 if verbose:
469 print "Replaying", endgamefile
470 print "Black:", self.black
471 print "White:", self.white
472 game1.play("")
473 result1 = game1.result()[0]
474 if result1 != "0":
475 plain_result1 = re.search(r"([BW]\+)([0-9]*\.[0-9]*)", result1)
476 result1_float = float(plain_result1.group(2))
477 else:
478 plain_result1 = re.search(r"(0)", "0")
479 result1_float = 0.0
480 if result1[0] == "B":
481 result1_float *= -1
482 if verbose:
483 print "Result:", result1
484 print "Replaying", endgamefile
485 print "Black:", self.white
486 print "White:", self.black
487 game2.play("")
488 result2 = game2.result()[1]
489 if verbose:
490 print "Result:", result2
491 if result2 != "0":
492 plain_result2 = re.search(r"([BW]\+)([0-9]*\.[0-9]*)", result2)
493 result2_float = float(plain_result2.group(2))
494 else:
495 plain_result2 = re.search(r"(0)", "0")
496 result2_float = 0.0
497
498 if result2[0] == "B":
499 result2_float *= -1
500 results.append(result1_float - result2_float)
501 if (result1 != result2):
502 print endgamefile+ ":", plain_result1.group(), \
503 plain_result2.group(), "Difference:",
504 print result1_float - result2_float
505 else:
506 print endgamefile+": Same result:", plain_result1.group()
507 sgffilename = "%s%03d" % (sgfbase, i)
508 game1.writesgf(sgffilename + "_1.sgf")
509 game2.writesgf(sgffilename + "_2.sgf")
510 game1.quit()
511 game2.quit()
512 i += 1
513 return results
514
515 def play(self, games, sgfbase):
516 last_color = ""
517 last_streak = 0
518 game = GTP_game(self.white, self.black,
519 self.size, self.komi, self.handicap,
520 self.handicap_type, "")
521 results = []
522 for i in range(games):
523 sgffilename = "%s%03d.sgf" % (sgfbase, i + 1)
524 game.play(sgffilename)
525 result = game.result()
526 if result[0] == result[1]:
527 print "Game %d: %s" % (i + 1, result[0])
528 else:
529 print "Game %d: %s %s" % (i + 1, result[0], result[1])
530
531 if result[0][0] == last_color:
532 last_streak += 1
533 elif result[0][0] != "0":
534 last_color = result[0][0]
535 last_streak = 1
536
537 if last_streak == self.streak_length:
538 if last_color == "W":
539 self.handicap += 1
540 if self.handicap == 1:
541 self.handicap = 2
542 print "White wins too often. Increasing handicap to %d" \
543 % (self.handicap)
544 else:
545 if self.handicap > 0:
546 self.handicap -= 1
547 if self.handicap == 1:
548 self.handicap = 0
549 print "Black wins too often. Decreasing handicap to %d" \
550 % (self.handicap)
551 else:
552 self.handicap = 2
553 game.swap_players()
554 print "Black looks stronger than white. Swapping colors and setting handicap to 2"
555 game.set_handicap(self.handicap)
556 last_color = ""
557 last_streak = 0
558 results.append(result)
559 cputime = game.cputime()
560 game.quit()
561 return results, cputime
562
563
564# ================================================================
565# Main program
566#
567
568
569# Default values
570#
571
572white = ""
573black = ""
574komi = ""
575size = "19"
576handicap = 0
577handicap_type = "fixed"
578streak_length = -1
579endgame_start_at = 0
580
581games = 1
582sgfbase = "twogtp"
583
584verbose = 0
585
586helpstring = """
587
588Run with:
589
590twogtp --white \'<path to program 1> --mode gtp [program options]\' \\
591 --black \'<path to program 2> --mode gtp [program options]\' \\
592 [twogtp options]
593
594Possible twogtp options:
595
596 --verbose 1 (to list moves) or --verbose 2 (to draw board)
597 --komi <amount>
598 --handicap <amount>
599 --free-handicap <amount>
600 --adjust-handicap <length> (change handicap by 1 after <length> wins
601 in a row)
602 --size <board size> (default 19)
603 --games <number of games to play> (default 1)
604 --sgfbase <filename> (create sgf files with sgfbase as basename)
605 --endgame <moves before end> (endgame contest - add filenames of
606 games to be replayed after last option)
607"""
608
609def usage():
610 print helpstring
611 sys.exit(1)
612
613try:
614 (opts, params) = getopt(sys.argv[1:], "",
615 ["black=",
616 "white=",
617 "verbose=",
618 "komi=",
619 "boardsize=",
620 "size=",
621 "handicap=",
622 "free-handicap=",
623 "adjust-handicap=",
624 "games=",
625 "sgfbase=",
626 "endgame=",
627 ])
628except:
629 usage();
630
631for opt, value in opts:
632 if opt == "--black":
633 black = value
634 elif opt == "--white":
635 white = value
636 elif opt == "--verbose":
637 verbose = int(value)
638 elif opt == "--komi":
639 komi = value
640 elif opt == "--boardsize" or opt == "--size":
641 size = value
642 elif opt == "--handicap":
643 handicap = int(value)
644 handicap_type = "fixed"
645 elif opt == "--free-handicap":
646 handicap = int(value)
647 handicap_type = "free"
648 elif opt == "--adjust-handicap":
649 streak_length = int(value)
650 elif opt == "--games":
651 games = int(value)
652 elif opt == "--sgfbase":
653 sgfbase = value
654 elif opt == "--endgame":
655 endgame_start_at = int(value)
656
657if endgame_start_at != 0:
658 endgame_filelist = params
659else:
660 endgame_filelist = []
661 if params != []:
662 usage()
663
664
665if black == "" or white == "":
666 usage()
667
668if komi == "":
669 if handicap > 1 and streak_length == -1:
670 komi = "0.5"
671 else:
672 komi = "5.5"
673
674match = GTP_match(white, black, size, komi, handicap, handicap_type,
675 streak_length, endgame_filelist)
676if endgame_filelist != []:
677 results = match.endgame_contest(sgfbase)
678 win_black = 0
679 win_white = 0
680 for res in results:
681 print res
682 if res > 0.0:
683 win_white += 1
684 elif res < 0.0:
685 win_black += 1
686 print "%d games, %d wins for black, %d wins for white." \
687 % (len(results), win_black, win_white)
688
689else:
690 results, cputimes = match.play(games, sgfbase)
691
692 i = 0
693 for resw, resb in results:
694 i = i + 1
695 if resw == resb:
696 print "Game %d: %s" % (i, resw)
697 else:
698 print "Game %d: %s %s" % (i, resb, resw)
699 if (cputimes["white"] != "0"):
700 print "White: %ss CPU time" % cputimes["white"]
701 if (cputimes["black"] != "0"):
702 print "Black: %ss CPU time" % cputimes["black"]