diff --git a/.gitignore b/.gitignore index 3583a144..4227970a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ sgf_ogs log* tmp.pickle my +logs +callgrind.* # debug outdated_log.txt diff --git a/bots/ai2gtp.py b/bots/ai2gtp.py index eb3d35f8..602acf76 100644 --- a/bots/ai2gtp.py +++ b/bots/ai2gtp.py @@ -2,6 +2,7 @@ import json import sys import time +import random from core.ai import ai_move from core.common import OUTPUT_ERROR, OUTPUT_INFO @@ -85,7 +86,6 @@ def malkovich_analysis(cn): while True: - p = game.current_node.next_player line = input() logger.log(f"GOT INPUT {line}", OUTPUT_ERROR) if "boardsize" in line: @@ -103,8 +103,14 @@ def malkovich_analysis(cn): logger.log(f"Setting komi {game.root.properties}", OUTPUT_ERROR) elif "place_free_handicap" in line: _, n = line.split(" ") - game.place_handicap_stones(int(n)) - gtp = [Move.from_sgf(m, game.board_size, "B").gtp() for m in game.root.get_list_property("AB")] + n = int(n) + game.place_handicap_stones(n) + handicaps = set(game.root.get_list_property("AB")) + bx, by = game.board_size + while len(handicaps) < min(n, bx * by): # really obscure cases + handicaps.add(Move((random.randint(0, bx - 1), random.randint(0, by - 1)), player="B").sgf(board_size=game.board_size)) + game.root.set_property("AB", list(handicaps)) + gtp = [Move.from_sgf(m, game.board_size, "B").gtp() for m in handicaps] logger.log(f"Chose handicap placements as {gtp}", OUTPUT_ERROR) print(f"= {' '.join(gtp)}\n") sys.stdout.flush() @@ -115,6 +121,12 @@ def malkovich_analysis(cn): game.root.set_property("AB", [Move.from_gtp(move.upper()).sgf(game.board_size) for move in stones]) logger.log(f"Set handicap placements to {game.root.get_list_property('AB')}", OUTPUT_ERROR) elif "genmove" in line: + _, player = line.strip().split(" ") + if player[0].upper() != game.next_player: + logger.log(f"ERROR generating move: UNEXPECTED PLAYER {player} != {game.next_player}.", OUTPUT_ERROR) + print(f"= ??\n") + sys.stdout.flush() + continue logger.log(f"{ai_strategy} generating move", OUTPUT_ERROR) game.current_node.analyze(engine) malkovich_analysis(game.current_node) @@ -130,7 +142,12 @@ def malkovich_analysis(cn): move = game.play(Move(None, player=game.next_player)).single_move else: move, node = ai_move(game, ai_strategy, ai_settings) - logger.log(f"Generated move {move}", OUTPUT_ERROR) + if node is None: + while node is None: + logger.log(f"ERROR generating move, backing up with weighted.", OUTPUT_ERROR) + move, node = ai_move(game, "p:weighted", {"pick_override": 1.0, "lower_bound": 0.001, "weaken_fac": 1}) + else: + logger.log(f"Generated move {move}", OUTPUT_ERROR) print(f"= {move.gtp()}\n") sys.stdout.flush() malkovich_analysis(game.current_node) diff --git a/core/ai.py b/core/ai.py index bf420f20..225bc882 100644 --- a/core/ai.py +++ b/core/ai.py @@ -34,8 +34,7 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode raise EngineDiedException(f"Engine for {cn.next_player} ({engine.config}) died") ai_mode = ai_mode.lower() ai_thoughts = "" - candidate_ai_moves = cn.candidate_moves - if ("policy" in ai_mode or "p:" in ai_mode) and cn.policy: + if ("policy" in ai_mode or "p:" in ai_mode) and cn.policy: # pure policy based move policy_moves = cn.policy_ranking pass_policy = cn.policy[-1] top_5_pass = any([polmove[1].is_pass for polmove in policy_moves[:5]]) # dont make it jump around for the last few sensible non pass moves @@ -131,28 +130,30 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode ai_thoughts += f"Pick policy strategy {ai_mode} failed to find legal moves, so is playing top policy move {aimove.gtp()}." else: raise ValueError(f"Unknown AI mode {ai_mode}") - elif "balance" in ai_mode and candidate_ai_moves[0]["move"] != "pass": # don't play suicidal to balance score - pass when it's best - sign = cn.player_sign(cn.next_player) - sel_moves = [ # top move, or anything not too bad, or anything that makes you still ahead - move - for i, move in enumerate(candidate_ai_moves) - if i == 0 - or move["visits"] >= ai_settings["min_visits"] - and (move["pointsLost"] < ai_settings["random_loss"] or move["pointsLost"] < ai_settings["max_loss"] and sign * move["scoreLead"] > ai_settings["target_score"]) - ] - aimove = Move.from_gtp(random.choice(sel_moves)["move"], player=cn.next_player) - ai_thoughts += f"Balance strategy selected moves {sel_moves} based on target score and max points lost, and randomly chose {aimove.gtp()}." - elif "jigo" in ai_mode and candidate_ai_moves[0]["move"] != "pass": - sign = cn.player_sign(cn.next_player) - jigo_move = min(candidate_ai_moves, key=lambda move: abs(sign * move["scoreLead"] - ai_settings["target_score"])) - aimove = Move.from_gtp(jigo_move["move"], player=cn.next_player) - ai_thoughts += f"Jigo strategy found candidate moves {candidate_ai_moves} moves and chose {aimove.gtp()} as closest to 0.5 point win" - else: - if "default" not in ai_mode and "katago" not in ai_mode: - game.katrain.log(f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO) - ai_thoughts += f"Strategy {ai_mode} not found or unexpected fallback." - aimove = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player) - ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move" + else: # Engine based move + candidate_ai_moves = cn.candidate_moves + if "balance" in ai_mode and candidate_ai_moves[0]["move"] != "pass": # don't play suicidal to balance score - pass when it's best + sign = cn.player_sign(cn.next_player) + sel_moves = [ # top move, or anything not too bad, or anything that makes you still ahead + move + for i, move in enumerate(candidate_ai_moves) + if i == 0 + or move["visits"] >= ai_settings["min_visits"] + and (move["pointsLost"] < ai_settings["random_loss"] or move["pointsLost"] < ai_settings["max_loss"] and sign * move["scoreLead"] > ai_settings["target_score"]) + ] + aimove = Move.from_gtp(random.choice(sel_moves)["move"], player=cn.next_player) + ai_thoughts += f"Balance strategy selected moves {sel_moves} based on target score and max points lost, and randomly chose {aimove.gtp()}." + elif "jigo" in ai_mode and candidate_ai_moves[0]["move"] != "pass": + sign = cn.player_sign(cn.next_player) + jigo_move = min(candidate_ai_moves, key=lambda move: abs(sign * move["scoreLead"] - ai_settings["target_score"])) + aimove = Move.from_gtp(jigo_move["move"], player=cn.next_player) + ai_thoughts += f"Jigo strategy found candidate moves {candidate_ai_moves} moves and chose {aimove.gtp()} as closest to 0.5 point win" + else: + if "default" not in ai_mode and "katago" not in ai_mode: + game.katrain.log(f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO) + ai_thoughts += f"Strategy {ai_mode} not found or unexpected fallback." + aimove = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player) + ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move" game.katrain.log(f"AI thoughts: {ai_thoughts}", OUTPUT_DEBUG) try: played_node = game.play(aimove) @@ -160,3 +161,4 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode return aimove, played_node except IllegalMoveException as e: game.katrain.log(f"AI Strategy {ai_mode} generated illegal move {aimove.gtp()}: {e}", OUTPUT_ERROR) + return None, None diff --git a/core/engine.py b/core/engine.py index efeee833..5fb6915d 100644 --- a/core/engine.py +++ b/core/engine.py @@ -113,7 +113,7 @@ def send_query(self, query, callback, error_callback, next_move=None): query["id"] = f"QUERY:{str(self.query_counter)}" self.queries[query["id"]] = (callback, error_callback, time.time(), next_move) if self.katago_process: - self.katrain.log(f"Sending query {query['id']}: {str(query)}", OUTPUT_DEBUG) + self.katrain.log(f"Sending query {query['id']}: {json.dumps(query)}", OUTPUT_DEBUG) try: self.katago_process.stdin.write((json.dumps(query) + "\n").encode()) self.katago_process.stdin.flush() diff --git a/core/game.py b/core/game.py index fd636ef8..6692ce59 100644 --- a/core/game.py +++ b/core/game.py @@ -154,10 +154,8 @@ def switch_branch(self, direction): def place_handicap_stones(self, n_handicaps): board_size_x, board_size_y = self.board_size - near_x = 3 if board_size_x >= 13 else 2 - near_y = 3 if board_size_y >= 13 else 2 - if board_size_x < 3 or board_size_y < 3: - return + near_x = 3 if board_size_x >= 13 else min(2, board_size_x - 1) + near_y = 3 if board_size_y >= 13 else min(2, board_size_y - 1) far_x = board_size_x - 1 - near_x far_y = board_size_y - 1 - near_y middle_x = board_size_x // 2 # what for even sizes? @@ -176,7 +174,7 @@ def place_handicap_stones(self, n_handicaps): if n_handicaps % 2 == 1: stones.append((middle_x, middle_y)) stones += [(near_x, middle_y), (far_x, middle_y), (middle_x, near_y), (middle_x, far_y)] - self.root.set_property("AB", [Move(stone).sgf(board_size=(board_size_x, board_size_y)) for stone in stones[:n_handicaps]]) + self.root.set_property("AB", list({Move(stone).sgf(board_size=(board_size_x, board_size_y)) for stone in stones[:n_handicaps]})) @property def board_size(self): diff --git a/core/sgf_parser.py b/core/sgf_parser.py index bca1459c..c3e59b9b 100644 --- a/core/sgf_parser.py +++ b/core/sgf_parser.py @@ -9,7 +9,7 @@ class ParseError(Exception): class Move: - GTP_COORD = list("ABCDEFGHJKLMNOPQRSTUVWXYZ") + ["A" + c for c in "ABCDEFGHJKLMNOPQRSTUVWXYZ"] # kata board size 29 support + GTP_COORD = list("ABCDEFGHJKLMNOPQRSTUVWXYZ") + [xa + c for xa in "AB" for c in "ABCDEFGHJKLMNOPQRSTUVWXYZ"] # kata board size 29 support PLAYERS = "BW" SGF_COORD = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ".lower()) + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ") @@ -79,10 +79,11 @@ def order_children(children): def sgf(self, **xargs) -> str: """Generates an SGF, calling sgf_properties on each node with the given xargs, so it can filter relevant properties if needed.""" - import sys + if self.is_root: + import sys - bszx, bszy = self.board_size - sys.setrecursionlimit(max(sys.getrecursionlimit(), 3 * bszx * bszy)) # thanks to lightvector for causing stack overflows ;) + bszx, bszy = self.board_size + sys.setrecursionlimit(max(sys.getrecursionlimit(), 4 * bszx * bszy)) sgf_str = "".join([prop + "".join(f"[{v}]" for v in values) for prop, values in self.sgf_properties(**xargs).items() if values]) if self.children: children = [c.sgf(**xargs) for c in self.order_children(self.children)] @@ -211,15 +212,17 @@ def play(self, move) -> "SGFNode": @property def next_player(self): - if "B" in self.properties or "AB" in self.properties: + if "B" in self.properties or "AB" in self.properties: # root or black moved return "W" - return "B" + else: + return "B" @property def player(self): - if "W" in self.properties: - return "W" - return "B" + if "B" in self.properties or "AB" in self.properties: + return "B" + else: + return "W" # nb root is considered white played if no handicap stones are placed class SGF: diff --git a/gui/popups.py b/gui/popups.py index bb9e8ddd..888d5d3d 100644 --- a/gui/popups.py +++ b/gui/popups.py @@ -173,13 +173,12 @@ def restart_engine(_dt): old_proc = old_engine.katago_process if old_proc: old_engine.shutdown(finish=True) - new_engine = KataGoEngine(self.katrain, self.config["engine"]) self.katrain.engine = new_engine self.katrain.game.engines = {"B": new_engine, "W": new_engine} if not old_proc: self.katrain.game.analyze_all_nodes() # old engine was broken, so make sure we redo any failures - + self.katrain.update_state() Clock.schedule_once(restart_engine, 0) self.katrain.debug_level = self.config["debug"]["level"]