From 0f87dedfbdb25273253b4a0286aaad85de9e1f49 Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Fri, 22 Dec 2023 21:37:20 +0900 Subject: [PATCH 1/4] Improve GTP compatibility for supporting Lizzie --- board/go_board.py | 12 ++++++ gtp/client.py | 94 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 78 insertions(+), 28 deletions(-) diff --git a/board/go_board.py b/board/go_board.py index 93668bb..bfea2b7 100644 --- a/board/go_board.py +++ b/board/go_board.py @@ -469,6 +469,18 @@ def get_komi(self) -> float: """ return self.komi + def get_to_move(self) -> Stone: + """手番の色を取得する。 + + Returns: + Stone: 手番の色。 + """ + if self.moves == 1: + return Stone.BLACK + else: + last_move_color, _, _ = self.record.get(self.moves - 1) + return Stone.get_opponent_color(last_move_color) + def count_score(self) -> int: # pylint: disable=R0912 """領地を簡易的にカウントする。 diff --git a/gtp/client.py b/gtp/client.py index 042f35d..e7e2f42 100644 --- a/gtp/client.py +++ b/gtp/client.py @@ -20,6 +20,7 @@ from sgf.reader import SGFReader +gtp_command_id = "" class GtpClient: # pylint: disable=R0902,R0903 """_Go Text Protocolクライアントの実装クラス @@ -295,6 +296,45 @@ def _load_sgf(self, arg_list: List[str]) -> NoReturn: respond_success("") + def _decode_analyze_arg(self, arg_list: List[str]) -> (Stone, float): + """analyzeコマンド(lz-analyze, cgos-analyze)の引数を解釈する。 + 不正な引数の場合は更新間隔として負値を返す。 + + Args: + arg_list (List[str]): コマンドの引数リスト。 + + Returns: + (Stone, float): 手番の色、更新間隔(秒) + """ + to_move = self.board.get_to_move() + interval = 0 + error_value = (to_move, -1.0) + # 受けつける形式の例 + # lz-analyze B 10 + # lz-analyze B + # lz-analyze 10 + # lz-analyze B interval 10 + # lz-analyze interval 10 + try: + if arg_list[0][0] in ['B', 'b']: + to_move = Stone.BLACK + arg_list.pop(0) + elif arg_list[0][0] in ['W', 'w']: + to_move = Stone.WHITE + arg_list.pop(0) + if arg_list[0] == "interval": + if len(arg_list) == 1: + return error_value + arg_list.pop(0) + if arg_list[0].isdigit(): + interval = int(arg_list[0])/100 + arg_list.pop(0) + except IndexError as e: + pass + if arg_list: + return error_value + return (to_move, interval) + def _analyze(self, mode: str, arg_list: List[str]) -> NoReturn: """analyzeコマンド(lz-analyze, cgos-analyze)を実行する。 @@ -302,18 +342,13 @@ def _analyze(self, mode: str, arg_list: List[str]) -> NoReturn: mode (str): 解析モード。値は"lz"か"cgos"。 arg_list (List[str]): コマンドの引数リスト (手番の色, 更新間隔)。 """ - interval = 0 - if len(arg_list) >= 2: - interval = int(arg_list[1])/100 - - if arg_list[0][0] in ['B', 'b']: - to_move = Stone.BLACK - elif arg_list[0][0] in ['W', 'w']: - to_move = Stone.WHITE - else: - respond_failure(f"{mode}-analyze color") + to_move, interval = self._decode_analyze_arg(arg_list) + if interval < 0: + respond_failure(f"{mode}-analyze [color] [interval]") return + respond_success("", ongoing=True) + analysis_query = { "mode" : mode, "interval" : interval, @@ -328,19 +363,13 @@ def _genmove_analyze(self, mode: str, arg_list: List[str]) -> NoReturn: mode (str): 解析モード。値は"lz"か"cgos"。 arg_list (List[str]): コマンドの引数リスト(手番の色, 更新間隔)。 """ - color = arg_list[0] - interval = 0 - if len(arg_list) >= 2: - interval = int(arg_list[1])/100 - - if color.lower()[0] == 'b': - genmove_color = Stone.BLACK - elif color.lower()[0] == 'w': - genmove_color = Stone.WHITE - else: - respond_failure(f"{mode}-genmove_analyze color") + genmove_color, interval = self._decode_analyze_arg(arg_list) + if interval < 0: + respond_failure(f"{mode}-analyze [color] [interval]") return + respond_success("", ongoing=True) + if self.use_network: # モンテカルロ木探索で着手生成 analysis_query = { @@ -369,13 +398,24 @@ def run(self) -> NoReturn: # pylint: disable=R0912,R0915 """Go Text Protocolのクライアントの実行処理。 入力されたコマンドに対応する処理を実行し、応答メッセージを表示する。 """ + global gtp_command_id while True: command = input() command_list = command.rstrip().split(' ') + gtp_command_id = "" input_gtp_command = command_list[0] + # 入力されたコマンドの冒頭が数字なら、それを id とみなす。 + # (参照) + # Specification of the Go Text Protocol, version 2, draft 2 + # の「2.5 Command Structure」 + # http://www.lysator.liu.se/~gunnar/gtp/gtp2-spec-draft2/gtp2-spec.html#SECTION00035000000000000000 + if input_gtp_command.isdigit(): + gtp_command_id = command_list.pop(0) + input_gtp_command = command_list[0] + if input_gtp_command == "version": _version() elif input_gtp_command == "protocol_version": @@ -445,18 +485,14 @@ def run(self) -> NoReturn: # pylint: disable=R0912,R0915 self.board.display_self_atari(Stone.WHITE) respond_success("") elif input_gtp_command == "lz-analyze": - print_out("= ") self._analyze("lz", command_list[1:]) print("") elif input_gtp_command == "lz-genmove_analyze": - print_out("= ") self._genmove_analyze("lz", command_list[1:]) elif input_gtp_command == "cgos-analyze": - print_out("= ") self._analyze("cgos", command_list[1:]) print("") elif input_gtp_command == "cgos-genmove_analyze": - print_out("= ") self._genmove_analyze("cgos", command_list[1:]) elif input_gtp_command == "hash_record": print_err(self.board.record.get_hash_history()) @@ -464,13 +500,15 @@ def run(self) -> NoReturn: # pylint: disable=R0912,R0915 else: respond_failure("unknown_command") -def respond_success(response: str) -> NoReturn: +def respond_success(response: str, ongoing: bool = False) -> NoReturn: """コマンド処理成功時の応答メッセージを表示する。 Args: response (str): 表示する応答メッセージ。 + ongoing (bool): 追加の応答メッセージが後に続くかどうか。 """ - print("= " + response + '\n') + terminator = "" if ongoing else '\n' + print(f"={gtp_command_id} " + response + terminator) def respond_failure(response: str) -> NoReturn: """コマンド処理失敗時の応答メッセージを表示する。 @@ -478,7 +516,7 @@ def respond_failure(response: str) -> NoReturn: Args: response (str): 表示する応答メッセージ。 """ - print("= ? " + response + '\n') + print(f"?{gtp_command_id} " + response + '\n') def _version() -> NoReturn: """versionコマンドを処理する。 From 79483d2f70f3beb54af40ec26c5bb8ff982f8c15 Mon Sep 17 00:00:00 2001 From: Hiraoka Date: Fri, 22 Dec 2023 21:37:20 +0900 Subject: [PATCH 2/4] Add tentative inefficient support for "undo" in GTP for Lizzie --- board/go_board.py | 10 +++++++++- gtp/client.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/board/go_board.py b/board/go_board.py index bfea2b7..a1cbe82 100644 --- a/board/go_board.py +++ b/board/go_board.py @@ -1,6 +1,6 @@ """碁盤のデータ定義と操作処理。 """ -from typing import List, NoReturn +from typing import List, Tuple, NoReturn from collections import deque import numpy as np @@ -481,6 +481,14 @@ def get_to_move(self) -> Stone: last_move_color, _, _ = self.record.get(self.moves - 1) return Stone.get_opponent_color(last_move_color) + def get_move_history(self) -> List[Tuple[Stone, int, np.array]]: + """着手の履歴を取得する。 + + Returns: + [(Stone, int, np.array), ...]: (着手の色、座標、ハッシュ値) のリスト。 + """ + return [self.record.get(m) for m in range(1, self.moves)] + def count_score(self) -> int: # pylint: disable=R0912 """領地を簡易的にカウントする。 diff --git a/gtp/client.py b/gtp/client.py index e7e2f42..a595621 100644 --- a/gtp/client.py +++ b/gtp/client.py @@ -164,6 +164,19 @@ def _play(self, color: str, pos: str) -> NoReturn: respond_success("") + def _undo(self) -> NoReturn: + """undoコマンドを処理する。 + """ + # 一旦クリアして初手から直前手まで打ち直す非効率実装 + history = self.board.get_move_history() + if not history: + respond_failure("cannot undo") + return + self._clear_board() + for (color, pos, _) in history[:-1]: + self.board.put_stone(pos, color) + respond_success("") + def _genmove(self, color: str) -> NoReturn: """genmoveコマンドを処理する。 入力された手番で思考し、着手を生成する。 @@ -432,6 +445,8 @@ def run(self) -> NoReturn: # pylint: disable=R0912,R0915 self._komi(command_list[1]) elif input_gtp_command == "play": self._play(command_list[1], command_list[2]) + elif input_gtp_command == "undo": + self._undo() elif input_gtp_command == "genmove": self._genmove(command_list[1]) elif input_gtp_command == "boardsize": From 0fa54c162e13e01cd4739859bcc958691f281e76 Mon Sep 17 00:00:00 2001 From: Yuki Kobayashi Date: Sat, 30 Dec 2023 20:30:33 +0900 Subject: [PATCH 3/4] support fixed_handicap command --- board/go_board.py | 63 +++++++++++++++++++++++++++++++++-- board/handicap.py | 83 +++++++++++++++++++++++++++++++++++++++++++++++ board/record.py | 10 ++++++ gtp/client.py | 41 +++++++++++++++++++++-- 4 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 board/handicap.py diff --git a/board/go_board.py b/board/go_board.py index a1cbe82..cfc2634 100644 --- a/board/go_board.py +++ b/board/go_board.py @@ -184,6 +184,56 @@ def put_stone(self, pos: int, color: Stone) -> NoReturn: self.record.save(self.moves, color, pos, self.positional_hash) self.moves += 1 + def put_handicap_stone(self, pos: int, color: Stone) -> NoReturn: + """指定された座標に指定された色の置き石を置く。 + + Args: + pos (int): 石を置く座標。 + color (Stone): 置く石の色。 + """ + opponent_color = Stone.get_opponent_color(color) + + self.board[pos] = color + self.pattern.put_stone(pos, color) + self.positional_hash = affect_stone_hash(self.positional_hash, pos, color) + + neighbor4 = self.get_neighbor4(pos) + + connection = [] + prisoner = 0 + + for neighbor in neighbor4: + if self.board[neighbor] == color: + self.strings.remove_liberty(neighbor, pos) + connection.append(self.strings.get_id(neighbor)) + elif self.board[neighbor] == opponent_color: + self.strings.remove_liberty(neighbor, pos) + if self.strings.get_num_liberties(neighbor) == 0: + removed_stones = self.strings.remove_string(self.board, neighbor) + prisoner += len(removed_stones) + for removed_pos in removed_stones: + self.pattern.remove_stone(removed_pos) + self.positional_hash = affect_string_hash(self.positional_hash, \ + removed_stones, opponent_color) + + if color == Stone.BLACK: + self.prisoner[0] += prisoner + elif color == Stone.WHITE: + self.prisoner[1] += prisoner + + if len(connection) == 0: + self.strings.make_string(self.board, pos, color) + if prisoner == 1 and self.strings.get_num_liberties(pos) == 1: + self.ko_move = self.moves + self.ko_pos = self.strings.string[self.strings.get_id(pos)].lib[0] + elif len(connection) == 1: + self.strings.add_stone(self.board, pos, color, connection[0]) + else: + self.strings.connect_string(self.board, pos, color, connection) + + # 着手した時に記録 + self.record.save_handicap(pos) + def _is_suicide(self, pos: int, color: Stone) -> bool: """自殺手か否かを判定する。 自殺手ならTrue、そうでなければFalseを返す。 @@ -477,9 +527,8 @@ def get_to_move(self) -> Stone: """ if self.moves == 1: return Stone.BLACK - else: - last_move_color, _, _ = self.record.get(self.moves - 1) - return Stone.get_opponent_color(last_move_color) + last_move_color, _, _ = self.record.get(self.moves - 1) + return Stone.get_opponent_color(last_move_color) def get_move_history(self) -> List[Tuple[Stone, int, np.array]]: """着手の履歴を取得する。 @@ -489,6 +538,14 @@ def get_move_history(self) -> List[Tuple[Stone, int, np.array]]: """ return [self.record.get(m) for m in range(1, self.moves)] + def get_handicap_history(self) -> List[int]: + """置き石の座標を取得する。 + + Returns: + List[int]: 置き石の座標のリスト。 + """ + return self.record.handicap_pos[:] + def count_score(self) -> int: # pylint: disable=R0912 """領地を簡易的にカウントする。 diff --git a/board/handicap.py b/board/handicap.py new file mode 100644 index 0000000..a977d99 --- /dev/null +++ b/board/handicap.py @@ -0,0 +1,83 @@ +"""置き石の座標。 +""" +from typing import List + + +handicap_coordinate_map = { + 9 : { + 2 : ["G7", "C3",], + 3 : ["C7", "G7", "C3"], + 4 : ["C7", "G7", "C3", "G3"], + 5 : ["C7", "G7", "E5", "C3", "G3"], + 6 : ["C7", "G7", "C5", "G5", "C3", "G3"], + 7 : ["C7", "G7", "C5", "E5", "G5", "C3", "G3"], + 8 : ["C7", "E7", "G7", "C5", "G5", "C3", "E3", "G3"], + 9 : ["C7", "E7", "G7", "C5", "E5", "G5", "C3", "E3", "G3"], + }, + 11 : { + 2 : ["J9", "C3"], + 3 : ["C9", "J9", "C3"], + 4 : ["C9", "J9", "C3", "J3"], + 5 : ["C9", "J9", "F6", "C3", "J3"], + 6 : ["C9", "J9", "C6", "J6", "C3", "J3"], + 7 : ["C9", "J9", "C6", "F6", "J6", "C3", "J3"], + 8 : ["C9", "F9", "J9", "C6", "J6", "C3", "F3", "J3"], + 9 : ["C9", "F9", "J9", "C6", "F6", "J6", "C3", "F3", "J3"], + }, + 13 : { + 2 : ["K10", "D4"], + 3 : ["D10", "K10", "D4"], + 4 : ["D10", "K10", "D4", "K4"], + 5 : ["D10", "K10", "G7", "D4", "K4"], + 6 : ["D10", "K10", "D7", "K7", "D4", "K4"], + 7 : ["D10", "K10", "D7", "G7", "K7", "D4", "K4"], + 8 : ["D10", "G10", "K10", "D7", "K7", "D4", "G4", "K4"], + 9 : ["D10", "G10", "K10", "D7", "G7", "K7", "D4", "G4", "K4"], + }, + 15 : { + 2 : ["M12", "D4"], + 3 : ["D12", "M12", "D4"], + 4 : ["D12", "M12", "D4", "M4"], + 5 : ["D12", "M12", "H8", "D4", "M4"], + 6 : ["D12", "M12", "D8", "M8", "D4", "M4"], + 7 : ["D12", "M12", "D8", "H8", "M8", "D4", "M4"], + 8 : ["D12", "H12", "M12", "D8", "M8", "D4", "H4", "M4"], + 9 : ["D12", "H12", "M12", "D8", "H8", "M8", "D4", "H4", "M4"], + }, + 17 : { + 2 : ["O14", "D4"], + 3 : ["D14", "O14", "D4"], + 4 : ["D14", "O14", "D4", "O4"], + 5 : ["D14", "O14", "J9", "D4", "O4"], + 6 : ["D14", "O14", "D9", "O9", "D4", "O4"], + 7 : ["D14", "O14", "D9", "J9", "O9", "D4", "O4"], + 8 : ["D14", "J14", "O14", "D9", "O9", "D4", "J4", "O4"], + 9 : ["D14", "J14", "O14", "D9", "J9", "O9", "D4", "J4", "O4"], + }, + 19 : { + 2 : ["Q16", "D4"], + 3 : ["D16", "Q16", "D4"], + 4 : ["D16", "Q16", "D4", "Q4"], + 5 : ["D16", "Q16", "K10", "D4", "Q4"], + 6 : ["D16", "Q16", "D10", "Q10", "D4", "Q4"], + 7 : ["D16", "Q16", "D10", "K10", "Q10", "D4", "Q4"], + 8 : ["D16", "K16", "Q16", "D10", "Q10", "D4", "K4", "Q4"], + 9 : ["D16", "K16", "Q16", "D10", "K10", "Q10", "D4", "K4", "Q4"], + }, +} + + +def get_handicap_coordinates(size: int, handicaps: int) -> List[int]: + """置き石の座標リストを取得する。 + + Args: + size (int): 碁盤のサイズ。 + handicaps (int): 置き石の数。 + + Returns: + List[int]: 置き石の座標リスト。 + """ + if size in handicap_coordinate_map and \ + handicaps in handicap_coordinate_map[size]: + return handicap_coordinate_map[size][handicaps] + return None diff --git a/board/record.py b/board/record.py index 5f06ea2..e8658c1 100644 --- a/board/record.py +++ b/board/record.py @@ -17,6 +17,7 @@ def __init__(self): self.color = [Stone.EMPTY] * MAX_RECORDS self.pos = [PASS] * MAX_RECORDS self.hash_value = np.zeros(shape=MAX_RECORDS, dtype=np.uint64) + self.handicap_pos = [] def clear(self) -> NoReturn: """データを初期化する。 @@ -24,6 +25,7 @@ def clear(self) -> NoReturn: self.color = [Stone.EMPTY] * MAX_RECORDS self.pos = [PASS] * MAX_RECORDS self.hash_value.fill(0) + self.handicap_pos = [] def save(self, moves: int, color: Stone, pos: int, hash_value: np.array) -> NoReturn: """着手の履歴の記録する。 @@ -41,6 +43,14 @@ def save(self, moves: int, color: Stone, pos: int, hash_value: np.array) -> NoRe else: print_err("Cannot save move record.") + def save_handicap(self, pos: int) -> NoReturn: + """置き石の座標を記録する。 + + Args: + pos (int): 置き石の座標。 + """ + self.handicap_pos.append(pos) + def has_same_hash(self, hash_value: np.array) -> bool: """同じハッシュ値があるかを確認する。 diff --git a/gtp/client.py b/gtp/client.py index a595621..29aa32e 100644 --- a/gtp/client.py +++ b/gtp/client.py @@ -9,6 +9,7 @@ from board.constant import PASS, RESIGN from board.coordinate import Coordinate from board.go_board import GoBoard +from board.handicap import get_handicap_coordinates from board.stone import Stone from common.print_console import print_err, print_out from gtp.gogui import GoguiAnalyzeCommand, display_policy_distribution, \ @@ -65,6 +66,7 @@ def __init__(self, board_size: int, superko: bool, model_file_path: str, \ "komi", "showboard", "load_sgf", + "fixed_handicap", "gogui-analyze_commands", "lz-analyze", "lz-genmove_analyze", @@ -167,14 +169,21 @@ def _play(self, color: str, pos: str) -> NoReturn: def _undo(self) -> NoReturn: """undoコマンドを処理する。 """ - # 一旦クリアして初手から直前手まで打ち直す非効率実装 history = self.board.get_move_history() if not history: respond_failure("cannot undo") return - self._clear_board() + + handicap_history = self.board.get_handicap_history() + + self.board.clear() + + for handicap in handicap_history: + self.board.put_handicap_stone(handicap, Stone.BLACK) + for (color, pos, _) in history[:-1]: self.board.put_stone(pos, color) + respond_success("") def _genmove(self, color: str) -> NoReturn: @@ -309,6 +318,32 @@ def _load_sgf(self, arg_list: List[str]) -> NoReturn: respond_success("") + def _fixed_handicap(self, handicaps: str) -> NoReturn: + """fixed_handicapコマンドを処理する。 + 指定した数の置き石を置く。 + + Args: + handicaps (str): 置き石の個数 + """ + if self.board.moves > 1 or len(self.board.get_handicap_history()) > 1 : + respond_failure("board not empty") + return + + num_handicaps = int(handicaps) + board_size = self.board.get_board_size() + + handicap_list = get_handicap_coordinates(board_size, num_handicaps) + + if handicap_list is None: + respond_failure(f"size {board_size}, handicaps {handicaps} is not supported") + return + + for handicap in handicap_list: + pos = self.board.coordinate.convert_from_gtp_format(handicap) + self.board.put_handicap_stone(pos, Stone.BLACK) + + respond_success(" ".join(handicap_list)) + def _decode_analyze_arg(self, arg_list: List[str]) -> (Stone, float): """analyzeコマンド(lz-analyze, cgos-analyze)の引数を解釈する。 不正な引数の場合は更新間隔として負値を返す。 @@ -463,6 +498,8 @@ def run(self) -> NoReturn: # pylint: disable=R0912,R0915 self._showboard() elif input_gtp_command == "load_sgf": self._load_sgf(command_list[1:]) + elif input_gtp_command == "fixed_handicap": + self._fixed_handicap(command_list[1]) elif input_gtp_command == "final_score": respond_success("?") elif input_gtp_command == "showstring": From 4e91913b173fbdcc9e1ec7088365d36551ccc61a Mon Sep 17 00:00:00 2001 From: Yuki Kobayashi Date: Sun, 31 Dec 2023 20:24:45 +0900 Subject: [PATCH 4/4] version up --- program.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/program.py b/program.py index 72e26e6..d5ed5b5 100644 --- a/program.py +++ b/program.py @@ -27,4 +27,6 @@ # Version 0.8.0 : SHOTでMixed value approximationを使うように変更 # 持ち時間の残りが少なくなった時にプログラムが落ちる不具合を修正。 # 強化学習の棋譜生成時に経過情報の表示を追加。 -VERSION="0.8.0" +# Version 0.9.0 : undo, fixed_handicapコマンド、コマンドID付きGTPコマンドのサポート。 +# 不正なGTPコマンドの応答誤りを修正。 +VERSION="0.9.0"