From 42b1df20157930974069951bb02153ea9d8a3dbb Mon Sep 17 00:00:00 2001 From: Sander Land Date: Sat, 30 May 2020 19:09:35 +0200 Subject: [PATCH] nearly ready! --- README.md | 44 +- i18n.py | 8 + katrain/__main__.py | 20 +- katrain/config.json | 2 +- katrain/core/ai.py | 37 +- katrain/core/constants.py | 3 +- katrain/core/game.py | 7 +- katrain/core/game_node.py | 4 +- katrain/gui.kv | 23 +- katrain/gui/badukpan.py | 2 +- .../i18n/locales/en/LC_MESSAGES/katrain.mo | Bin 10287 -> 10313 bytes .../i18n/locales/en/LC_MESSAGES/katrain.po | 4 + .../i18n/locales/haha/LC_MESSAGES/katrain.mo | Bin 10906 -> 10938 bytes .../i18n/locales/haha/LC_MESSAGES/katrain.po | 3 + .../i18n/locales/ko/LC_MESSAGES/katrain.mo | Bin 10663 -> 10688 bytes .../i18n/locales/ko/LC_MESSAGES/katrain.po | 4 + .../i18n/locales/nl/LC_MESSAGES/katrain.mo | Bin 10559 -> 0 bytes .../i18n/locales/nl/LC_MESSAGES/katrain.po | 688 ------------------ 18 files changed, 63 insertions(+), 786 deletions(-) delete mode 100644 katrain/i18n/locales/nl/LC_MESSAGES/katrain.mo delete mode 100644 katrain/i18n/locales/nl/LC_MESSAGES/katrain.po diff --git a/README.md b/README.md index 2e2d1b6..eec9cc6 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# KaTrain v1.0.6 +# KaTrain v1.1 [![Latest Release](https://img.shields.io/github/release/sanderland/katrain?label=download)](https://github.com/sanderland/katrain/releases) [![Latest version on PyPI](https://img.shields.io/pypi/v/katrain.svg)](https://pypi.org/project/katrain) ![License:MIT](https://img.shields.io/pypi/l/katrain) ![Build Status](https://github.com/sanderland/katrain/workflows/release/badge.svg) -[![Supported Python versions](https://img.shields.io/pypi/pyversions/katrain.svg)](#Installation) -[![Code style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +![GitHub Downloads](https://img.shields.io/github/downloads/sanderland/katrain/total?color=%23336699&label=github%20downloads) +[![PyPI Downloads](https://pepy.tech/badge/katrain)](https://pepy.tech/project/katrain) [![Discord](https://img.shields.io/discord/417022162348802048?logo=discord)](https://discord.com/channels/417022162348802048/629446365688365067) KaTrain is a tool for analyzing and playing go with AI feedback from KataGo. @@ -24,19 +24,11 @@ but has since grown to include a wide range of features, including: | ------------- | ------------- | | ![screenshot](katrain/img/anim_analyze.gif) | ![screenshot](katrain/img/anim_teach.gif) | -## Quickstart - -* You can right-click most button or checkbox labels to get a tooltip with help. -* To analyze a game, load it using the button in the bottom right, or press `ctrl-L` -* To play against AI, pick an AI from the dropdown 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. - - # Documentation ## Installation * See the [releases tab](https://github.com/sanderland/katrain/releases) for pre-built installers for windows. -* Alternatively use `pip3 install -U katrain` to install the latest version from PyPI on any OS. +* Alternatively use `pip3 install -U katrain` to install the latest version from PyPI on any 64-bit OS. * Note that on MacOS you will need to set up KataGo using brew, as described [here](INSTALL.md). * See [here](INSTALL.md#MacPrereq) for detailed instructions for running from source files on Window, Linux and MacOS, as well as setting up KataGo to use multiple GPUs. @@ -44,37 +36,27 @@ but has since grown to include a wide range of features, including: ## Manual ### Play +* Select the players in the main menu, or under 'New Game'. +* In a teaching game, KaTrain will analyze your moves and automatically undo those that are sufficiently bad. -Under the 'play' tab you can select who is playing black and white. +#### Instant feedback while playing. -* 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' -* AI will activate the AI in the dropdown menu next to the buttons. - * Settings for all AIs 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. - -#### What are all these coloured dots? - -The dots indicate how many points were lost by that move. +The dots on the move indicate how many points were lost by that move. * The colour indicates the size of the mistake according to KataGo * The size indicates if the mistake was actually punished. Going from fully punished at maximal size, to no actual effect on the score at minimal size. In short, if you are a weaker player you should mostly on large dots that are red or purple, -while stronger players can pay more attention to smaller mistakes. If you want to hide some colours, you -can do so under 'Configure Teacher'. +while stronger players can pay more attention to smaller mistakes. If you want to hide some colours +on the board or not output details for them in SGFs,you can do so under 'Configure Teacher'. #### AIs Available AIs, with strength indicating an estimate for the default settings based on their current OGS rankings, are: -* **[9p+]** **Default** is full KataGo, above professional level. -* **(RECOMMENDED)** **[~5k]** **ScoreLoss** is KataGo making moves with probability `~ e^(-strength * points lost)`, playing a varied style with small mistakes. -* **Balance** is KataGo occasionally making weaker moves, attempting to win by ~2 points. It can still win by more if you make too many mistakes. +* **[9p+]** **KataGo** is full KataGo, above professional level. +* **[~5k]** **ScoreLoss** is KataGo making moves with probability `~ e^(-strength * points lost)`, playing a varied style with small mistakes. * **Jigo** is KataGo aggressively making weaker moves, attempting to win by 0.5 points. * **[~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. There is a setting to increase variety in the opening, but otherwise it plays deterministically. * **(RECOMMENDED)** **[~3k]**: **P:Weighted** picks a random move weighted by the policy, @@ -175,5 +157,3 @@ In addition to shortcuts mentioned above, there are: * You can also contact me on [discord](https://discord.gg/AjTPFpN) (Sander#3278), [KakaoTalk](https://open.kakao.com/o/gTsMJCac) or [Reddit](http://reddit.com/u/sanderbaduk) to give feedback, or simply show your appreciation. * Some people have also asked me how to donate. Something go-related such as a book or teaching time is highly appreciated. -![GitHub Downloads](https://img.shields.io/github/downloads/sanderland/katrain/total?color=%23336699&label=github%20downloads) -[![PyPI Downloads](https://pepy.tech/badge/katrain)](https://pepy.tech/project/katrain) diff --git a/i18n.py b/i18n.py index 54c3237..5ef0b8d 100644 --- a/i18n.py +++ b/i18n.py @@ -9,6 +9,7 @@ locales = set(os.listdir(localedir)) print("locales found:", locales) strings_to_langs = defaultdict(dict) +strings_to_keys = defaultdict(dict) lang_to_strings = defaultdict(set) DEFAULT_LANG = "en" @@ -27,12 +28,19 @@ for lang in locales: num_todo[lang] += 1 else: strings_to_langs[entry.msgid][lang] = entry.msgstr + strings_to_keys[entry.msgid][lang] = set(re.findall("{.*?}",entry.msgstr)) lang_to_strings[lang].add(entry.msgid) if num_todo[lang]: print(f"{lang} has {num_todo[lang]} TODO entries") for lang in locales: + if lang != DEFAULT_LANG: + for msgid in lang_to_strings[lang]: + if DEFAULT_LANG in strings_to_keys[msgid] and strings_to_keys[msgid][lang] != strings_to_keys[msgid][DEFAULT_LANG]: + print(f"{msgid} has inconstent formatting keys for {lang}: ",strings_to_keys[msgid][lang],'is different from default', strings_to_keys[msgid][DEFAULT_LANG]) + errors=True + for msgid in strings_to_langs.keys() - lang_to_strings[lang]: if lang == DEFAULT_LANG: print("Message id", msgid, "found as ", strings_to_langs[msgid], "but missing in default", DEFAULT_LANG) diff --git a/katrain/__main__.py b/katrain/__main__.py index f451cb2..caa31a4 100644 --- a/katrain/__main__.py +++ b/katrain/__main__.py @@ -265,12 +265,6 @@ class KaTrainGui(Screen, KaTrainBase): self.fileselect_popup.open() def _do_output_sgf(self): - for pl in Move.PLAYERS: - if not self.game.root.get_property(f"P{pl}"): - _, model_file = os.path.split(self.engine.config["model"]) - self.game.root.set_property( - f"P{pl}", f"AI {self.controls.ai_mode(pl)} (KataGo { os.path.splitext(model_file)[0]})" if "ai" in self.controls.player_mode(pl) else "Player" - ) msg = self.game.write_sgf(self.config("general/sgf_save")) self.log(msg, OUTPUT_INFO) self.controls.set_status(msg) @@ -330,13 +324,7 @@ class KaTrainGui(Screen, KaTrainBase): first_child.dismiss() shortcuts = self.shortcuts - if keycode[1] in shortcuts.keys(): - shortcut = shortcuts[keycode[1]] - if isinstance(shortcut, Widget): - shortcut.trigger_action(duration=0) - else: - self(*shortcut) - elif keycode[1] == "tab": + if keycode[1] == "tab": self.play_mode.switch_mode() elif keycode[1] == "shift": self.nav_drawer.set_state("toggle") @@ -361,6 +349,12 @@ class KaTrainGui(Screen, KaTrainBase): self.controls.set_status(i18n._("Copied SGF to clipboard.")) elif keycode[1] == "v" and "ctrl" in modifiers: self.load_sgf_from_clipboard() + elif keycode[1] in shortcuts.keys() and "ctrl" not in modifiers: + shortcut = shortcuts[keycode[1]] + if isinstance(shortcut, Widget): + shortcut.trigger_action(duration=0) + else: + self(*shortcut) return True diff --git a/katrain/config.json b/katrain/config.json index 1c3a950..d25a359 100644 --- a/katrain/config.json +++ b/katrain/config.json @@ -64,7 +64,7 @@ true ], "eval_off_show_last": 3, - "eval_show_ai": true, + "eval_show_ai": false, "lock_ai": true }, "ai": { diff --git a/katrain/core/ai.py b/katrain/core/ai.py index 94ee8a1..dc4f96b 100644 --- a/katrain/core/ai.py +++ b/katrain/core/ai.py @@ -5,7 +5,7 @@ import time from typing import Dict, List, Tuple from katrain.core.utils import var_to_grid -from katrain.core.constants import OUTPUT_INFO, OUTPUT_DEBUG, AI_STRATEGIES_POLICY, AI_POLICY, AI_WEIGHTED +from katrain.core.constants import OUTPUT_INFO, OUTPUT_DEBUG, AI_STRATEGIES_POLICY, AI_POLICY, AI_WEIGHTED, AI_STRATEGIES_PICK, AI_JIGO, AI_SCORELOSS, AI_DEFAULT from katrain.core.engine import EngineDiedException from katrain.core.game import Game, GameNode, Move @@ -44,7 +44,7 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode top_policy_move = policy_moves[0][1] ai_thoughts += f"Using policy based strategy, base top 5 moves are {fmt_moves(policy_moves[:5])}. " if ai_mode == AI_POLICY and cn.depth <= ai_settings["opening_moves"]: - ai_mode = "p:weighted" + ai_mode = AI_WEIGHTED ai_thoughts += f"Switching to weighted strategy in the opening {int(ai_settings['opening_moves'] * (game.board_size[0]*game.board_size[1]))} moves. " ai_settings = {"pick_override": 0.9, "weaken_fac": 1, "lower_bound": 0.02} if top_5_pass: @@ -77,21 +77,7 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode ai_thoughts += f"Playing policy-weighted random move {aimove.gtp()} ({policy_value:.1%})" + ( " because no other moves were found." if not top else f" because strategy is weighted (lower bound={lower_bound:.2%}, num moves > lb={len(weighted_coords)})." ) - elif "noise" in ai_mode: # DEPRECATED - noise_str = ai_settings["noise_strength"] - lower_bound = max(0, ai_settings["lower_bound"]) - selected_policy_moves = [(pol, mv) for pol, mv in policy_moves if not mv.is_pass if pol > lower_bound] - d_noise = dirichlet_noise(len(selected_policy_moves)) - noisy_policy_moves = [(((1 - noise_str) * pol + noise_str * noise), mv) for ((pol, mv), noise) in zip(selected_policy_moves, d_noise)] - new_top = heapq.nlargest(5, noisy_policy_moves) - ai_thoughts += f"Noisy policy strategy (strength={noise_str:.2f}) generated 5 moves {fmt_moves(new_top)} " - aimove = new_top[0][1] - if new_top[0][0] < pass_policy: - ai_thoughts += f", but found pass ({pass_policy:.2%} to be higher rated than {aimove.gtp()} ({new_top[0][0]:.2%}) so will play top policy move instead." - aimove = top_policy_move - else: - ai_thoughts += f" so picked {aimove.gtp()} ({policy_grid[aimove.coords[1]][aimove.coords[0]]:.2%})." - elif "p:" in ai_mode: + elif ai_mode in AI_STRATEGIES_PICK: legal_policy_moves = [(pol, mv) for pol, mv in policy_moves if not mv.is_pass if pol > 0] 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: @@ -151,30 +137,19 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode aimove = top_cand ai_thoughts += f"Top move is pass, so passing regardless of strategy." else: - if "balance" in ai_mode: # deprecated - 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: + if ai_mode == AI_JIGO: 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 {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} as closest to 0.5 point win" - elif "scoreloss" in ai_mode: + elif ai_mode == AI_SCORELOSS: c = ai_settings["strength"] moves = [(d["pointsLost"], math.exp(min(200, -c * max(0, d["pointsLost"]))), Move.from_gtp(d["move"], player=cn.next_player)) for d in candidate_ai_moves] topmove = weighted_selection_without_replacement(moves, 1)[0] aimove = topmove[2] ai_thoughts += f"ScoreLoss strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} (weight {topmove[1]:.3f}, point loss {topmove[0]:.1f}) based on score weights." else: - if "default" not in ai_mode and "katago" not in ai_mode: + if ai_mode != AI_DEFAULT: 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 = top_cand diff --git a/katrain/core/constants.py b/katrain/core/constants.py index c18eaf6..9e189de 100644 --- a/katrain/core/constants.py +++ b/katrain/core/constants.py @@ -22,7 +22,8 @@ AI_TERRITORY = "ai:p:territory" AI_CONFIG_DEFAULT = AI_SCORELOSS AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_SCORELOSS, AI_JIGO] -AI_STRATEGIES_POLICY = [AI_WEIGHTED, AI_POLICY, AI_PICK, AI_LOCAL, AI_TENUKI, AI_INFLUENCE, AI_TERRITORY] +AI_STRATEGIES_PICK = [AI_PICK, AI_LOCAL, AI_TENUKI, AI_INFLUENCE, AI_TERRITORY] +AI_STRATEGIES_POLICY = [AI_WEIGHTED, AI_POLICY] + AI_STRATEGIES_PICK AI_STRATEGIES = AI_STRATEGIES_ENGINE + AI_STRATEGIES_POLICY AI_STRATEGIES_RECOMMENDED_ORDER = [AI_DEFAULT, AI_SCORELOSS, AI_WEIGHTED, AI_PICK, AI_POLICY, AI_LOCAL, AI_TENUKI, AI_TERRITORY, AI_INFLUENCE, AI_JIGO] diff --git a/katrain/core/game.py b/katrain/core/game.py index 8bad811..ac37877 100644 --- a/katrain/core/game.py +++ b/katrain/core/game.py @@ -246,12 +246,15 @@ class Game: if eval_thresholds is None: eval_thresholds = self.katrain.config("trainer/eval_thresholds") - player_names = {bw: re.sub(r"['<>:\"/\\|?*]", "", self.root.get_property("P" + bw) or str(self.katrain.players_info[bw])) for bw in "BW"} + def player_name(player_info): + return f"{i18n._(player_info.player_type)} ({i18n._(player_info.player_subtype)})" + + player_names = {bw: re.sub(r"['<>:\"/\\|?*]", "", self.root.get_property("P" + bw) or player_name(self.katrain.players_info[bw])) for bw in "BW"} game_name = f"katrain_{player_names['B']} vs {player_names['W']} {self.game_id}" file_name = os.path.abspath(os.path.join(path, f"{game_name}.sgf")) os.makedirs(os.path.dirname(file_name), exist_ok=True) - show_dots_for = {bw: trainer_config.get("eval_show_ai", True) or pl.human for bw, pl in self.players.items()} + show_dots_for = {bw: trainer_config.get("eval_show_ai", True) or pl.human for bw, pl in self.katrain.players_info.items()} sgf = self.root.sgf(save_comments_player=show_dots_for, save_comments_class=save_feedback, eval_thresholds=eval_thresholds) with open(file_name, "w") as f: f.write(sgf) diff --git a/katrain/core/game_node.py b/katrain/core/game_node.py index 85d1a0c..2a57209 100644 --- a/katrain/core/game_node.py +++ b/katrain/core/game_node.py @@ -125,7 +125,7 @@ class GameNode(SGFNode): if not self.parent or not single_move: # root return "" - text = f"Move {self.depth}: {single_move.player} {single_move.gtp()}\n" + text = f"{i18n._('move')} {self.depth}: {single_move.player} {single_move.gtp()}\n" if self.analysis_ready: score = self.score if sgf: @@ -137,7 +137,7 @@ class GameNode(SGFNode): if previous_top_move["move"] != single_move.gtp(): points_lost = self.points_lost if sgf and points_lost > 0.5: - text += i18n._("Info:point loss").format(points=points_lost) + "\n" + text += i18n._("Info:point loss").format(points_lost=points_lost) + "\n" text += i18n._("Info:top move").format(top_move=previous_top_move["move"], score=self.format_score(previous_top_move["scoreLead"])) + "\n" else: text += i18n._("Info:best move") + "\n" diff --git a/katrain/gui.kv b/katrain/gui.kv index 47df17e..d9dace0 100644 --- a/katrain/gui.kv +++ b/katrain/gui.kv @@ -31,22 +31,18 @@ #:set WINRATE_COLOR GREEN #:set GRAPH_DOT_COLOR [0.85, 0.3, 0.3,1] - #:set BUTTON_INACTIVE_COLOR LIGHTGREY - - #:set NOTES_FONT_SIZE dp(18) - # for sizing help -