diff --git a/README.md b/README.md index 72f7c44..991f745 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,20 @@ but has since grown to include a wide range of features, including: * Play against a wide range of weakened versions of AI with various styles. * Play against a stronger player and use the retry option instead of handicap stones. -![screenshot](https://imgur.com/t3Im6Xu.png) +## Screenshots + +| Analyze games | Play against an AI Teacher | +| ------------- | ------------- | +| ![screenshot](img/screenshot_analyze.png) | ![screenshot](img/screenshot_play.png) | + +## Quickstart +* Right-click any button you don't understand for help. +* To analyze a game, load it using the button in the top right, or press `ctrl-L` +* To play against AI, pick an AI from the drop down a color and either 'human' or 'teach' for yourself and start playing. + * For different board sizes, use the button with the little goban in the bottom right for a new game. + + ## Installation ### Quick Installation for Windows users @@ -35,70 +47,80 @@ but has since grown to include a wide range of features, including: * Executables for Mac are not available, so compiling from source code is required there. * Start the app by running `python katrain.py`. Note that the program can be slow to initialize the first time, due to KataGo's GPU tuning. -## Quickstart -* To analyze a game, load it using the button in the top right, or press `ctrl-L` -* To play against AI, pick an AI from the drop down a color and either 'human' or 'teach' for yourself and start playing. - * For different board sizes, use the button with the little goban in the bottom right for a new game. - + ## Manual -[Screenshot] ### Play - +Under the 'play' tab you can select who is playing black and white. * Human is simple play with potential feedback, but without auto-undo. * Teach will give you instant feedback, and auto-undo bad moves to give you a second chance. - * Settings for this mode can be found under 'Configure teacher' + * Settings for this mode can be found under 'Configure Teacher' * AI will activate the AI in the dropdown next to the buttons. - * Settings for the selected AI(s) can be found under 'Configure AI' + * Settings for the selected AI(s) can be found under 'Configure AIs' + +If you do not want to see 'Points lost' or other feedback for your moves, + set 'show last n dots' to 0 under 'Configure Teacher', and click on the words 'Points lost' to hide its value. #### AIs Available AIs, with strength indicating an estimate for the default settings, are: -* **9p**: **Default** is full KataGo, above professional level. +* **[9p+]** **Default** is full KataGo, above professional level. * **Balance** is KataGo occasionally making weaker moves, attempting to win by ~2 points. * **Jigo** is KataGo aggressively making weaker moves, attempting to win by 0.5 points. -* **~4d**: **Policy** is the top move from the policy network (it's 'shape sense' without reading), should be around high dan level depending on the model used. -* **~1d**: **P:Weighted** will pick a random move weighted by the policy, as long as it's above `lower_bound`. `weaken_fac` uses policy^(1/weaken_fac), increasing the chance for weaker moves. -* **~5k**: **P:Pick** will pick a `pick_n + pick_frac * ` moves at random, and play the best move among them. +* **[~4d]** **Policy** uses the top move from the policy network (it's 'shape sense' without reading), should be around high dan level depending on the model used. +* **[~3k]**: **P:Weighted** picks a random move weighted by the policy, as long as it's above `lower_bound`. `weaken_fac` uses `policy^(1/weaken_fac)`, increasing the chance for weaker moves. +* **[~5k]**: **P:Pick** picks `pick_n + pick_frac * ` moves at random, and play the best move among them. The setting `pick_override` determines the minimum value at which this process is bypassed to play the best move instead, preventing obvious blunders. - This is probably the best choice for kyu players who want a chance of winning. Variants of this strategy include: - * **~3k**: **P:Local** will pick such moves biased towards the last move with probability related to `local_stddev`. - * **~10k**: **~P:Tenuki** is biased in the opposite way as P:Local, using the same setting. - * **~7k**: P:Influence is biased towards 4th+ line moves, with every line below that dividing both the chance of considering the move and the policy value by `influence_weight`. Consider setting `pick_frac=1.0` to only affect the policy weight. - * **~7k**: P:Territory is biased in the opposite way, towards 1-3rd line moves, using the same setting. -* * **~7k**: P:Noise mixes the policy with `noise_strength` Dirichlet noise. At `noise_strength=0.9` play is near-random, while `noise_strength=0.7` is still quite strong. Regardless, mistakes are typically strange can include senseless first-line moves. -* `` pauses AI moves, in case you want to do analysis without triggering moves, or simply hide the evaluation dots for this player. + This, along with 'Weighted' are probably the best choice for kyu players who want a chance of winning without playing the sillier bots below. Variants of this strategy include: + * **[~3k]**: **P:Local** will pick such moves biased towards the last move with probability related to `local_stddev`. + * **[~7k]**: **~P:Tenuki** is biased in the opposite way as P:Local, using the same setting. + * **[~7k]**: **P:Influence** is biased towards 4th+ line moves, with every line below that dividing both the chance of considering the move and the policy value by `influence_weight`. Consider setting `pick_frac=1.0` to only affect the policy weight. + * **[~7k]**: **P:Territory** is biased in the opposite way, towards 1-3rd line moves, using the same setting. +* * **[~7k]**: **P:Noise** mixes the policy with `noise_strength` Dirichlet noise. At `noise_strength=0.9` play is near-random, while `noise_strength=0.7` is still quite strong. A threshold setting is included to avoid senseless first-line moves. +* ****: pauses AI moves, in case you want to do analysis without triggering moves, or simply hide the evaluation dots for this player. Selecting the AI as either white or black opens up the option to configure it under 'Configure AI'. ### Analysis -* The checkboxes have the following keyboard shortscuts, and they configure: +* The checkboxes have the following keyboard shortcuts, and they configure: * **[q]**: Child moves are shown. On by default, can turn it off to avoid obscuring other information or when wanting to guess the next move. - * **[w]**: All dots: Show all evaluation dots instead of the last few. You can configure how many are shown with thsi setting off under 'Configure Teacher'. - * **[e]**: + * **[w]**: All dots: Show all evaluation dots instead of the last few. + * You can configure how many are shown with this setting off, and whether they are shown for AIs under 'Play/Configure Teacher'. + * **[e]**: Top moves: Show the next moves KataGo considered, colored by their expected point loss. Small dots indicate high uncertainty. Hover over any of them to see the principal variation. + * **[r]**: Show owner: Show expected ownership of each square. + * **[t]**: NN Policy: Show KataGo's policy network evaluation, i.e. where it thinks the best next move is purely from the position, and in the absence of any 'reading'. + +* The analysis buttons have the following keyboard shortcuts, and they do: + * **[a]**: Extra: Re-evaluate the position using more visits, usually resulting in a more accurate evaluation. + * **[s]**: Equalize: Re-evaluate all currently shown next moves with the same visits as the current top move. Useful to increase confidence in the suggestions with high uncertainty. + * **[d]**: Sweep: Evaluate all possible next moves. This can take a bit of time, but the result is nothing if not colourful. + - - ### Keyboard shortcuts +In addition to shortcuts mentioned above, there are: -In addition to these, there are: - -* Tab to switch between analysis and play modes. (NB. keyboard shortcuts function regardless) -* ~ or ` or p : Hide side panel UI and only show the board. -* Ctrl-v : Load SGF from clipboard -* Ctrl-c : Save SGF to clipboard -* Ctrl-l : Load SGF from file -* Ctrl-s : Load SGF to file -* Ctrl-n : Load SGF from clipboard +* **[Tab]**: to switch between analysis and play modes. (NB. keyboard shortcuts function regardless) +* **[~]** or **[`]** or **[p]**: Hide side panel UI and only show the board. +* **[enter]**: AI Move +* **[arrow up]** or **[z]**: Undo move. Hold shift for 10 moves at a time, or ctrl to skip to thte start. +* **[arrow down]** or **[x]**: Redo move. Hold shift for 10 moves at a time, or ctrl to skip to thte start. +* **[scroll up]**: Undo move. Only works with the mouse pointer on the board. +* **[scroll down]**: Redo move. +* **[Ctrl-v]**: Load SGF from clipboard +* **[Ctrl-c]**: Save SGF to clipboard +* **[Ctrl-l]**: Load SGF from file +* **[Ctrl-s]**: Load SGF to file +* **[Ctrl-n]**: Load SGF from clipboard +* **[space]**: Pass ### Configuration Configuration is stored in `config.json`. Most settings are now available to edit in the program, but - some cosmetic options are now. + some cosmetic options are not. You can use `python katrain.py your_config_file.json` to use another config file instead. If you ever need to reset to the original settings, simply re-download the `config.json` file in this repository. @@ -108,7 +130,7 @@ If you ever need to reset to the original settings, simply re-download the `conf * The program is slow to start! * The first startup of KataGo can be slow, after that it should be much faster. * The program is running too slowly! - * Lower the visits count in the `max_visits` block of `config.json` by half or so and try again. + * Adjust the number of visits or maximum time allowed in the settings. ## Contributing diff --git a/config.json b/config.json index 0912dd2..522ba5b 100644 --- a/config.json +++ b/config.json @@ -124,11 +124,6 @@ "line_weight": 5, "_help_right": "Samples `pick_n + pick_frac * ` and plays the best one, biased to below the `threshold` line.", "_help_left": "Increase `line_weight` to penalize moves closer to the center more." - }, - "": { - "_help_right": "--Edward Lasker", - "_help_left": "The rules of go are so elegant, organic, and rigorously logical that if intelligent life forms exist elsewhere in the universe, they almost certainly play go." - } }, "board_ui": { diff --git a/game_node.py b/game_node.py index 72c65d7..406f6ac 100644 --- a/game_node.py +++ b/game_node.py @@ -19,13 +19,13 @@ class GameNode(SGFNode): self.move_number = 0 self.undo_threshold = random.random() # for fractional undos, store the random threshold in the move itself for consistency - def sgf_properties(self, save_comments_player, save_comments_class, eval_thresholds): + def sgf_properties(self, save_comments_player=None, save_comments_class=None, eval_thresholds=None): properties = copy.copy(super().sgf_properties()) - if self.points_lost: + if self.points_lost and save_comments_class is not None and eval_thresholds is not None: show_class = save_comments_class[evaluation_class(self.points_lost, eval_thresholds)] else: show_class = False - if save_comments_player.get(self.player, False) and show_class and self.analysis_ready: + if (save_comments_player or {}).get(self.player, False) and show_class and self.analysis_ready: candidate_moves = self.candidate_moves top_x = Move.from_gtp(candidate_moves[0]["move"]).sgf(self.board_size) best_sq = [Move.from_gtp(d["move"]).sgf(self.board_size) for d in candidate_moves[1:] if d["pointsLost"] <= 0.5] @@ -39,7 +39,7 @@ class GameNode(SGFNode): if self.is_root: properties["C"] = [ "Moves marked 'X' indicate the top move according to KataGo, those with a square are all moves which lose at most 0.5 points.\n" - + properties.get("C", "") + + "\n".join(properties.get("C", "")) + "\nSGF with review generated by KaTrain." ] return properties @@ -97,12 +97,16 @@ class GameNode(SGFNode): previous_top_move = self.parent.candidate_moves[0] if sgf or hints: if previous_top_move["move"] != single_move.gtp(): - text += f"Predicted top move was {previous_top_move['move']} ({self.format_score(previous_top_move['scoreLead'])}).\n" points_lost = self.points_lost if sgf and points_lost > 0.5: text += f"Estimated point loss: {points_lost:.1f}\n" + text += f"Predicted top move was {previous_top_move['move']} ({self.format_score(previous_top_move['scoreLead'])}).\n" else: text += f"Move was predicted best move.\n" + if sgf: + if previous_top_move.get("pv") and (sgf or hints): + text += f"PV: {' '.join(previous_top_move['pv'])}" + if sgf or hints or teach: policy_ranking = self.parent.policy_ranking policy_ix = [ix + 1 for (p, m), ix in zip(policy_ranking, range(len(policy_ranking))) if m == single_move] @@ -112,7 +116,7 @@ class GameNode(SGFNode): text += f"Top policy move was {policy_ranking[0][1].gtp()} ({policy_ranking[0][0]:.1%}).\n" if self.auto_undo and sgf: text += "Move was automatically undone in teaching mode." - if self.ai_thoughts: + if self.ai_thoughts and (sgf or hints): text += f"\nAI thought process: {self.ai_thoughts}" else: text = "No analysis available" if sgf else "Analyzing move..." diff --git a/gui/badukpan.py b/gui/badukpan.py index 6de3805..46dbce2 100644 --- a/gui/badukpan.py +++ b/gui/badukpan.py @@ -199,7 +199,6 @@ class BadukPanWidget(Widget): evalsize = 0 else: evalsize = min(1, max(0, realized_points_lost / points_lost)) - print(node.single_move, realized_points_lost, points_lost, evalsize) for m in node.move_with_placements: if has_stone.get(m.coords) and not drawn_stone.get(m.coords): # skip captures, last only for move_eval_on = show_dots_for.get(m.player) and (i < show_n_eval or full_eval_on) diff --git a/gui/controls.py b/gui/controls.py index 594fdf3..3f8dba2 100644 --- a/gui/controls.py +++ b/gui/controls.py @@ -65,9 +65,15 @@ class Controls(BoxLayout): points_lost = current_node.points_lost self.score_change.label = f"Points lost" if points_lost and points_lost > 0 else f"Points gained" self.score_change.text = f"{move.player}: {abs(points_lost):.1f}" if points_lost else "..." + print(current_player_is_ai_playing_human, move, next_player_is_human_or_both_robots) elif not current_player_is_ai_playing_human: + print(current_player_is_ai_playing_human, move, next_player_is_human_or_both_robots) self.score_change.label = f"Points lost" self.score_change.text = "" + elif current_player_is_ai_playing_human and current_node.parent and current_node.parent.single_move: + points_lost = current_node.parent.points_lost + self.score_change.label = f"Points lost" if points_lost and points_lost > 0 else f"Points gained" + self.score_change.text = f"{current_node.parent.single_move.player}: {abs(points_lost):.1f}" if points_lost else "..." elif both_players_are_robots and current_node.parent and current_node.parent.analysis_ready: self.score.text = current_node.parent.format_score() self.win_rate.text = current_node.parent.format_win_rate() diff --git a/gui/popups.py b/gui/popups.py index 7832758..88e5240 100644 --- a/gui/popups.py +++ b/gui/popups.py @@ -237,7 +237,6 @@ class ConfigTeacherPopup(QuickConfigGui): def __init__(self, katrain, popup, **kwargs): self.settings = katrain.config("trainer") self.sgf_settings = katrain.config("sgf") - print(self.sgf_settings, katrain.config("sgf")) self.ui_settings = katrain.config("board_ui") super().__init__(katrain, popup, self.settings, **kwargs) Clock.schedule_once(self._build, 0) diff --git a/img/screenshot_analyze.png b/img/screenshot_analyze.png new file mode 100644 index 0000000..c3f9e85 Binary files /dev/null and b/img/screenshot_analyze.png differ diff --git a/img/screenshot_play.png b/img/screenshot_play.png new file mode 100644 index 0000000..75b8a58 Binary files /dev/null and b/img/screenshot_play.png differ diff --git a/katrain.kv b/katrain.kv index b6bfa3e..708ce24 100644 --- a/katrain.kv +++ b/katrain.kv @@ -366,6 +366,7 @@ scroll_type: ['bars'] bar_width: 5 bar_color: BUTTON_COLOR + label: label canvas.before: Color: rgba: root.border_color @@ -378,6 +379,7 @@ rectangle: [self.pos[0]+1,self.pos[1]+2,self.width-3,self.height-3] width:1 Label: + id: label padding: 5, 5 font_size: dp(18) color: BLACK @@ -588,10 +590,12 @@ border_color: 0.5,0.1,0.1,1 opacity: 1 if self.text else 0 text: '' - size_hint_y: 0.00001 if not self.text else 0.66 + size_hint_y: None + height: 0.00001 if not self.text else min(0.66*self.parent.height,self.label.texture_size[1]) ScrollableLabel: id: info - size_hint: 1, 1 - status_label.size_hint_y + size_hint: 1, None + height: self.parent.height - status_label.height - 1 BoxLayout: orientation: 'horizontal' size_hint: 1, None diff --git a/katrain.py b/katrain.py index cab27a7..82a4a8d 100644 --- a/katrain.py +++ b/katrain.py @@ -91,14 +91,7 @@ class KaTrainGui(BoxLayout): auto_undo = cn.player and "undo" in self.controls.player_mode(cn.player) if auto_undo and cn.analysis_ready and cn.parent and cn.parent.analysis_ready: self.game.analyze_undo(cn, self.config("trainer")) # not via message loop - if ( - cn.analysis_ready - and "ai" in self.controls.player_mode(cn.next_player).lower() - and "pause" not in self.controls.ai_mode(cn.next_player).lower() - and not cn.children - and not self.game.ended - and not (auto_undo and cn.auto_undo is None) - ): + if cn.analysis_ready and "ai" in self.controls.player_mode(cn.next_player).lower() and not cn.children and not self.game.ended and not (auto_undo and cn.auto_undo is None): self._do_ai_move(cn) # cn mismatch stops this if undo fired. avoid message loop here or fires repeatedly. # Handle prisoners and next player display @@ -195,7 +188,7 @@ class KaTrainGui(BoxLayout): except ParseError as e: self.log(f"Failed to load SGF. Parse Error: {e}", OUTPUT_ERROR) return - self._do_new_game(move_tree=move_tree, analyze_fast=fileselect_popup.fast.active) + self._do_new_game(move_tree=move_tree, analyze_fast=popup_contents.fast.active) popup_contents.filesel.on_submit = readfile fileselect_popup.open() @@ -229,10 +222,11 @@ class KaTrainGui(BoxLayout): self.controls.set_status(msg) def on_touch_up(self, touch): - if touch.button == "scrollup": - self("redo") - elif touch.button == "scrolldown": - self("undo") + if self.board_gui.collide_point(*touch.pos) or self.board_controls.collide_point(*touch.pos): + if touch.button == "scrollup": + self("redo") + elif touch.button == "scrolldown": + self("undo") return super().on_touch_up(touch) def _on_keyboard_down(self, keyboard, keycode, text, modifiers):