diff --git a/.gitignore b/.gitignore index 40310c6..42e313b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ log.txt *.sgf sgfout sgf_selfplay +sgf_ogs log* tmp.pickle my diff --git a/ai.py b/ai.py index 7061950..63a1adb 100644 --- a/ai.py +++ b/ai.py @@ -56,8 +56,9 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode ai_thoughts += f"Top policy move has weight > {ai_settings['pick_override']:.1%}, so overriding other strategies." elif top_5_pass or "weighted" in ai_mode: weighted_coords = [(policy_grid[y][x], policy_grid[y][x], x, y) for x in range(size[0]) for y in range(size[1]) if policy_grid[y][x] > 0] - aimove = Move(weighted_selection_without_replacement(weighted_coords, 1)[0][2:], player=cn.next_player) # just take a random move by policy w/o noise - ai_thoughts += f"Playing policy-weighted random move {aimove.gtp()}" + (" because one of them is pass." if top_5_pass else " because strategy is weighted.") + best = weighted_selection_without_replacement(weighted_coords, 1)[0] + aimove = Move(best[2:], player=cn.next_player) # just take a random move by policy w/o noise + ai_thoughts += f"Playing policy-weighted random move {aimove.gtp()} ({best[0]:.1%})" + (" because one of them is pass." if top_5_pass else " because strategy is weighted.") elif "noise" in ai_mode: noise_str = ai_settings["noise_strength"] d_noise = dirichlet_noise(len(legal_policy_moves)) @@ -69,9 +70,9 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode n_moves = int(ai_settings["pick_frac"] * len(legal_policy_moves) + ai_settings["pick_n"]) if "influence" in ai_mode or "territory" in ai_mode: if "influence" in ai_mode: - weight = lambda x, y: (1/ai_settings["line_weight"]) ** max(0, 3 - min(size[0] - 1 - x, x, y, size[1] - 1 - y)) + weight = lambda x, y: (1 / ai_settings["line_weight"]) ** max(0, 3 - min(size[0] - 1 - x, x, y, size[1] - 1 - y)) else: - weight = lambda x, y:(1/ai_settings["line_weight"]) ** max(0, min(size[0] - 1 - x, x, y, size[1] - 1 - y) - 2) + weight = lambda x, y: (1 / ai_settings["line_weight"]) ** max(0, min(size[0] - 1 - x, x, y, size[1] - 1 - y) - 2) weighted_coords = [(policy_grid[y][x] * weight(x, y), weight(x, y), x, y) for x in range(size[0]) for y in range(size[1]) if policy_grid[y][x] > 0] ai_thoughts += f"Generated weights for {ai_mode} according to weight factor {ai_settings['line_weight']} and distance from 4th line. " elif "local" in ai_mode or "tenuki" in ai_mode: @@ -120,7 +121,7 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode 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) # TODO check - jigo_move = min(candidate_ai_moves, key=lambda move: abs(sign * move["scoreLead"] - ai_settings['target_score'])) + 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: diff --git a/ai2gtp.py b/ai2gtp.py index ebff032..351b24f 100644 --- a/ai2gtp.py +++ b/ai2gtp.py @@ -27,14 +27,7 @@ ENGINE_SETTINGS = { "enable_ownership": False, "threads": 1, } -ai_settings = { - "noise_strength": 0.8, - "pick_n": 10, - "pick_frac": 0.2, - "stddev": 10, - "line_weight": 10, - "pick_override": 0.95, -} +ai_settings = {"noise_strength": 0.8, "pick_n": 10, "pick_frac": 0.2, "stddev": 10, "line_weight": 10, "pick_override": 0.95} engine = KataGoEngine(logger, ENGINE_SETTINGS) @@ -53,7 +46,7 @@ ai_settings["pick_n"] = 5 # dropping below 7k at 5/0.33 ai_strategy = "P+Weighted" -ai_settings = {"pick_override": 0.95} +ai_settings = {"pick_override": 0.95} ai_strategy = "P+Local" ai_settings["pick_frac"] = 0.0 diff --git a/ai_performance.pickle b/ai_performance.pickle index b8a8140..b6e755f 100644 Binary files a/ai_performance.pickle and b/ai_performance.pickle differ diff --git a/engine.py b/engine.py index 807e0c6..fcd57b1 100644 --- a/engine.py +++ b/engine.py @@ -80,7 +80,7 @@ class KataGoEngine: self.katrain.log(f"Query result {analysis['id']} discarded -- recent new game?", OUTPUT_DEBUG) continue if "error" in analysis: - if not (next_move is None and "Illegal move" in analysis["error"]): # sweep + if not (next_move and "Illegal move" in analysis["error"]): # sweep self.katrain.log(f"{analysis} received from KataGo", OUTPUT_ERROR) continue else: diff --git a/game.py b/game.py index f06e7f0..19218ee 100644 --- a/game.py +++ b/game.py @@ -5,6 +5,7 @@ from datetime import datetime from typing import List, Union, Dict import threading +from common import var_to_grid from engine import KataGoEngine from game_node import GameNode from sgf_parser import SGF, Move @@ -224,7 +225,13 @@ class Game: return elif mode == "sweep": board_size_x, board_size_y = self.board_size - analyze_moves = [Move(coords=(x, y), player=cn.next_player) for x in range(board_size_x) for y in range(board_size_y) if (x, y) not in stones] + policy_grid = var_to_grid(self.current_node.policy, size=(board_size_x, board_size_y)) if self.current_node.policy else None + analyze_moves = [ + Move(coords=(x, y), player=cn.next_player) + for x in range(board_size_x) + for y in range(board_size_y) + if (policy_grid is None and (x, y) not in stones) or policy_grid[y][x] >= 0 + ] visits = int(engine.config["max_visits"] * self.config["sweep_visits_frac"] + 0.5) self.katrain.controls.set_status(f"Refining analysis of entire board to {visits} visits") priority = -1_000_000_000 diff --git a/game_node.py b/game_node.py index a36ee71..04bd7f0 100644 --- a/game_node.py +++ b/game_node.py @@ -43,7 +43,8 @@ class GameNode(SGFNode): def set_analysis(self, analysis_json, refine_move): if refine_move: - self.update_move_analysis(analysis_json["rootInfo"], refine_move.gtp()) + pvtail = analysis_json["moveInfos"][0]["pv"] if analysis_json["moveInfos"] else [] + self.update_move_analysis({"pv": [refine_move.gtp()] + pvtail, **analysis_json["rootInfo"]}, refine_move.gtp()) else: for move_analysis in analysis_json["moveInfos"]: self.update_move_analysis(move_analysis, move_analysis["move"]) @@ -134,7 +135,7 @@ class GameNode(SGFNode): def policy_ranking(self) -> Optional[List[Tuple[float, Move]]]: # return moves from highest policy value to lowest if self.policy: szx, szy = self.board_size - policy_grid = var_to_grid(self.policy, size=[szx, szy]) + policy_grid = var_to_grid(self.policy, size=(szx, szy)) moves = [(policy_grid[y][x], Move((x, y), player=self.next_player)) for x in range(szx) for y in range(szy)] moves.append((self.policy[-1], Move(None, player=self.next_player))) return sorted(moves, key=lambda mp: -mp[0]) diff --git a/gui/controls.py b/gui/controls.py index 19a4890..5e7f7eb 100644 --- a/gui/controls.py +++ b/gui/controls.py @@ -64,7 +64,7 @@ class Controls(BoxLayout): self.win_rate.text = current_node.format_win_rate() if move and next_player_is_human_or_both_robots: # don't immediately hide this when an ai moves comes in points_lost = current_node.points_lost - self.score_change.label = f"Points lost for {move.player}" if points_lost and points_lost > 0 else f"Points gained for {move.player}" + self.score_change.label = f"Points lost ({move.player})" if points_lost and points_lost > 0 else f"Points gained ({move.player})" self.score_change.text = f"{abs(points_lost):.1f}" if points_lost else "..." elif not current_player_is_ai_playing_human: self.score_change.label = f"Points lost" diff --git a/gui/kivyutils.py b/gui/kivyutils.py index 2596411..b2463e1 100644 --- a/gui/kivyutils.py +++ b/gui/kivyutils.py @@ -24,9 +24,11 @@ class DarkLabel(Label): class ScaledLightLabel(DarkLabel): pass + class LightHelpLabel(ScaledLightLabel): pass + class StyledButton(Button): button_color = ListProperty([]) button_color_down = ListProperty([]) @@ -79,7 +81,7 @@ class ToggleButtonContainer(GridLayout): def _size(self, _dt): if self.autosize: for tb in self.children: - tb.size_hint = (tb.texture_size[0] + 25, 1) + tb.size_hint = (tb.texture_size[0] + 10, 1) @property def value(self): diff --git a/gui/popups.py b/gui/popups.py index 265b499..3bdd27d 100644 --- a/gui/popups.py +++ b/gui/popups.py @@ -8,7 +8,17 @@ from kivy.uix.label import Label from common import OUTPUT_DEBUG, OUTPUT_ERROR from engine import KataGoEngine from game import Game, GameNode -from gui.kivyutils import LabelledCheckBox, LabelledFloatInput, LabelledIntInput, LabelledObjectInputArea, LabelledSpinner, LabelledTextInput, ScaledLightLabel, StyledButton, LightHelpLabel +from gui.kivyutils import ( + LabelledCheckBox, + LabelledFloatInput, + LabelledIntInput, + LabelledObjectInputArea, + LabelledSpinner, + LabelledTextInput, + ScaledLightLabel, + StyledButton, + LightHelpLabel, +) class InputParseError(Exception): @@ -16,7 +26,7 @@ class InputParseError(Exception): class QuickConfigGui(BoxLayout): - def __init__(self, katrain, popup, initial_values=None,**kwargs): + def __init__(self, katrain, popup, initial_values=None, **kwargs): super().__init__(**kwargs) self.katrain = katrain self.popup = popup @@ -65,7 +75,7 @@ class LoadSGFPopup(BoxLayout): class NewGamePopup(QuickConfigGui): def __init__(self, katrain, popup, properties, **kwargs): properties["RU"] = KataGoEngine.get_rules(katrain.game.root) - super().__init__(katrain, popup, properties,**kwargs) + super().__init__(katrain, popup, properties, **kwargs) self.rules_spinner.values = list(set(self.katrain.engine.RULESETS.values())) self.rules_spinner.text = properties["RU"] @@ -82,11 +92,11 @@ class NewGamePopup(QuickConfigGui): class ConfigPopup(QuickConfigGui): - def __init__(self, katrain, popup, config, ignore_cats,**kwargs): + def __init__(self, katrain, popup, config, ignore_cats, **kwargs): self.config = config self.ignore_cats = ignore_cats self.orientation = "vertical" - super().__init__(katrain, popup,**kwargs) + super().__init__(katrain, popup, **kwargs) Clock.schedule_once(self._build, 0) def _build(self, _): @@ -161,22 +171,22 @@ class ConfigPopup(QuickConfigGui): class ConfigAIPopup(QuickConfigGui): def __init__(self, katrain, popup, ai_modes, **kwargs): - super().__init__(katrain, popup, katrain.ai_settings, **kwargs) + super().__init__(katrain, popup, katrain.ai_settings, **kwargs) self.settings = self.katrain.ai_settings self.ai_modes = ai_modes Clock.schedule_once(self._build, 0) - self.orientation ='vertical' + self.orientation = "vertical" - def _build(self,_dt): + def _build(self, _dt): colbox = BoxLayout(spacing=5) for mode in self.ai_modes: mode_settings = self.settings[mode] num_rows = len(mode_settings) - 2 - column = GridLayout(cols=2, rows=max(num_rows,4) + 3,spacing=1,padding=3) + column = GridLayout(cols=2, rows=max(num_rows, 4) + 3, spacing=1, padding=3) column.add_widget(ScaledLightLabel(text=f"Settings for AI")) column.add_widget(ScaledLightLabel(text=f"{mode}", bold=True)) - column.add_widget(LightHelpLabel(size_hint=(1,3),text=mode_settings.get('_help_left',''))) - column.add_widget(LightHelpLabel(size_hint=(1,3),text=mode_settings.get('_help_right', ''))) + column.add_widget(LightHelpLabel(size_hint=(1, 3), text=mode_settings.get("_help_left", ""))) + column.add_widget(LightHelpLabel(size_hint=(1, 3), text=mode_settings.get("_help_right", ""))) for k, v in mode_settings.items(): if not k.startswith("_"): column.add_widget(ScaledLightLabel(text=f"{k}")) @@ -185,9 +195,9 @@ class ConfigAIPopup(QuickConfigGui): column.add_widget(ScaledLightLabel(text=f"")) column.add_widget(ScaledLightLabel(text=f"")) colbox.add_widget(column) - if len(self.ai_modes)==1: + if len(self.ai_modes) == 1: colbox.add_widget(ScaledLightLabel(text=f"")) - bl = BoxLayout(size_hint=(1,0.2),spacing=2) + bl = BoxLayout(size_hint=(1, 0.2), spacing=2) bl.add_widget(StyledButton(text=f"Apply", on_press=lambda _: self.update_config(False))) bl.add_widget(StyledButton(text=f"Apply and Save", on_press=lambda _: self.update_config(True))) self.add_widget(colbox) @@ -197,7 +207,7 @@ class ConfigAIPopup(QuickConfigGui): try: for k, v in self.collect_properties(self).items(): k1, k2 = k.split("/") - print(k1,k2,v,self.settings[k1][k2]) + print(k1, k2, v, self.settings[k1][k2]) if self.settings[k1][k2] != v: self.katrain.log(f"Updating setting {k} = {v}", OUTPUT_DEBUG) diff --git a/katrain.kv b/katrain.kv index 62d1e39..810b55c 100644 --- a/katrain.kv +++ b/katrain.kv @@ -36,7 +36,7 @@ : text: self.values[0] if self.values else '' - font_size: self.size[1] * 0.35 + font_size: self.size[1] * 0.33 sync_height: True background_color: [*[c*255/88 for c in BUTTON_COLOR[:3]], 1] # compensate for texture option_cls: 'StyledSpinnerOption' diff --git a/katrain.py b/katrain.py index e9f3e2d..0682196 100644 --- a/katrain.py +++ b/katrain.py @@ -193,7 +193,7 @@ class KaTrainGui(BoxLayout): def _do_config_popup(self): config_popup = Popup(title="Edit Settings", size_hint=(0.9, 0.9)) - popup_contents = ConfigPopup(self, config_popup, dict(self._config), ignore_cats=("board_ui","ai")) + popup_contents = ConfigPopup(self, config_popup, dict(self._config), ignore_cats=("board_ui", "ai")) config_popup.add_widget(popup_contents) config_popup.open() diff --git a/selfplay.py b/selfplay.py index 5851246..c96510e 100644 --- a/selfplay.py +++ b/selfplay.py @@ -143,18 +143,9 @@ test_ais = [ AI("P+Local", {"local_stddev": 5}), AI("P+Local", {"local_stddev": 1}), AI("P+Local", {"local_stddev": 1, "pick_frac": 0.0, "pick_n": 20}), + AI("P+Weighted", {"pick_override": 1.0}), ] -test_ais = [ - AI("Policy", {}), - AI("P+Weighted", {'pick_override':1.0}), - AI("P+Noise", {"noise_strength": 0.6}), - AI("P+Noise", {"noise_strength": 0.7}), - AI("P+Noise", {"noise_strength": 0.8}), - AI("P+Noise", {"noise_strength": 0.9}), -] - - # test_ais = [ # AI("Policy", {}),