diff --git a/ai.py b/ai.py index 3931570..1573d20 100644 --- a/ai.py +++ b/ai.py @@ -54,10 +54,10 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode elif policy_moves[0][0] > ai_settings["pick_override"]: aimove = top_policy_move ai_thoughts += f"Top policy move has weight > {ai_settings['pick_override']:.1%}, so overriding other strategies." - elif top_5_pass: + 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." + 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.") elif "noise" in ai_mode: noise_str = ai_settings["noise_strength"] d_noise = dirichlet_noise(len(legal_policy_moves)) @@ -65,13 +65,13 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode new_top = heapq.nlargest(5, noisy_policy_moves) aimove = new_top[0][1] ai_thoughts += f"Noisy policy strategy (strength={noise_str:.2f}) generated 5 moves {fmt_moves(new_top)} so picked {aimove.gtp()}. " - elif any(keyword in ai_mode for keyword in ["influence", "territory", "local", "tenuki", "pick"]): + elif "p+" in ai_mode: 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: 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: 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 +120,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"] - 0.5)) + 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 2962b28..fa20353 100644 --- a/ai2gtp.py +++ b/ai2gtp.py @@ -44,7 +44,7 @@ engine = KataGoEngine(logger, ENGINE_SETTINGS) ai_strategy = "P+Influence" ai_settings["pick_frac"] = 0.5 -ai_settings["line_weight"] = 0.1 +ai_settings["line_weight"] = 10 ai_strategy = "P+Local" ai_settings["pick_frac"] = 0.0 diff --git a/ai_performance.pickle b/ai_performance.pickle index 81552a6..b8a8140 100644 Binary files a/ai_performance.pickle and b/ai_performance.pickle differ diff --git a/config.json b/config.json index 2bddec5..60646c7 100644 --- a/config.json +++ b/config.json @@ -38,14 +38,42 @@ "eval_show_ai": false }, "ai": { - "Default": {}, - "Balance": {"target_score":2, "random_loss": 1, "max_loss": 5, "min_visits": 20}, - "P+Noise" : {"noise_strength": 0.8}, - "P+Pick": {"pick_override": 0.95, "pick_n":5, "pick_frac": 0.33}, - "P+Local": {"pick_override": 0.95, "stddev": 1.5, "pick_n":15, "pick_frac": 0.0}, - "P+Tenuki": {"pick_override": 0.95, "stddev": 10, "pick_n":5, "pick_frac": 0.33}, - "P+Influence": {"pick_override": 0.95, "pick_n":5, "pick_frac": 0.5,"line_weight": 0.1}, - "P+Territory": {"pick_override": 0.95, "pick_n":5, "pick_frac": 0.5,"line_weight": 0.1} + "Default": {"_help_right": "No settings available here, strength is mainly affected by `max_visits` and `model` in the main settings `engine` section.","_help_left": ""}, + "Balance": {"target_score":2, "random_loss": 1, "max_loss": 5, "min_visits": 20, + "_help_right": "Will try to win by `target_score`, lose at most `random_loss` when behind and `max_loss` when ahead.", + "_help_left": "Never plays moves with less than `min_visits` visits, so also check engine settings." + }, + "Jigo": {"target_score": 0.5, + "_help_right": "Will try to win by `target_score`, without further restrictions.", + "_help_left": "Also affected by engine settings such as `max_visits`." + }, + "Policy": {"_help_right":"No settings available for this mode, strength is mainly affected by `model` in engine settings, but should be high dan regardless.","_help_left": "" }, + "P+Weighted": {"_help_right":"Strength is mainly affected by `model` in engine settings.","_help_left": "`pick_override` determines when top move is chosen without randomness, and is effectively disabled by default (1.0).", + "pick_override": 1.0}, + "P+Noise" : {"pick_override": 0.95,"noise_strength": 0.8, + "_help_right": "Adds `noise_strength` noise to the policy and plays the top move.", + "_help_left": "Plays top move if policy value is above `pick_override` to avoid obvious mistakes. Noise above 0.9 is near random, below 0.7 is fairly strong." + }, + "P+Pick": {"pick_override": 0.95, "pick_n":5, "pick_frac": 0.33, + "_help_right": "Picks `pick_n + pick_frac * ` at random and plays the best one. Change `pick_frac` to make it see more moves.", + "_help_left": "Plays top move if policy value is above `pick_override` to avoid obvious mistakes." + }, + "P+Local": {"pick_override": 0.95, "stddev": 1.5, "pick_n":15, "pick_frac": 0.0, + "_help_right": "Samples `pick_n + pick_frac * ` near the last move and plays the best one.", + "_help_left": "Lower `stddev` makes it prefer closer moves." + }, + "P+Tenuki": {"pick_override": 0.95, "stddev": 10, "pick_n":5, "pick_frac": 0.25, + "_help_right": "Samples `pick_n + pick_frac * ` away from the last move and plays the best one.", + "_help_left": "Increase `stddev` makes it prefer moves further away." + }, + "P+Influence": {"pick_override": 0.95, "pick_n":5, "pick_frac": 0.4,"line_weight": 10, + "_help_right": "Samples `pick_n + pick_frac * ` and plays the best one, biased towards the center.", + "_help_left": "Increase `line_weight` to penalize moves near the edge more." + }, + "P+Territory": {"pick_override": 0.95, "pick_n":5, "pick_frac": 0.4,"line_weight": 10, + "_help_right": "Samples `pick_n + pick_frac * ` and plays the best one, biased towards the edge.", + "_help_left": "Increase `line_weight` to penalize moves closer to the center more." + } }, "board_ui": { "starpoint_size": 0.1, diff --git a/gui/kivyutils.py b/gui/kivyutils.py index 96cebb3..2596411 100644 --- a/gui/kivyutils.py +++ b/gui/kivyutils.py @@ -24,6 +24,8 @@ class DarkLabel(Label): class ScaledLightLabel(DarkLabel): pass +class LightHelpLabel(ScaledLightLabel): + pass class StyledButton(Button): button_color = ListProperty([]) diff --git a/gui/popups.py b/gui/popups.py index d7d88e9..265b499 100644 --- a/gui/popups.py +++ b/gui/popups.py @@ -8,7 +8,7 @@ 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 +from gui.kivyutils import LabelledCheckBox, LabelledFloatInput, LabelledIntInput, LabelledObjectInputArea, LabelledSpinner, LabelledTextInput, ScaledLightLabel, StyledButton, LightHelpLabel class InputParseError(Exception): @@ -16,8 +16,8 @@ class InputParseError(Exception): class QuickConfigGui(BoxLayout): - def __init__(self, katrain, popup, initial_values=None): - super().__init__() + def __init__(self, katrain, popup, initial_values=None,**kwargs): + super().__init__(**kwargs) self.katrain = katrain self.popup = popup if initial_values: @@ -65,7 +65,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) + 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 +82,11 @@ class NewGamePopup(QuickConfigGui): class ConfigPopup(QuickConfigGui): - def __init__(self, katrain, popup, config, ignore_cats): + def __init__(self, katrain, popup, config, ignore_cats,**kwargs): self.config = config self.ignore_cats = ignore_cats self.orientation = "vertical" - super().__init__(katrain, popup) + super().__init__(katrain, popup,**kwargs) Clock.schedule_once(self._build, 0) def _build(self, _): @@ -161,23 +161,50 @@ class ConfigPopup(QuickConfigGui): class ConfigAIPopup(QuickConfigGui): def __init__(self, katrain, popup, ai_modes, **kwargs): + super().__init__(katrain, popup, katrain.ai_settings, **kwargs) self.settings = self.katrain.ai_settings - super().__init__(katrain, popup, self.settings, **kwargs) self.ai_modes = ai_modes Clock.schedule_once(self._build, 0) + self.orientation ='vertical' - def _build(self): + def _build(self,_dt): + colbox = BoxLayout(spacing=5) for mode in self.ai_modes: mode_settings = self.settings[mode] - column = GridLayout(rows=2 + len(mode_settings), columns=2, size_hint=(0.5, 1)) - column.add_widget(ScaledLightLabel(text=f"Settings for AI {mode}", bold=True)) + num_rows = len(mode_settings) - 2 + 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', ''))) for k, v in mode_settings.items(): - column.add_widget(ScaledLightLabel(text=f"{k}")) - column.add_widget(ConfigPopup.type_to_widget_class(v)(text=str(v), input_property=f"{mode}/{k}")) + if not k.startswith("_"): + column.add_widget(ScaledLightLabel(text=f"{k}")) + column.add_widget(ConfigPopup.type_to_widget_class(v)(text=str(v), input_property=f"{mode}/{k}")) + for _ in range(4 - num_rows): + column.add_widget(ScaledLightLabel(text=f"")) + column.add_widget(ScaledLightLabel(text=f"")) + colbox.add_widget(column) + if len(self.ai_modes)==1: + colbox.add_widget(ScaledLightLabel(text=f"")) + 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) + self.add_widget(bl) - column.add_widget(Label(text=f"Settings for AI {mode}", bold=True)) + def update_config(self, save_to_file=False): + try: + for k, v in self.collect_properties(self).items(): + k1, k2 = k.split("/") + print(k1,k2,v,self.settings[k1][k2]) + if self.settings[k1][k2] != v: + self.katrain.log(f"Updating setting {k} = {v}", OUTPUT_DEBUG) - self.add_widget(column) + self.popup.dismiss() + except InputParseError as e: + self.info_label.text = str(e) + self.katrain.log(e, OUTPUT_ERROR) + return - def on_submit(self): self.popup.dismiss() diff --git a/katrain.kv b/katrain.kv index 9587ff6..62d1e39 100644 --- a/katrain.kv +++ b/katrain.kv @@ -1,8 +1,6 @@ #:kivy 1.11.0 #:import ew kivy.uix.effectwidget - -#:set AI_MODES ['Default','Balance','Jigo','Policy','P+Pick','P+Local','P+Tenuki','P+Territory','P+Influence','P+Noise',''] #:set PLAYER_MODES ['Human', 'Teach','AI:'] #:set PLAYER_MODE_VALUES ['human','human+undo','ai'] #:set BUTTON_COLOR [0.23, 0.30, 0.35, 1] @@ -91,6 +89,12 @@ color: WHITE font_size: 0.45*self.size[1] +: + font_size: self.size[1]/10 + halign: 'center' + valign: 'center' + multiline: True + font_size: 0.2*self.size[1] @@ -478,7 +482,7 @@ StyledButton: text: 'Configure AIs' on_press: root.configure_ais() - size_hint: 0.45,1 + size_hint: 0.44,1 Label: size_hint: None,1 width: 3 @@ -657,21 +661,13 @@ input_property: 'RU' id: rules_spinner Label: - ScaledLightLabel: - font_size: self.size[1]/8 + LightHelpLabel: text: "Use x:y (e.g. 19:9) to play on a non-square board." size_hint: 1,2 - halign: 'center' - valign: 'center' - multiline: True text_size: self.width-6, None id: info - ScaledLightLabel: + LightHelpLabel: text: "Note that handicaps above 9 are not supported on non-square boards." - font_size: self.size[1]/8 - halign: 'center' - valign: 'center' - multiline: True text_size: self.width-6, None id: info StyledButton: diff --git a/katrain.py b/katrain.py index 88a84d7..e9f3e2d 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")) + 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 4602cb8..5851246 100644 --- a/selfplay.py +++ b/selfplay.py @@ -45,7 +45,7 @@ class AI: "pick_n": 10, "pick_frac": 0.2, "local_stddev": 10, - "line_weight": 0.1, + "line_weight": 10, "pick_override": 0.95, } IGNORE_SETTINGS_IN_TAG = {"threads", "enable_ownership", "katago"} # katago for switching from/to bs version @@ -133,8 +133,8 @@ test_ais = [ AI("P+Pick", {"pick_frac": 0.5, "pick_n": 0}), AI("P+Influence", {"pick_frac": 0.2, "pick_n": 20}), AI("P+Territory", {"pick_frac": 0.2, "pick_n": 20}), - AI("P+Influence", {"pick_frac": 0.33, "line_weight": 0.05}), - AI("P+Territory", {"pick_frac": 0.33, "line_weight": 0.05}), + AI("P+Influence", {"pick_frac": 0.33, "line_weight": 20}), + AI("P+Territory", {"pick_frac": 0.33, "line_weight": 20}), AI("P+Pick", {"pick_frac": 0.0, "pick_n": 1}), AI("P+Tenuki", {"local_stddev": 20}), AI("P+Tenuki", {"local_stddev": 10}), @@ -145,6 +145,17 @@ test_ais = [ AI("P+Local", {"local_stddev": 1, "pick_frac": 0.0, "pick_n": 20}), ] +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", {}), # AI("P+Noise", {"noise_strength": 0.4}),