Elo: Fix ko-prohibited local being ignored twice
[pachi/json.git] / twogtp.py
blob0a04ebeac806fa2eebe4cb6f4578a08f23eb7bfc
1 #! /usr/bin/env python
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 and 2007 #
10 # 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 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
29 from getopt import *
30 import popen2
31 import sys
32 import string
33 import re
36 debug = 0
39 def coords_to_sgf(size, board_coords):
40 global debug
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
58 class GTP_connection:
61 # Class members:
62 # outfile File to write to
63 # infile File to read from
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
74 def exec_cmd(self, cmd):
75 global debug
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")
89 # Remove trailing newline from the result
90 if result[-1] == "\n":
91 result = result[:-1]
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
102 class GTP_player:
104 # Class members:
105 # connection GTP_connection
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"
115 def is_known_command(self, command):
116 return self.connection.exec_cmd("known_command " + command) == "true"
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
128 return self.connection.exec_cmd(command)
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)
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)
142 def komi(self, komi):
143 self.connection.exec_cmd("komi " + komi)
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")
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))
157 return string.split(result, " ")
159 def loadsgf(self, endgamefile, move_number):
160 self.connection.exec_cmd(string.join(["loadsgf", endgamefile,
161 str(move_number)]))
163 def list_stones(self, color):
164 return string.split(self.connection.exec_cmd("list_stones " + color), " ")
166 def quit(self):
167 return self.connection.exec_cmd("quit")
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
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
181 def set_random_seed(self, seed):
182 self.connection.exec_cmd("set_random_seed " + seed)
184 def get_program_name(self):
185 return self.connection.exec_cmd("name") + " " + \
186 self.connection.exec_cmd("version")
188 def final_score(self):
189 return self.connection.exec_cmd("final_score")
191 def score(self):
192 return self.final_score(self)
194 def cputime(self):
195 if (self.is_known_command("cputime")):
196 return self.connection.exec_cmd("cputime")
197 else:
198 return "0"
201 class GTP_game:
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
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 = ""
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"
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"
272 def writesgf(self, sgffilename):
273 "Write the game to an SGF file after a game"
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
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)
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")
300 to_play = self.first_to_play
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
312 def set_handicap(self, handicap):
313 self.handicap = handicap
315 def swap_players(self):
316 temp = self.whiteplayer
317 self.whiteplayer = self.blackplayer
318 self.blackplayer = temp
320 def play(self, sgffile):
321 "Play a game"
322 global verbose
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)
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)
334 self.handicap_stones = []
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)
354 to_play = self.first_to_play
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")
363 if move[:5] == "ERROR":
364 # FIXME: write_sgf
365 sys.exit(1)
367 if move[:6] == "resign":
368 if verbose >= 1:
369 print "Black resigns"
370 won_by_resignation = "W+Resign"
371 break
372 else:
373 self.moves.append(move)
374 if string.lower(move[:4]) == "pass":
375 passes = passes + 1
376 if verbose >= 1:
377 print "Black passes"
378 else:
379 passes = 0
380 self.whiteplayer.black(move)
381 if verbose >= 1:
382 print "Black plays " + move
383 to_play = "W"
384 else:
385 move = self.whiteplayer.genmove("white")
386 if move[:5] == "ERROR":
387 # FIXME: write_sgf
388 sys.exit(1)
390 if move[:6] == "resign":
391 if verbose >= 1:
392 print "White resigns"
393 won_by_resignation = "B+Resign"
394 break
395 else:
396 self.moves.append(move)
397 if string.lower(move[:4]) == "pass":
398 passes = passes + 1
399 if verbose >= 1:
400 print "White passes"
401 else:
402 passes = 0
403 self.blackplayer.white(move)
404 if verbose >= 1:
405 print "White plays " + move
406 to_play = "B"
408 if verbose >= 2:
409 print self.whiteplayer.showboard() + "\n"
411 if won_by_resignation == "":
412 self.resultw = self.whiteplayer.final_score()
413 self.resultb = self.blackplayer.final_score()
414 else:
415 self.resultw = won_by_resignation;
416 self.resultb = won_by_resignation;
417 # if self.resultb == self.resultw:
418 # print "Result: ", self.resultw
419 # else:
420 # print "Result according to W: ", self.resultw
421 # print "Result according to B: ", self.resultb
422 # FIXME: $self->writesgf($sgffile) if defined $sgffile;
423 if sgffile != "":
424 self.writesgf(sgffile)
426 def result(self):
427 return (self.resultw, self.resultb)
429 def cputime(self):
430 cputime = {}
431 cputime["white"] = self.whiteplayer.cputime()
432 cputime["black"] = self.blackplayer.cputime()
433 return cputime
435 def quit(self):
436 self.blackplayer.quit()
437 self.whiteplayer.quit()
440 class GTP_match:
442 # Class members:
443 # black
444 # white
445 # size
446 # komi
447 # handicap
448 # handicap_type
450 def __init__(self, whitecommand, blackcommand, size, komi, handicap,
451 handicap_type, streak_length, endgamefilelist):
452 self.white = whitecommand
453 self.black = blackcommand
454 self.size = size
455 self.komi = komi
456 self.handicap = handicap
457 self.handicap_type = handicap_type
458 self.streak_length = streak_length
459 self.endgamefilelist = endgamefilelist
461 def endgame_contest(self, sgfbase):
462 results = []
463 i = 1
464 for endgamefile in self.endgamefilelist:
465 game1 = GTP_game(self.white, self.black, self.size, self.komi,
466 0, "", endgamefile)
467 game2 = GTP_game(self.black, self.white, self.size, self.komi,
468 0, "", endgamefile)
469 if verbose:
470 print "Replaying", endgamefile
471 print "Black:", self.black
472 print "White:", self.white
473 game1.play("")
474 result1 = game1.result()[0]
475 if result1 != "0":
476 plain_result1 = re.search(r"([BW]\+)([0-9]*\.[0-9]*)", result1)
477 result1_float = float(plain_result1.group(2))
478 else:
479 plain_result1 = re.search(r"(0)", "0")
480 result1_float = 0.0
481 if result1[0] == "B":
482 result1_float *= -1
483 if verbose:
484 print "Result:", result1
485 print "Replaying", endgamefile
486 print "Black:", self.white
487 print "White:", self.black
488 game2.play("")
489 result2 = game2.result()[1]
490 if verbose:
491 print "Result:", result2
492 if result2 != "0":
493 plain_result2 = re.search(r"([BW]\+)([0-9]*\.[0-9]*)", result2)
494 result2_float = float(plain_result2.group(2))
495 else:
496 plain_result2 = re.search(r"(0)", "0")
497 result2_float = 0.0
499 if result2[0] == "B":
500 result2_float *= -1
501 results.append(result1_float - result2_float)
502 if (result1 != result2):
503 print endgamefile+ ":", plain_result1.group(), \
504 plain_result2.group(), "Difference:",
505 print result1_float - result2_float
506 else:
507 print endgamefile+": Same result:", plain_result1.group()
508 sgffilename = "%s%03d" % (sgfbase, i)
509 game1.writesgf(sgffilename + "_1.sgf")
510 game2.writesgf(sgffilename + "_2.sgf")
511 game1.quit()
512 game2.quit()
513 i += 1
514 return results
516 def play(self, games, sgfbase):
517 last_color = ""
518 last_streak = 0
519 game = GTP_game(self.white, self.black,
520 self.size, self.komi, self.handicap,
521 self.handicap_type, "")
522 results = []
523 for i in range(games):
524 sgffilename = "%s%03d.sgf" % (sgfbase, i + 1)
525 game.play(sgffilename)
526 result = game.result()
527 if result[0] == result[1]:
528 print "Game %d: %s" % (i + 1, result[0])
529 else:
530 print "Game %d: %s %s" % (i + 1, result[0], result[1])
532 if result[0][0] == last_color:
533 last_streak += 1
534 elif result[0][0] != "0":
535 last_color = result[0][0]
536 last_streak = 1
538 if last_streak == self.streak_length:
539 if last_color == "W":
540 self.handicap += 1
541 if self.handicap == 1:
542 self.handicap = 2
543 print "White wins too often. Increasing handicap to %d" \
544 % (self.handicap)
545 else:
546 if self.handicap > 0:
547 self.handicap -= 1
548 if self.handicap == 1:
549 self.handicap = 0
550 print "Black wins too often. Decreasing handicap to %d" \
551 % (self.handicap)
552 else:
553 self.handicap = 2
554 game.swap_players()
555 print "Black looks stronger than white. Swapping colors and setting handicap to 2"
556 game.set_handicap(self.handicap)
557 last_color = ""
558 last_streak = 0
559 results.append(result)
560 cputime = game.cputime()
561 game.quit()
562 return results, cputime
565 # ================================================================
566 # Main program
570 # Default values
573 white = ""
574 black = ""
575 komi = ""
576 size = "19"
577 handicap = 0
578 handicap_type = "fixed"
579 streak_length = -1
580 endgame_start_at = 0
582 games = 1
583 sgfbase = "twogtp"
585 verbose = 0
587 helpstring = """
589 Run with:
591 twogtp --white \'<path to program 1> --mode gtp [program options]\' \\
592 --black \'<path to program 2> --mode gtp [program options]\' \\
593 [twogtp options]
595 Possible twogtp options:
597 --verbose 1 (to list moves) or --verbose 2 (to draw board)
598 --komi <amount>
599 --handicap <amount>
600 --free-handicap <amount>
601 --adjust-handicap <length> (change handicap by 1 after <length> wins
602 in a row)
603 --size <board size> (default 19)
604 --games <number of games to play> (default 1)
605 --sgfbase <filename> (create sgf files with sgfbase as basename)
606 --endgame <moves before end> (endgame contest - add filenames of
607 games to be replayed after last option)
610 def usage():
611 print helpstring
612 sys.exit(1)
614 try:
615 (opts, params) = getopt(sys.argv[1:], "",
616 ["black=",
617 "white=",
618 "verbose=",
619 "komi=",
620 "boardsize=",
621 "size=",
622 "handicap=",
623 "free-handicap=",
624 "adjust-handicap=",
625 "games=",
626 "sgfbase=",
627 "endgame=",
629 except:
630 usage();
632 for opt, value in opts:
633 if opt == "--black":
634 black = value
635 elif opt == "--white":
636 white = value
637 elif opt == "--verbose":
638 verbose = int(value)
639 elif opt == "--komi":
640 komi = value
641 elif opt == "--boardsize" or opt == "--size":
642 size = value
643 elif opt == "--handicap":
644 handicap = int(value)
645 handicap_type = "fixed"
646 elif opt == "--free-handicap":
647 handicap = int(value)
648 handicap_type = "free"
649 elif opt == "--adjust-handicap":
650 streak_length = int(value)
651 elif opt == "--games":
652 games = int(value)
653 elif opt == "--sgfbase":
654 sgfbase = value
655 elif opt == "--endgame":
656 endgame_start_at = int(value)
658 if endgame_start_at != 0:
659 endgame_filelist = params
660 else:
661 endgame_filelist = []
662 if params != []:
663 usage()
666 if black == "" or white == "":
667 usage()
669 if komi == "":
670 if handicap > 1 and streak_length == -1:
671 komi = "0.5"
672 else:
673 komi = "5.5"
675 match = GTP_match(white, black, size, komi, handicap, handicap_type,
676 streak_length, endgame_filelist)
677 if endgame_filelist != []:
678 results = match.endgame_contest(sgfbase)
679 win_black = 0
680 win_white = 0
681 for res in results:
682 print res
683 if res > 0.0:
684 win_white += 1
685 elif res < 0.0:
686 win_black += 1
687 print "%d games, %d wins for black, %d wins for white." \
688 % (len(results), win_black, win_white)
690 else:
691 results, cputimes = match.play(games, sgfbase)
693 i = 0
694 for resw, resb in results:
695 i = i + 1
696 if resw == resb:
697 print "Game %d: %s" % (i, resw)
698 else:
699 print "Game %d: %s %s" % (i, resb, resw)
700 if (cputimes["white"] != "0"):
701 print "White: %ss CPU time" % cputimes["white"]
702 if (cputimes["black"] != "0"):
703 print "Black: %ss CPU time" % cputimes["black"]