From 0fa54c162e13e01cd4739859bcc958691f281e76 Mon Sep 17 00:00:00 2001 From: Yuki Kobayashi Date: Sat, 30 Dec 2023 20:30:33 +0900 Subject: [PATCH] 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":