diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index cb594a9..e23f350 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,12 +1,12 @@ # These are supported funding model platforms github: sanderland # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username +patreon: katrainbaduk # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: sanderbaduk +liberapay: # sanderbaduk issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/CONTRIBUTIONS.md b/CONTRIBUTIONS.md index 73d500b..9b314ca 100644 --- a/CONTRIBUTIONS.md +++ b/CONTRIBUTIONS.md @@ -59,7 +59,7 @@ Many thanks to the following contributors for translations. * French: "Dontbtme" with contributions from "wonderingabout" * Korean: "isty2e" -* German: "nimets123" +* German: "nimets123" with contributions from "trohde" * Spanish: Sergio Villegas ("serpiente") with contributions from the Spanish OGS community * Russian: Dmitry Ivankov and Alexander Kiselev * Simplified Chinese: Qing Mu with contributions from "Medwin" diff --git a/INSTALL.md b/INSTALL.md index 8c96098..ef7bbaf 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -25,6 +25,8 @@ These commands install [Homebrew](https://brew.sh), which simplifies installing To upgrade to a newer version, simply run `pip3 install -U katrain` +If you encounter an error about SDL being missing, try ` brew sdl sdl_image sdl_mixer sdl_ttf portmidi` + ### Troubleshooting and Installation from sources Installation from sources is essentially the same as for Linux, see [here](#LinuxSources), diff --git a/README.md b/README.md index 508fb96..634cc85 100644 --- a/README.md +++ b/README.md @@ -154,23 +154,21 @@ Keyboard shortcuts are shown with **[key]**. wanting to guess the next move. * **[w]**: Show 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 '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]**: Policy moves: 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'. + * **[e]**: Top moves: Show the next moves KataGo considered, colored by their expected point loss. + Small/faint dots indicate high uncertainty and never show text (lower than your 'fast visits' setting). + Hover over any of them to see the principal variation. + * **[r]**: Policy moves: 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'. This turns off the 'top moves' setting as the overlap is often not useful. * **[t]**: Expected territory: Show expected ownership of each intersection. * The analysis options available under the 'Analysis' button are used for deeper evaluation of the position: * **[a]**: Deeper analysis: Re-evaluate the position using more visits, usually resulting in a more accurate evaluation. * **[s]**: Equalize visits: 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]**: Analyze all moves: Evaluate all possible next moves. This can take a bit of time even though 'fast_visits' is used, but can be useful to see how many reasonable next moves are available. + * **[f]**: Find alternatives: Increases analysis of current candidate moves to at least the 'fast visits' level, and request a new query that excludes all current candidate moves. * **[spacebar]**: Turn continuous analysis on/off. This will continuously improve analysis of the current position, similar to Lizzie's 'pondering', but only when there are no other queries going on. * **[enter]** AI move. Makes the AI move for the current player regardless of current player selection. -### Rank Estimation - -A new feature in v1.3 is the rank estimation panel. This adds an extra graph which uses a statistical model to estimate the playing strength -of both players for every 80 move segment. It can be used for determining which part of your game needs extra attention, -but keep in mind the estimation is based purely on how well moves correspond to the 'shape sense' of KataGo and can be very inaccurate at times. - ## Keyboard and mouse shortcuts In addition to shortcuts mentioned above and those shown in the main menu: @@ -179,8 +177,8 @@ In addition to shortcuts mentioned above and those shown in the main menu: * **[~]** or **[ ` ]** or **[m]**: Cycles through more minimalistic UI modes. * **[p]**: Pass * **[b]**: Pause/Resume timer -* **[arrow left]** or **[z]**: Undo move. Hold shift for 10 moves at a time, or ctrl to skip to the start. -* **[arrow right]** or **[x]**: Redo move. Hold shift for 10 moves at a time, or ctrl to skip to the start. +* **[arrow left]** or **[z]**: Undo move. Hold alt for 10 moves at a time, or ctrl to skip to the start. +* **[arrow right]** or **[x]**: Redo move. Hold alt for 10 moves at a time, or ctrl to skip to the start. * **[arrow up/down]** Switch branch, as would be expected from the move tree. * **[scroll up]**: Undo move. Only works when hovering the cursor over the board. * **[scroll down]**: Redo move. Only works when hovering the cursor over the board. @@ -197,15 +195,11 @@ In addition to shortcuts mentioned above and those shown in the main menu: * Try using a lower number for `nnMaxBatchSize` in `KataGo/analysis_config.cfg`, and avoid using versions compiled with large board sizes. * If still encountering problems, please start KataGo by itself to check for any errors it gives. * Note that if you don't have a GPU, or your GPU does not support OpenCL, you should use the 'eigen' binaries which run on CPU only. -* How can I play on larger boards? - * For windows, change the `katago` setting to `katrain\KataGo\katago-bs52.exe`. For other operating systems, you need to compile your own KataGo version with higher limits. - ## Support / Contribute [![GitHub issues](https://img.shields.io/github/issues/sanderland/katrain)](https://github.com/sanderland/katrain/issues) [![Contributors](https://img.shields.io/static/v1?label=contributors&message=19&color=dcb424)](CONTRIBUTIONS.md) -[![Liberapay patrons](https://img.shields.io/liberapay/patrons/sanderbaduk)](https://liberapay.com/sanderbaduk/) [![Github sponsors](https://img.shields.io/static/v1?label=sponsor&message=7&logo=GitHub&color=dcb424&link=https://github.com/sponsors/sanderland/)](https://github.com/sponsors/sanderland) * Ideas, feedback, and contributions to code or translations are all very welcome. @@ -213,7 +207,7 @@ In addition to shortcuts mentioned above and those shown in the main menu: * I am looking for contributors of more translations of both this manual and the program itself. The best way to help with this is to contact me on discord. * You can 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 get help, discuss improvements, or simply show your appreciation. -* You can also donate to the project through [Liberapay](https://liberapay.com/KaTrain/) or [Github Sponsors](https://github.com/sponsors/sanderland). +* You can also donate to the project through [Github Sponsors](https://github.com/sponsors/sanderland). diff --git a/katrain/__main__.py b/katrain/__main__.py index 5dd6f62..502859e 100644 --- a/katrain/__main__.py +++ b/katrain/__main__.py @@ -196,14 +196,14 @@ class KaTrainGui(Screen, KaTrainBase): and cn.analysis_ready and cn.parent.analysis_ready and not cn.children - and not self.game.ended + and not self.game.end_result ): self.game.analyze_undo(cn) # not via message loop if ( cn.analysis_ready and next_player.ai and not cn.children - and not self.game.ended + and not self.game.end_result and not (teaching_undo and cn.auto_undo is None) ): # cn mismatch stops this if undo fired. avoid message loop here or fires repeatedly. self._do_ai_move(cn) @@ -266,7 +266,7 @@ class KaTrainGui(Screen, KaTrainBase): player_info.name = move_tree.root.get_property("P" + bw) self.update_player(bw) self.controls.graph.initialize_from_game(self.game.root) - self.controls.rank_graph.initialize_from_game(self.game.root) + # self.controls.rank_graph.initialize_from_game(self.game.root) self.update_state(redraw_board=True) def _do_ai_move(self, node=None): @@ -286,6 +286,9 @@ class KaTrainGui(Screen, KaTrainBase): self.board_gui.animating_pv = None self.game.undo(n_times) + def _do_resign(self): + self.game.current_node.end_state = f"{self.game.current_node.player}+R" + def _do_redo(self, n_times=1): self.board_gui.animating_pv = None self.game.redo(n_times) @@ -436,6 +439,7 @@ class KaTrainGui(Screen, KaTrainBase): "a": ("analyze-extra", "extra"), "s": ("analyze-extra", "equalize"), "d": ("analyze-extra", "sweep"), + "f": ("analyze-extra", "alternative"), "p": ("play", None), "down": ("switch-branch", 1), "up": ("switch-branch", -1), @@ -475,9 +479,9 @@ class KaTrainGui(Screen, KaTrainBase): elif keycode[1] in ["`", "~", "m"] and "ctrl" not in modifiers: self.zen = (self.zen + 1) % 3 elif keycode[1] in ["left", "z"]: - self("undo", 1 + ("shift" in modifiers) * 9 + ("ctrl" in modifiers) * 999) + self("undo", 1 + ("alt" in modifiers) * 9 + ("ctrl" in modifiers) * 999) elif keycode[1] in ["right", "x"]: - self("redo", 1 + ("shift" in modifiers) * 9 + ("ctrl" in modifiers) * 999) + self("redo", 1 + ("alt" in modifiers) * 9 + ("ctrl" in modifiers) * 999) elif keycode[1] == "n" and "ctrl" in modifiers: self("new-game-popup") elif keycode[1] == "l" and "ctrl" in modifiers: diff --git a/katrain/config.json b/katrain/config.json index 6c51d8a..a8e5203 100644 --- a/katrain/config.json +++ b/katrain/config.json @@ -17,7 +17,7 @@ "anim_pv_time": 0.5, "debug_level": 0, "lang": "en", - "version": "1.4.0" + "version": "1.5.0" }, "timer": { "byo_length": 30, @@ -65,8 +65,9 @@ true, true ], + "low_visits": 25, "eval_off_show_last": 3, - "text_point_loss": false, + "text_point_loss": true, "eval_show_ai": true, "lock_ai": false }, diff --git a/katrain/core/base_katrain.py b/katrain/core/base_katrain.py index 627a3de..e8604a8 100644 --- a/katrain/core/base_katrain.py +++ b/katrain/core/base_katrain.py @@ -61,8 +61,8 @@ class KaTrainBase: if self.debug_level >= OUTPUT_DEBUG: Config.set("kivy", "log_enable", 1) Config.set("kivy", "log_level", "debug") - if self.debug_level >= OUTPUT_EXTRA_DEBUG: - Config.set("kivy", "log_level", "trace") + # if self.debug_level >= OUTPUT_EXTRA_DEBUG: + # Config.set("kivy", "log_level", "trace") self.players_info = {"B": Player("B"), "W": Player("W")} self.reset_players() diff --git a/katrain/core/constants.py b/katrain/core/constants.py index d37d410..2fc1fb6 100644 --- a/katrain/core/constants.py +++ b/katrain/core/constants.py @@ -1,4 +1,4 @@ -VERSION = "1.4.0" +VERSION = "1.5.0" HOMEPAGE = "https://github.com/sanderland/katrain" CONFIG_MIN_VERSION = "1.4.0" # keep config files from this version diff --git a/katrain/core/engine.py b/katrain/core/engine.py index 89714c8..63cffe5 100644 --- a/katrain/core/engine.py +++ b/katrain/core/engine.py @@ -1,6 +1,7 @@ import copy import json import os +import shlex import subprocess import threading import time @@ -41,10 +42,12 @@ class KataGoEngine: self._lock = threading.Lock() self.analysis_thread = None self.stderr_thread = None + self.shell = False exe = config.get("katago", "").strip() if config.get("altcommand", ""): self.command = config["altcommand"] + self.shell = True else: if not exe: if platform == "win": @@ -74,14 +77,16 @@ class KataGoEngine: elif not os.path.isfile(cfg): self.katrain.log(i18n._("Kata config not found").format(config=cfg), OUTPUT_ERROR) return # don't start - self.command = f'"{exe}" analysis -model "{model}" -config "{cfg}" -analysis-threads {config["threads"]}' + self.command = shlex.split( + f'"{exe}" analysis -model "{model}" -config "{cfg}" -analysis-threads {config["threads"]}' + ) self.start() def start(self): try: self.katrain.log(f"Starting KataGo with {self.command}", OUTPUT_DEBUG) self.katago_process = subprocess.Popen( - self.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True + self.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=self.shell ) except (FileNotFoundError, PermissionError, OSError) as e: self.katrain.log( @@ -221,13 +226,15 @@ class KataGoEngine: visits: int = None, analyze_fast: bool = False, time_limit=True, + find_alternatives: bool = False, priority: int = 0, ownership: Optional[bool] = None, next_move: Optional[GameNode] = None, extra_settings: Optional[Dict] = None, ): - moves = [m for node in analysis_node.nodes_from_root for m in node.moves] - initial_stones = analysis_node.root.placements + nodes = analysis_node.nodes_from_root + moves = [m for node in nodes for m in node.moves] + initial_stones = [m for node in nodes for m in node.placements] if next_move: moves.append(next_move) if ownership is None: @@ -237,6 +244,17 @@ class KataGoEngine: if analyze_fast and self.config.get("fast_visits"): visits = self.config["fast_visits"] + if find_alternatives: + avoid = [ + { + "moves": list(analysis_node.analysis["moves"].keys()), + "player": analysis_node.next_player, + "untilDepth": 1, + } + ] + else: + avoid = [] + size_x, size_y = analysis_node.board_size settings = copy.copy(self.override_settings) if time_limit: @@ -250,11 +268,13 @@ class KataGoEngine: "analyzeTurns": [len(moves)], "maxVisits": visits, "komi": analysis_node.komi, + "avoidMoves": avoid, "boardXSize": size_x, "boardYSize": size_y, "includeOwnership": ownership and not next_move, "includePolicy": not next_move, "initialStones": [[m.player, m.gtp()] for m in initial_stones], + "initialPlayer": analysis_node.root.next_player, "moves": [[m.player, m.gtp()] for m in moves], "overrideSettings": {**settings, **(extra_settings or {})}, } diff --git a/katrain/core/game.py b/katrain/core/game.py index 21ed7fd..ee4077d 100644 --- a/katrain/core/game.py +++ b/katrain/core/game.py @@ -55,7 +55,11 @@ class Game: self.root = move_tree self.komi = self.root.komi handicap = int(self.root.get_property("HA", 0)) - if handicap >= 2 and not self.root.placements: + if ( + handicap >= 2 + and not self.root.placements + and not (not self.root.move_with_placements and self.root.children and self.root.children[0].placements) + ): # not really according to sgf, and not sure if still needed, last clause for fox self.root.place_handicap_stones(handicap) else: board_size = katrain.config("game/size") @@ -203,8 +207,11 @@ class Game: return sum(self.chains, []) @property - def ended(self): - return self.current_node.parent and self.current_node.is_pass and self.current_node.parent.is_pass + def end_result(self): + if self.current_node.end_state: + return self.current_node.end_state + if self.current_node.parent and self.current_node.is_pass and self.current_node.parent.is_pass: + return self.manual_score or i18n._("board-game-end") @property def prisoner_count( @@ -279,13 +286,17 @@ class Game: def player_name(player_info): return f"{i18n._(player_info.player_type)} ({i18n._(player_info.player_subtype)})" + root_properties = self.root.properties + x_properties = {} if "KaTrain" in self.root.get_property("AP", ""): for bw in "BW": - self.root.set_property("P" + bw, player_name(self.katrain.players_info[bw])) + x_properties["P" + bw] = player_name(self.katrain.players_info[bw]) player_info = self.katrain.players_info[bw] if player_info.player_type == PLAYER_AI: - self.root.set_property(bw + "R", rank_label(player_info.calculated_rank)) - + x_properties[bw + "R"] = rank_label(player_info.calculated_rank) + if "+" in str(self.end_result): + x_properties["RE"] = self.end_result + self.root.properties = {**root_properties, **{k: [v] for k, v in x_properties.items()}} player_names = {bw: re.sub(r"['<>:\"/\\|?*]", "", self.root.get_property("P" + bw, 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")) @@ -299,6 +310,7 @@ class Game: ) with open(file_name, "w", encoding="utf-8") as f: f.write(sgf) + self.root.properties = root_properties return i18n._("sgf written").format(file_name=file_name) def analyze_extra(self, mode, **kwargs): @@ -310,7 +322,9 @@ class Game: if mode == "extra": if kwargs.get("continuous", False): - visits = max(engine.config["max_visits"], math.ceil(cn.analysis_visits_requested * 1.25)) + visits = min( + 1_000_000_000, max(engine.config["max_visits"], math.ceil(cn.analysis_visits_requested * 1.25)) + ) else: visits = cn.analysis_visits_requested + engine.config["max_visits"] self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS) @@ -355,21 +369,27 @@ class Game: visits = engine.config["fast_visits"] self.katrain.controls.set_status(i18n._("sweep analysis").format(visits=visits), STATUS_ANALYSIS) priority = -1_000_000_000 - elif mode == "equalize": + elif mode in ["equalize", "alternative"]: if not cn.analysis_ready: self.katrain.controls.set_status(i18n._("wait-before-equalize"), STATUS_INFO, self.current_node) return analyze_moves = [Move.from_gtp(gtp, player=cn.next_player) for gtp, _ in cn.analysis["moves"].items()] - visits = max(d["visits"] for d in cn.analysis["moves"].values()) - self.katrain.controls.set_status(i18n._("equalizing analysis").format(visits=visits), STATUS_ANALYSIS) priority = -1_000 + if mode == "alternative": # also do a quick update on current candidates so it doesn't look too weird + self.katrain.controls.set_status(i18n._("alternative analysis"), STATUS_ANALYSIS) + cn.analyze(engine, priority=-500, time_limit=False, find_alternatives=True) + visits = engine.config["fast_visits"] + else: + visits = max(d["visits"] for d in cn.analysis["moves"].values()) + self.katrain.controls.set_status(i18n._("equalizing analysis").format(visits=visits), STATUS_ANALYSIS) else: raise ValueError("Invalid analysis mode") for move in analyze_moves: - cn.analyze( - engine, priority, visits=visits, refine_move=move, time_limit=False - ) # explicitly requested so take as long as you need + if cn.analysis["moves"].get(move.gtp(), {"visits": 0})["visits"] < visits: + cn.analyze( + engine, priority, visits=visits, refine_move=move, time_limit=False + ) # explicitly requested so take as long as you need def analyze_undo(self, node): train_config = self.katrain.config("trainer") diff --git a/katrain/core/game_node.py b/katrain/core/game_node.py index 6321f72..0a73a3b 100644 --- a/katrain/core/game_node.py +++ b/katrain/core/game_node.py @@ -23,6 +23,7 @@ class GameNode(SGFNode): self.time_used = 0 self.analysis_visits_requested = 0 self.undo_threshold = random.random() # for fractional undos + self.end_state = None def sgf_properties(self, save_comments_player=None, save_comments_class=None, eval_thresholds=None): properties = copy.copy(super().sgf_properties()) @@ -67,15 +68,25 @@ class GameNode(SGFNode): ) # analyzed/not undone main, non-teach second, undone last # various analysis functions - def analyze(self, engine, priority=0, visits=None, time_limit=True, refine_move=None, analyze_fast=False): + def analyze( + self, + engine, + priority=0, + visits=None, + time_limit=True, + refine_move=None, + analyze_fast=False, + find_alternatives=False, + ): engine.request_analysis( self, - lambda result: self.set_analysis(result, refine_move), + lambda result: self.set_analysis(result, refine_move, find_alternatives), priority=priority, visits=visits, analyze_fast=analyze_fast, time_limit=time_limit, next_move=refine_move, + find_alternatives=find_alternatives, ) def update_move_analysis(self, move_analysis, move_gtp): @@ -86,21 +97,30 @@ class GameNode(SGFNode): "order": 999, **move_analysis, } # some default values for keys missing in rootInfo - elif cur["visits"] < move_analysis["visits"]: - cur.update(move_analysis) + else: + cur["order"] = min(cur["order"], move_analysis.get("order", 999)) # parent arriving after child + if cur["visits"] < move_analysis["visits"]: + cur.update(move_analysis) - def set_analysis(self, analysis_json, refine_move): + def set_analysis(self, analysis_json: Dict, refine_move: Optional[Move], alternatives_mode: bool): if refine_move: 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: + if alternatives_mode: + for m in analysis_json["moveInfos"]: + m["order"] += 10 # offset for not making this top + if refine_move is None and not alternatives_mode: + for move_dict in self.analysis["moves"].values(): + move_dict["order"] = 999 # old moves to end for move_analysis in analysis_json["moveInfos"]: self.update_move_analysis(move_analysis, move_analysis["move"]) self.ownership = analysis_json.get("ownership") self.policy = analysis_json.get("policy") - self.analysis["root"] = analysis_json["rootInfo"] + if not alternatives_mode: + self.analysis["root"] = analysis_json["rootInfo"] if self.parent and self.move: analysis_json["rootInfo"]["pv"] = [self.move.gtp()] + ( analysis_json["moveInfos"][0]["pv"] if analysis_json["moveInfos"] else [] @@ -151,6 +171,8 @@ class GameNode(SGFNode): def comment(self, sgf=False, teach=False, details=False, interactive=True): single_move = self.move if not self.parent or not single_move: # root + if self.root: + return f"{i18n._('komi')}: {self.komi:.1f}\n{i18n._('ruleset')}: {i18n._(self.get_property('RU','Japanese').lower())}\n" return "" text = i18n._("move").format(number=self.depth) + f": {single_move.player} {single_move.gtp()}\n" @@ -193,8 +215,10 @@ class GameNode(SGFNode): text += "\n" + i18n._("Info:AI thoughts").format(thoughts=self.ai_thoughts) else: text = i18n._("No analysis available") if sgf else i18n._("Analyzing move...") + if "C" in self.properties: text += "\n[u]SGF Comments:[/u]\n" + "\n".join(self.properties["C"]) + return text @property diff --git a/katrain/core/sgf_parser.py b/katrain/core/sgf_parser.py index fd3ca01..7f284a4 100644 --- a/katrain/core/sgf_parser.py +++ b/katrain/core/sgf_parser.py @@ -275,7 +275,7 @@ class SGFNode: @property def next_player(self): """Returns player to move""" - if "B" in self.properties or "AB" in self.properties: # root or black moved + if "B" in self.properties or ("AB" in self.properties and not "W" in self.properties): # root or black moved return "W" else: return "B" @@ -283,7 +283,7 @@ class SGFNode: @property def player(self): """Returns player that moved last. nb root is considered white played if no handicap stones are placed""" - if "B" in self.properties or "AB" in self.properties: + if "B" in self.properties or ("AB" in self.properties and not "W" in self.properties): return "B" else: return "W" @@ -332,7 +332,17 @@ class SGF: """Parse a string as SGF.""" match = re.search(cls.SGF_PAT, input_str) clipped_str = match.group() if match else input_str - return cls(clipped_str).root + root = cls(clipped_str).root + # Fix weird FoxGo server KM values + if "foxwq" in root.get_list_property("AP", []): + if int(root.get_property("HA", 0)) >= 1: + corrected_komi = 0.5 + elif root.get_property("RU").lower() in ["chinese", "cn"]: + corrected_komi = 7.5 + else: + corrected_komi = 6.5 + root.set_property("KM", corrected_komi) + return root @classmethod def parse_file(cls, filename, encoding=None) -> SGFNode: @@ -343,7 +353,7 @@ class SGF: with open(filename, "rb") as f: bin_contents = f.read() if not encoding: - if is_gib or is_ngf: + if is_gib or is_ngf or b"AP[foxwq]" in bin_contents: encoding = "utf8" else: # sgf match = re.search(rb"CA\[(.*?)\]", bin_contents) diff --git a/katrain/gui.kv b/katrain/gui.kv index 407e970..07219f9 100644 --- a/katrain/gui.kv +++ b/katrain/gui.kv @@ -284,9 +284,9 @@ outline_width: 2 padding: [2*CP_PADDING,CP_PADDING,CP_PADDING,CP_PADDING] CircleWithText: + size_hint: 0.32, 1 player: root.player text: str(root.captures) - size_hint: 0.32, 1 BoxLayout: size_hint: 0.75, 1 pos_hint: {'x':0,'y':0} @@ -511,7 +511,6 @@ engine_status_pondering: -1 timer:timer graph: graph - rank_graph: rank_graph stats: stats info: info note: note @@ -551,41 +550,34 @@ padding: 2*CP_PADDING,CP_PADDING spacing: CP_SPACING size_hint_x: 0.35 - SizedRoundedRectangleButton: - text: i18n._('pass-button-text') - size_hint: 1, 0.5 - on_left_release: root.katrain("play",None) SizedRoundedRectangleButton: text: i18n._('undo-button-text') size_hint: 1, 0.5 on_left_release: root.katrain("undo","smart") + SizedRoundedRectangleButton: + text: i18n._('resign') + size_hint: 1, 0.5 + on_left_release: root.katrain("resign") MoveTree: id: move_tree size_hint_x: EPSILON if self.parent.mode == MODE_PLAY else 1 opacity: 0 if self.parent.mode == MODE_PLAY else 1 CollapsablePanel: id: graph_panel - content_height: graph.height + rank_graph.height + content_height: graph.height # + rank_graph.height options_height: root.tab_option_height closed_label: 'closedlabel:scoregraph' - options: ['score','winrate','rank_est'] - option_colors: [SCORE_COLOR,WINRATE_COLOR,POINTLOSS_COLOR] + options: ['score','winrate'] + option_colors: [SCORE_COLOR,WINRATE_COLOR] option_active: [True,False,False] on_option_state: graph.show_graphs(args[1]) graph.hidden = not (args[1]['score'] or args[1]['winrate']) - rank_graph.hidden = not args[1]['rank_est'] - rank_graph.trigger_calculate() ScoreGraph: id: graph opacity: 0 if self.hidden else 1 size_hint_y: None height: EPSILON if self.hidden else root.graph_height - RankGraph: - id: rank_graph - opacity: 0 if self.hidden else 1 - size_hint_y: None - height: EPSILON if self.hidden else root.graph_height CollapsablePanel: id: stats_panel num_active: 3 @@ -705,12 +697,13 @@ halign: 'center' font_size: root.height / 3 font_name: root.font_name - color: checkbox.selected_color if checkbox.active else checkbox.unselected_color + color: checkbox.disabled_color if root.disabled else (checkbox.selected_color if checkbox.active else checkbox.unselected_color) size_hint_x: None width: self.texture_size[0] + 4 on_left_press: checkbox.active = not checkbox.active MDCheckbox: id: checkbox + disabled: root.disabled size_hint: None,0.5 pos_hint: {'center_y': 0.5} width: self.height @@ -751,6 +744,11 @@ icon: 'img/Sweep.png' shortcut: 'd' on_action: root.katrain("analyze-extra", "sweep") + AnalysisMenuItem: + text: i18n._("analysis:alternatives") + icon: 'img/Alternative.png' + shortcut: 'f' + on_action: root.katrain("analyze-extra", "alternative") AnalysisMenuItem: text: i18n._("analysis:game") icon: 'img/analysis.png' @@ -797,6 +795,7 @@ AnalysisToggle: id: hints text: i18n._("analysis:topmoves") + disabled: policy.checkbox.active AnalysisToggle: id: policy text: i18n._("analysis:policy") diff --git a/katrain/gui/badukpan.py b/katrain/gui/badukpan.py index 01ddf54..611e706 100644 --- a/katrain/gui/badukpan.py +++ b/katrain/gui/badukpan.py @@ -17,7 +17,7 @@ from kivymd.app import MDApp from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.floatlayout import MDFloatLayout -from katrain.core.constants import MODE_PLAY, OUTPUT_DEBUG, STATUS_TEACHING +from katrain.core.constants import MODE_PLAY, OUTPUT_DEBUG, STATUS_TEACHING, OUTPUT_EXTRA_DEBUG from katrain.core.game import Move from katrain.core.lang import i18n from katrain.core.utils import evaluation_class, var_to_grid @@ -120,8 +120,9 @@ class BadukPanWidget(Widget): katrain.game.set_current_node(nodes_here[-1].parent) katrain.update_state() else: # load comments & pv - katrain.log(f"\nAnalysis:\n{nodes_here[-1].analysis}", OUTPUT_DEBUG) - katrain.log(f"\nParent Analysis:\n{nodes_here[-1].parent.analysis}", OUTPUT_DEBUG) + katrain.log(f"\nAnalysis:\n{nodes_here[-1].analysis}", OUTPUT_EXTRA_DEBUG) + katrain.log(f"\nParent Analysis:\n{nodes_here[-1].parent.analysis}", OUTPUT_EXTRA_DEBUG) + katrain.log(f"\nRoot Stats:\n{nodes_here[-1].analysis['root']}", OUTPUT_DEBUG) katrain.controls.info.text = nodes_here[-1].comment(sgf=True) katrain.controls.active_comment_node = nodes_here[-1] if nodes_here[-1].parent.analysis_ready: @@ -254,7 +255,7 @@ class BadukPanWidget(Widget): self.canvas.clear() # stones current_node = katrain.game.current_node - game_ended = katrain.game.ended + game_ended = katrain.game.end_result full_eval_on = katrain.analysis_controls.eval.active has_stone = {} drawn_stone = {} @@ -268,7 +269,6 @@ class BadukPanWidget(Widget): nodes = katrain.game.current_node.nodes_from_root realized_points_lost = None - katrain.config("trainer/show_dots") for i, node in enumerate(nodes[::-1]): # reverse order! points_lost = node.points_lost evalscale = 1 @@ -362,7 +362,7 @@ class BadukPanWidget(Widget): polsize = 1.1 * math.sqrt(policy_grid[y][x]) policy_circle_color = ( *POLICY_COLOR, - GHOST_ALPHA + TOP_MOVE_ALPHA * (policy_grid[y][x] == best_move_policy), + POLICY_ALPHA + TOP_POLICY_ALPHA * (policy_grid[y][x] == best_move_policy), ) draw_circle( (self.gridpos_x[x], self.gridpos_y[y]), polsize * self.stone_size, policy_circle_color @@ -377,15 +377,15 @@ class BadukPanWidget(Widget): # pass circle passed = len(nodes) > 1 and current_node.is_pass - if passed: + if passed or game_ended: if game_ended: - text = katrain.game.manual_score or i18n._("board-game-end") + text = game_ended katrain.controls.timer.paused = True else: text = i18n._("board-pass") Color(0.45, 0.05, 0.45, 0.7) center = (self.gridpos_x[int(board_size_x / 2)], self.gridpos_y[int(board_size_y / 2)]) - size = min(self.width, self.height) * 0.22 + size = min(self.width, self.height) * 0.227 Ellipse(pos=(center[0] - size / 2, center[1] - size / 2), size=(size, size)) Color(0.85, 0.85, 0.85) draw_text( @@ -395,9 +395,9 @@ class BadukPanWidget(Widget): self.draw_hover_contents() def draw_hover_contents(self, *_args): - ghost_alpha = GHOST_ALPHA + ghost_alpha = POLICY_ALPHA katrain = self.katrain - game_ended = katrain.game.ended + game_ended = katrain.game.end_result current_node = katrain.game.current_node player, next_player = current_node.player, current_node.next_player @@ -409,52 +409,51 @@ class BadukPanWidget(Widget): self.canvas.after.clear() self.active_pv_moves = [] - # children of current moves in undo / review - alpha = GHOST_ALPHA - if katrain.analysis_controls.show_children.active: - for child_node in current_node.children: - points_lost = child_node.points_lost - move = child_node.move - if move and move.coords is not None: - if points_lost is None: - evalcol = None - else: - evalcol = copy.copy(self.eval_color(points_lost)) - evalcol[3] = alpha - if child_node.analysis_ready: - self.active_pv_moves.append( - (move.coords, [move.gtp()] + child_node.candidate_moves[0]["pv"], current_node) - ) - scale = CHILD_SCALE - self.draw_stone( - move.coords[0], - move.coords[1], - move.player, - alpha=alpha, - evalcol=evalcol, - evalscale=scale, - scale=scale, - ) - # hints or PV - if katrain.analysis_controls.hints.active and not game_ended: + hint_moves = [] + if ( + katrain.analysis_controls.hints.active + and not katrain.analysis_controls.policy.active + and not game_ended + ): hint_moves = current_node.candidate_moves - for i, move_dict in enumerate(hint_moves): + elif katrain.controls.status_state[1] == STATUS_TEACHING: # show score hint for teaching undo + hint_moves = [ + m + for m in current_node.candidate_moves + for c in current_node.children + if c.move and c.auto_undo and c.move.gtp() == m["move"] + ] + + top_move_coords = None + if hint_moves: + low_visits_threshold = katrain.config("trainer/low_visits", 25) + for move_dict in hint_moves: move = Move.from_gtp(move_dict["move"]) if move.coords is not None: - alpha, scale = GHOST_ALPHA, 1.0 - if move_dict["visits"] < VISITS_FRAC_SMALL * hint_moves[0]["visits"]: - scale = 0.8 + engine_best_move = move_dict.get("order", 99) == 0 + scale = HINT_SCALE + text_on = True + alpha = HINTS_ALPHA + if move_dict["visits"] < low_visits_threshold and not engine_best_move: + scale = UNCERTAIN_HINT_SCALE + text_on = False + alpha = HINTS_MIN_ALPHA + (HINTS_ALPHA - HINTS_MIN_ALPHA) * ( + move_dict["visits"] / low_visits_threshold + ) if "pv" in move_dict: self.active_pv_moves.append((move.coords, move_dict["pv"], current_node)) else: katrain.log(f"PV missing for move_dict {move_dict}", OUTPUT_DEBUG) - draw_circle( - (self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]]), - col=[*self.eval_color(move_dict["pointsLost"])[:3], alpha], - r=self.stone_size * scale, + evalsize = self.stone_size * scale + evalcol = self.eval_color(move_dict["pointsLost"]) + Color(*evalcol[:3], alpha) + Rectangle( + pos=(self.gridpos_x[move.coords[0]] - evalsize, self.gridpos_y[move.coords[1]] - evalsize), + size=(2 * evalsize, 2 * evalsize), + source="img/topmove.png", ) - if self.trainer_config["text_point_loss"]: + if self.trainer_config["text_point_loss"] and text_on: if move_dict["pointsLost"] < 0.05: ptloss_text = "0.0" else: @@ -468,13 +467,50 @@ class BadukPanWidget(Widget): font_name="Roboto", ) - if i == 0: + if engine_best_move: + top_move_coords = move.coords Color(*TOP_MOVE_BORDER_COLOR) Line( circle=( self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]], - self.stone_size * scale - 1.2, + self.stone_size - dp(1.2), + ), + width=dp(1.2), + ) + + # children of current moves in undo / review + if katrain.analysis_controls.show_children.active: + for child_node in current_node.children: + move = child_node.move + if move and move.coords is not None: + if child_node.analysis_ready: + self.active_pv_moves.append( + (move.coords, [move.gtp()] + child_node.candidate_moves[0]["pv"], current_node) + ) + + if move.coords != top_move_coords: # for contrast + dashed_width = 18 + Color(*STONE_CONTRAST_COLORS[child_node.player]) + Line( + circle=( + self.gridpos_x[move.coords[0]], + self.gridpos_y[move.coords[1]], + self.stone_size - dp(1.2), + ), + width=dp(1.2), + ) + else: + dashed_width = 10 + Color(*STONE_COLORS[child_node.player]) + for s in range(0, 360, 30): + Line( + circle=( + self.gridpos_x[move.coords[0]], + self.gridpos_y[move.coords[1]], + self.stone_size - dp(1.2), + s, + s + dashed_width, ), width=dp(1.2), ) @@ -524,9 +560,9 @@ class BadukPanWidget(Widget): stone_size = self.stone_size * sizefac Color(1, 1, 1, 1) - Rectangle( - pos=(board_coords[0] - stone_size, board_coords[1] - stone_size), - size=(2 * stone_size, 2 * stone_size), + Rectangle( # not sure why the -1 here, but seems to center better + pos=(board_coords[0] - stone_size - 1, board_coords[1] - stone_size), + size=(2 * stone_size + 1, 2 * stone_size + 1), source=f"img/{move_player}_stone.png", ) Color(*STONE_TEXT_COLORS[move_player]) diff --git a/katrain/gui/controlspanel.py b/katrain/gui/controlspanel.py index 37f1cf5..245c0bd 100644 --- a/katrain/gui/controlspanel.py +++ b/katrain/gui/controlspanel.py @@ -127,7 +127,7 @@ class ControlsPanel(BoxLayout): lock_ai = katrain.config("trainer/lock_ai") and katrain.play_analyze_mode == MODE_PLAY details = self.info.detailed and not lock_ai info = "" - if current_node.move and not current_node.is_root: + if current_node.move or current_node.is_root: info = self.active_comment_node.comment( teach=katrain.players_info[self.active_comment_node.player].being_taught, details=details ) @@ -144,7 +144,7 @@ class ControlsPanel(BoxLayout): self.stats.player = "" self.graph.update_value(current_node) - self.rank_graph.update_value(current_node) + # self.rank_graph.update_value(current_node) self.note.text = current_node.note self.info.text = info diff --git a/katrain/gui/kivyutils.py b/katrain/gui/kivyutils.py index 5da0548..a2b953b 100644 --- a/katrain/gui/kivyutils.py +++ b/katrain/gui/kivyutils.py @@ -205,7 +205,7 @@ class IMETextField(MDTextField): self._imo_cursor = self.cursor -class I18NSpinner(Spinner): +class KeyValueSpinner(Spinner): __events__ = ["on_select"] sync_height_frac = NumericProperty(1.0) value_refs = ListProperty() @@ -214,9 +214,8 @@ class I18NSpinner(Spinner): def __init__(self, **kwargs): super().__init__(**kwargs) - self.bind(size=self.update_dropdown_props, pos=self.update_dropdown_props, value_refs=self.i18n_values) - self.i18n_values() - MDApp.get_running_app().bind(language=self.i18n_values) + self.build_values() + self.bind(size=self.update_dropdown_props, pos=self.update_dropdown_props, value_refs=self.build_values) @property def selected(self): @@ -245,9 +244,8 @@ class I18NSpinner(Spinner): except (ValueError, IndexError): pass - def i18n_values(self, *_args): - if self.value_refs: - self.values = [i18n._(ref) for ref in self.value_refs] + def build_values(self, *_args): + if self.value_refs and self.values: self.text = self.values[self.selected_index] self.font_name = i18n.font_name self.update_dropdown_props() @@ -269,6 +267,22 @@ class I18NSpinner(Spinner): item.font_name = self.font_name +class I18NSpinner(KeyValueSpinner): + __events__ = ["on_select"] + sync_height_frac = NumericProperty(1.0) + value_refs = ListProperty() + selected_index = NumericProperty(0) + font_name = StringProperty(DEFAULT_FONT) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + MDApp.get_running_app().bind(language=self.build_values) + + def build_values(self, *_args): + self.values = [i18n._(ref) for ref in self.value_refs] + super().build_values() + + class PlayerSetup(MDBoxLayout): player = OptionProperty("B", options=["B", "W"]) mode = StringProperty("") @@ -338,6 +352,7 @@ class PlayerInfo(MDBoxLayout, BackgroundMixin): captures = NumericProperty(0) player = OptionProperty("B", options=["B", "W"]) player_type = StringProperty("Player") + komi = NumericProperty(0) player_subtype = StringProperty("") name = StringProperty("", allownone=True) rank = StringProperty("", allownone=True) @@ -357,6 +372,7 @@ class AnalysisToggle(MDBoxLayout): text = StringProperty("") default_active = BooleanProperty(False) font_name = StringProperty(DEFAULT_FONT) + disabled = BooleanProperty(False) def trigger_action(self, *args, **kwargs): return self.checkbox.trigger_action(*args, **kwargs) diff --git a/katrain/gui/popups.py b/katrain/gui/popups.py index 1ba0c1d..43a108c 100644 --- a/katrain/gui/popups.py +++ b/katrain/gui/popups.py @@ -378,6 +378,7 @@ class ConfigPopup(QuickConfigGui): super().__init__(katrain) self.paths = [self.katrain.config("engine/model"), "katrain/models", "~/.katrain"] self.katago_paths = [self.katrain.config("engine/katago"), "~/.katrain"] + Clock.schedule_once(self.check_katas) MDApp.get_running_app().bind(language=self.check_models) MDApp.get_running_app().bind(language=self.check_katas) @@ -385,6 +386,18 @@ class ConfigPopup(QuickConfigGui): super().build_and_set_properties() def check_models(self, *args): + def find_description(path): + file = os.path.split(path)[1] + file_to_desc = { + re.match(r".*/([^/]+)", model)[1].replace(".zip", ".bin.gz"): desc + for mods in [self.MODELS, self.MODEL_DESC] + for desc, model in mods.items() + } + if file in file_to_desc: + return f"{file_to_desc[file]} - {path}" + else: + return path + done = set() model_files = [] for path in self.paths + [self.model_path.text]: @@ -406,13 +419,28 @@ class ConfigPopup(QuickConfigGui): if files and path not in self.paths: self.paths.append(path) # persistent on paths with models found model_files += files + + model_files = sorted([(find_description(path), path) for path in model_files]) models_available_msg = i18n._("models available").format(num=len(model_files)) - self.model_files.values = [models_available_msg] + model_files + self.model_files.values = [models_available_msg] + [desc for desc, path in model_files] + self.model_files.value_keys = [""] + [path for desc, path in model_files] self.model_files.text = models_available_msg def check_katas(self, *args): + def find_description(path): + file = os.path.split(path)[1].replace(".exe", "") + file_to_desc = { + re.match(r".*/([^/]+)", kg)[1].replace(".zip", ""): desc + for _, kgs in self.KATAGOS.items() + for desc, kg in kgs.items() + } + if file in file_to_desc: + return f"{file_to_desc[file]} - {path}" + else: + return path + done = set() - model_files = [] + kata_files = [] for path in self.katago_paths + [self.katago_path.text]: path = path.rstrip("/\\") if path.startswith("katrain"): @@ -432,34 +460,43 @@ class ConfigPopup(QuickConfigGui): ] if files and path not in self.paths: self.paths.append(path) # persistent on paths with models found - model_files += files - katas_available_msg = i18n._("katago binaries available").format(num=len(model_files)) - self.katago_files.values = [katas_available_msg, i18n._("default katago option")] + sorted( - model_files, key=lambda f: "bs29" in f - ) + kata_files += files + + kata_files = [(path, find_description(path)) for path in sorted(kata_files, key=lambda f: "bs29" in f)] + katas_available_msg = i18n._("katago binaries available").format(num=len(kata_files)) + self.katago_files.values = [katas_available_msg, i18n._("default katago option")] + [ + desc for path, desc in kata_files + ] + self.katago_files.value_keys = ["", ""] + [path for path, desc in kata_files] self.katago_files.text = katas_available_msg MODELS = { - "latest 20b": "https://github.com/lightvector/KataGo/releases/download/v1.4.5/g170e-b20c256x2-s5303129600-d1228401921.bin.gz", - "latest 30b": "https://github.com/lightvector/KataGo/releases/download/v1.4.5/g170-b30c320x2-s4824661760-d1229536699.bin.gz", - "latest 40b": "https://github.com/lightvector/KataGo/releases/download/v1.4.5/g170-b40c256x2-s5095420928-d1229425124.bin.gz", + "Latest 20 block model": "https://github.com/lightvector/KataGo/releases/download/v1.4.5/g170e-b20c256x2-s5303129600-d1228401921.bin.gz", + "Latest 30 block model": "https://github.com/lightvector/KataGo/releases/download/v1.4.5/g170-b30c320x2-s4824661760-d1229536699.bin.gz", + "Latest 40 block model": "https://github.com/lightvector/KataGo/releases/download/v1.4.5/g170-b40c256x2-s5095420928-d1229425124.bin.gz", + } + MODEL_DESC = { + "Fat 40 block model": "https://d3dndmfyhecmj0.cloudfront.net/g170/neuralnets/g170e-b40c384x2-s2348692992-d1229892979.zip", + "Latest 15 block model": "https://d3dndmfyhecmj0.cloudfront.net/g170/neuralnets/g170e-b15c192-s1672170752-d466197061.bin.gz", } KATAGOS = { "win": { "OpenCL v1.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-gpu-opencl-windows-x64.zip", - "CUDA v1.6.1 (New NVIDIA cards)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-gpu-cuda10.2-windows-x64.zip", "Eigen AVX2 (Modern CPUs) v1.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-cpu-eigen-avx2-windows-x64.zip", "Eigen (CPU, Non-optimized) v1.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-cpu-eigen-windows-x64.zip", "OpenCL v1.6.1 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1%2Bbs29/katago-v1.6.1+bs29-gpu-opencl-windows-x64.zip", }, "linux": { "OpenCL v1.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-gpu-opencl-linux-x64.zip", - "CUDA v1.6.1 (New NVIDIA cards)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-gpu-cuda10.2-linux-x64.zip", "Eigen AVX2 (Modern CPUs) v1.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-cpu-eigen-avx2-linux-x64.zip", "Eigen (CPU, Non-optimized) v1.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-cpu-eigen-linux-x64.zip", "OpenCL v1.6.1 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1%2Bbs29/katago-v1.6.1+bs29-gpu-opencl-linux-x64.zip", }, + "just-descriptions": { + "CUDA v1.6.1 (Windows)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-gpu-cuda10.2-windows-x64.zip", + "CUDA v1.6.1 (Linux)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-gpu-cuda10.2-linux-x64.zip", + }, } def download_models(self, *_largs): diff --git a/katrain/gui/style.py b/katrain/gui/style.py index dda7081..2065e8f 100644 --- a/katrain/gui/style.py +++ b/katrain/gui/style.py @@ -8,6 +8,8 @@ DEFAULT_FONT = "fonts/NotoSansCJKsc-Regular.otf" WHITE = [0.95, 0.95, 0.95, 1] BLACK = [0.05, 0.05, 0.05, 1] LIGHTGREY = [0.7, 0.7, 0.7, 1] +GREY = [0.5, 0.5, 0.5, 1] +LIGHTERGREY = [0.85, 0.85, 0.85, 1] RED = [0.8, 0.1, 0.1, 1] GREEN = [0.1, 0.8, 0.1, 1] @@ -31,7 +33,7 @@ EVAL_COLORS = { [0.447, 0.129, 0.42, 1], [0.8, 0, 0, 1], [0.9, 0.4, 0.1, 1], - [0.85, 0.89, 0.3, 1], + [0.95, 0.95, 0, 1], [0.67, 0.9, 0.18, 1], [0.117, 0.588, 0, 1], ], @@ -39,7 +41,7 @@ EVAL_COLORS = { [1, 0, 1, 1], [1, 0, 0, 1], [1, 0.5, 0, 1], - [0.85, 0.89, 0.3, 1], + [1, 1, 0, 1], [0, 1, 1, 1], [0, 0, 1, 1], ], @@ -50,6 +52,7 @@ EVAL_DOT_MAX_SIZE = 0.5 EVAL_DOT_MIN_SIZE = 0.25 STONE_COLORS = {"B": BLACK, "W": WHITE} +STONE_CONTRAST_COLORS = {"B": LIGHTERGREY, "W": GREY} OUTLINE_COLORS = {"B": [0.3, 0.3, 0.3, 0.5], "W": [0.7, 0.7, 0.7, 0.5]} STONE_TEXT_COLORS = {"W": BLACK, "B": WHITE} @@ -59,12 +62,16 @@ POLICY_COLOR = [0.9, 0.2, 0.8] STARPOINT_SIZE = 0.1 BOARD_COLOR = [0.85, 0.68, 0.40, 1] STONE_SIZE = 0.505 # texture edge is transparent -VISITS_FRAC_SMALL = 0.1 -GHOST_ALPHA = 0.5 -TOP_MOVE_ALPHA = 0.4 -TOP_MOVE_BORDER_COLOR = [46 / 255, 65 / 255, 88 / 255, 0.9] +HINTS_MIN_ALPHA = 0.2 # single visit +HINTS_ALPHA = 0.8 +POLICY_ALPHA = 0.6 +TOP_POLICY_ALPHA = 0.3 +TOP_MOVE_BORDER_COLOR = [10 / 255, 200 / 255, 250 / 255, 1.0] CHILD_SCALE = 0.95 +HINT_SCALE = 0.975 +UNCERTAIN_HINT_SCALE = 0.7 + # ponder light ENGINE_DOWN_COL = EVAL_COLORS["theme:normal"][1] diff --git a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo index 8db5b15..7254b38 100644 Binary files a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po index 9d1d6c3..2622d3a 100644 --- a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po @@ -521,6 +521,7 @@ msgstr "读秒声音开启" msgid "tab:info-details" msgstr "详细" +#. TODO: updated msgid "teaching undo message" msgstr "撤销第 {move} 手棋因为损失超过 {points_lost:.1f} 目. 悬停在该手棋 上方查看信息. 请再次尝试." @@ -627,3 +628,15 @@ msgstr "所有可用的exe文件已下载" msgid "Engine missing DLL" msgstr "启动katago失败因缺失dll文件. 请检查运行要求, 或尝试让exe文件自行运行并检查细节. " + +#. resign the game +msgid "resign" +msgstr "认输" + +#. Analyze again but exclude all currently considered moves +msgid "analysis:alternatives" +msgstr "寻找其他走法" + +#. status message +msgid "alternative analysis" +msgstr "正在搜索其他走法" diff --git a/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo index 49fc661..5c942fc 100644 Binary files a/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/de/LC_MESSAGES/katrain.po b/katrain/i18n/locales/de/LC_MESSAGES/katrain.po index 9941e6c..722e7ce 100644 --- a/katrain/i18n/locales/de/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/de/LC_MESSAGES/katrain.po @@ -105,12 +105,6 @@ msgstr "AI Zug erzwingen" msgid "pass-button-text" msgstr "Passen" -msgid "teaching undo message" -msgstr "" -"Zug {move} zurückgenommen, da er {points_lost:.1f} Punkte verliert. Bewege " -"Mauszeiger über den Zug,um die erwartete Widerlegung zu sehen. Bitte " -"versuche es erneut." - msgid "ai-ponder" msgstr "" "Zeigt den Enginestatus an von grün (Leerlauf) bis orange(Zugberechnung). Ist" @@ -636,55 +630,62 @@ msgstr "Geschätzte Spielstärke" msgid "reanalyze max visits" msgstr "Anzahl der Visits pro Zug" -#. TODO - on move tree editing msgid "Delete Node" -msgstr "Delete Node" +msgstr "Node löschen" -#. TODO - theme name msgid "theme:red-green-colourblind" -msgstr "Red-Green Colourblind" +msgstr "Rot-Grün-farbenblind" -#. TODO - theme name msgid "theme:normal" -msgstr "Default Theme" +msgstr "Standard-Farbschema" -#. TODO - theme label msgid "theme" -msgstr "Colour Theme" +msgstr "Farbschema" -#. TODO msgid "engine:altcommand:hint" msgstr "" -"Full command used to start the engine. All other model options will be " -"ignored." +"Kompletter Befehl um die Engine zu starten. Alle anderen Modelloptionen " +"werden ignoriert." -#. TODO msgid "engine:altcommand" -msgstr "Override Engine Command" +msgstr "Engine-Befehl überschreiben" -#. TODO - button in general settings for downloading katago executables msgid "download katago button" -msgstr "Download KataGo versions" +msgstr "KataGo-Versionen herunterladen" -#. TODO - shown on on the binaries select dropdown msgid "katago binaries available" -msgstr "{num} available katago versions found" +msgstr "{num} KataGo-Programme verfügbar" -#. TODO - shown on on the katago binary select dropdown msgid "default katago option" -msgstr "Detect platform and use provided OpenCL KataGo binary" +msgstr "Plattform erkennen und angebotenes Programm benutzen" -#. TODO - error message on trying to download models when already done msgid "All binaries downloaded" -msgstr "All available binaries already downloaded" +msgstr "Alle verfügbaren Programme wurden bereits heruntergeladen" -#. TODO msgid "Engine missing DLL" msgstr "" -"Failed to start KataGo due to missing DLL. Check the requirements in the " -"KataGo documentation, or try running the binary by itself for details. " +"Konnte KataGo wegen fehlender DLL nicht starten. Überprüf die Anforderungen " +"in der KataGo-Dokumentation oder versuch, KataGo alleine zu starten, um " +"Einzelheiten zu erfahren." -#. TODO - in addition to colour, show point loss as -0.1 etc on hints/top -#. moves msgid "show point loss as text" -msgstr "Show point loss on top moves" +msgstr "Zeig den Punkteverlust auf Beste Züge" + +#. TODO - updated +msgid "teaching undo message" +msgstr "" +"Zug {move} zurückgenommen, da er {points_lost:.1f} Punkte verliert. Bewege " +"Mauszeiger über den Zug,um die erwartete Widerlegung zu sehen. Bitte " +"versuche es erneut." + +#. resign the game +msgid "resign" +msgstr "Aufgeben" + +#. - Analyze again but exclude all currently considered moves +msgid "analysis:alternatives" +msgstr "Finde alternative Züge" + +#. - status message +msgid "alternative analysis" +msgstr "Suche nach alternativen Zügen" diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo index 2da0389..9c7f8dc 100644 Binary files a/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po index 2e84d25..c5b79e3 100644 --- a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po @@ -102,6 +102,10 @@ msgstr "Fast analysis of all moves" msgid "analysis:game" msgstr "Deeper full game analysis" +#. Analyze again but exclude all currently considered moves +msgid "analysis:alternatives" +msgstr "Find alternative moves" + #. Keep improving current position analysis when there is nothing else to do msgid "analysis:continuous" msgstr "Toggle continuous analysis" @@ -117,6 +121,10 @@ msgstr "Performing additional analysis to {visits} visits" msgid "sweep analysis" msgstr "Analyzing entire board using {visits} visits/move" +#. status message +msgid "alternative analysis" +msgstr "Searching for alternative moves" + #. status message msgid "equalizing analysis" msgstr "Equalizing analysis of candidate moves to {visits} visits" @@ -129,10 +137,16 @@ msgstr "Re-analyzing entire game using {visits} visits" msgid "pass-button-text" msgstr "Pass" +#. resign the game +msgid "resign" +msgstr "Resign" + +#. Shown when a move is undone in teaching mode. msgid "teaching undo message" msgstr "" -"Undid move {move} as it lost {points_lost:.1f} points. Hover over the move " -"to see expected refutation. Please try again." +"Undid move {move} as it lost {points_lost:.1f} points. Please try again. For" +" a hint, hover over the move to see expected refutation, or check expected " +"territory (t) to see the areas of the board where point loss is expected." msgid "ai-ponder" msgstr "" diff --git a/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo index 06f7b0e..75031f1 100644 Binary files a/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/es/LC_MESSAGES/katrain.po b/katrain/i18n/locales/es/LC_MESSAGES/katrain.po index 60d3152..1124967 100644 --- a/katrain/i18n/locales/es/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/es/LC_MESSAGES/katrain.po @@ -105,6 +105,7 @@ msgstr "Forzar jugada IA" msgid "pass-button-text" msgstr "Pasar" +#. TODO: updated msgid "teaching undo message" msgstr "" "Deshice jugada {move} ya que perdía {points_lost:.1f} puntos. Coloca el " @@ -684,3 +685,15 @@ msgstr "" #. moves msgid "show point loss as text" msgstr "Show point loss on top moves" + +#. TODO - resign the game +msgid "resign" +msgstr "Resign" + +#. TODO - Analyze again but exclude all currently considered moves +msgid "analysis:alternatives" +msgstr "Find alternative moves" + +#. TODO - status message +msgid "alternative analysis" +msgstr "Searching for alternative moves" diff --git a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo index 53682e9..83b7aa1 100644 Binary files a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po index bc7fb23..bb070f0 100644 --- a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po @@ -522,6 +522,7 @@ msgstr "" "possiblement pour cause de \n" "mémoire insuffisante : {error}" +#. TODO: updated msgid "teaching undo message" msgstr "" "Le coup {move} a été annulé car il perdait {points_lost:.1f} points. Vous " @@ -683,8 +684,8 @@ msgstr "Palette de couleurs" msgid "engine:altcommand:hint" msgstr "" -"Commande complète exécutée par le moteur de jeu, outrepassant les " -"réseaux/fichiers sélectionnés plus haut." +"Commande personnelle outrepassant l'exécutable, la configuration et le " +"réseau sélectionnés plus haut." msgid "engine:altcommand" msgstr "Redéfinir la commande" @@ -705,9 +706,18 @@ msgstr "Pas de nouveaux exécutables disponibles" msgid "Engine missing DLL" msgstr "" -"Impossible de démarrer KataGo à cause d'un DLL manquant. Veuillez répondre " -"aux conditions requises, ou bien essayez l'exécutable seul pour plus de " +"Impossible de démarrer KataGo à cause d'un DLL manquant. Veuillez vérifier " +"votre configuration système, ou bien essayez l'exécutable seul pour plus de " "détails." msgid "show point loss as text" msgstr "Indiquer les points perdus" + +msgid "resign" +msgstr "Abandon" + +msgid "analysis:alternatives" +msgstr "Proposer des coups différents" + +msgid "alternative analysis" +msgstr "Recherche de coups différents." diff --git a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo index 7785ec1..9c28dd2 100644 Binary files a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po index 81f5789..705244c 100644 --- a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po @@ -129,10 +129,6 @@ msgstr "一局全体を再分析中(各{visits}手)" msgid "pass-button-text" msgstr "パス" -msgid "teaching undo message" -msgstr "" -"待った。{move}は{points_lost:.1f}目損です(マウスカーソルをこの手にかざせばとがめる手順が表示されます)。打ち直してください。" - msgid "ai-ponder" msgstr "この印はエンジンの状態をシンプルに表します(緑=空き、オレンジ=処理中)。エンジンが止まったり起動できなかったりすると赤くなります。" @@ -647,11 +643,11 @@ msgstr "別版KataGoをダウンロード" #. shown on on the binaries select dropdown msgid "katago binaries available" -msgstr "入手済KataGo実行ファイルの一覧({num}個みつかりました)" +msgstr "入手済KataGoの一覧({num}個みつかりました)" #. shown on on the katago binary select dropdown msgid "default katago option" -msgstr "内蔵KataGo" +msgstr "内蔵KataGo(OpenCL版)" #. error message on trying to download models when already done msgid "All binaries downloaded" @@ -666,3 +662,20 @@ msgstr "" #. moves msgid "show point loss as text" msgstr "損失目数を候補手上に表示" + +#. resign the game +msgid "resign" +msgstr "投了" + +#. Analyze again but exclude all currently considered moves +msgid "analysis:alternatives" +msgstr "他の候補手を分析" + +#. message shown when move is undone +msgid "teaching undo message" +msgstr "" +"待った。{move}は{points_lost:.1f}目損です。打ち直してください。ヒント:マウスカーソルをこの手にかざせばとがめる手順が表示されます。あるいは、地合推定をオンに(tキー)すれば盤上どのあたりで損をしたかが見られます。" + +#. status message +msgid "alternative analysis" +msgstr "他の候補手を分析中" diff --git a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo index 1891a07..c14eca3 100644 Binary files a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po index f88a620..eaf984b 100644 --- a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po @@ -546,6 +546,7 @@ msgstr "" "`threshold`선 밑의 수를 더 많이 뽑은 후 그 중 최선의 수를 둡니다. `line_weight`을 증가시키면 바둑판 중앙에 있는" " 수를 더욱 꺼리게 됩니다. 판이 `endgame` 비율만큼 차면 평범하게 둡니다." +#. TODO: updated msgid "teaching undo message" msgstr "" "{points_lost:.1f}집 손해이므로 {move}을/를 물렀습니다. 커서를 올려 상대의 대응책을 보십시오. 다시 두어 주세요." @@ -644,3 +645,12 @@ msgstr "" msgid "show point loss as text" msgstr "돌 위에 집 손해 표시하기" + +msgid "resign" +msgstr "기권" + +msgid "analysis:alternatives" +msgstr "다른 수 찾기" + +msgid "alternative analysis" +msgstr "다른 수를 찾는 중입니다" diff --git a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo index 3458557..5459d12 100644 Binary files a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po index d7ceda8..bfed209 100644 --- a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po @@ -117,10 +117,13 @@ msgstr "Дополнительный анализ всех ходов-канди msgid "pass-button-text" msgstr "Пас" +#. Shown when a move is undone in teaching mode. msgid "teaching undo message" msgstr "" -"Ход {move} отменён, т.к. это потеря {points_lost:.1f} очков. Наведите на " -"этот ход чтобы увидеть опровергающее продолжение. Попытайтесь снова." +"Ход {move} отменён, т.к. это потеря {points_lost:.1f} очков. Попытайтесь " +"снова. Наведите на этот ход, чтобы увидеть опровергающее продолжение, или " +"посмотрите на ожидаемую территорию (t), чтобы увидеть, где на доске " +"прогнозируется потеря очков." msgid "ai-ponder" msgstr "" @@ -672,3 +675,12 @@ msgstr "" msgid "show point loss as text" msgstr "Показывать потерянные очки текстом" + +msgid "resign" +msgstr "Сдаться" + +msgid "analysis:alternatives" +msgstr "Поискать альтернативные ходы" + +msgid "alternative analysis" +msgstr "Поиск альтернативных ходов" diff --git a/katrain/img/Alternative.png b/katrain/img/Alternative.png new file mode 100644 index 0000000..5f23ca2 Binary files /dev/null and b/katrain/img/Alternative.png differ diff --git a/katrain/img/topmove.png b/katrain/img/topmove.png new file mode 100644 index 0000000..3b29783 Binary files /dev/null and b/katrain/img/topmove.png differ diff --git a/katrain/popups.kv b/katrain/popups.kv index 5522888..c9c5265 100644 --- a/katrain/popups.kv +++ b/katrain/popups.kv @@ -109,12 +109,16 @@ on_press: root.download_katas() size_hint_y: 0.7 AnchorLayout: - Spinner: + KeyValueSpinner: id: katago_files text: '' - on_text: - if self.text and self.text == self.values[1]: katago_path.text = '' - elif self.text and self.text != self.values[0]: katago_path.text = args[1] + text_autoupdate: True + -text_size: self.width, None + -halign: 'center' + -shorten: True + -shorten_from: 'right' + on_select: + if self.selected_index != 0: katago_path.text = self.value_keys[self.selected_index] size_hint_y: 0.7 sync_height_frac: 1.0 -font_size: self.height * 0.5 @@ -140,10 +144,16 @@ on_press: root.download_models() size_hint_y: 0.7 AnchorLayout: - Spinner: + KeyValueSpinner: id: model_files text: '' - on_text: if self.text and self.text != self.values[0]: model_path.text = args[1] + text_autoupdate: True + -text_size: self.width, None + -halign: 'center' + -shorten: True + -shorten_from: 'right' + on_select: + if self.selected_index != 0: model_path.text = self.value_keys[self.selected_index] size_hint_y: 0.7 sync_height_frac: 1.0 -font_size: self.height * 0.5 diff --git a/spec/KaTrain.spec b/spec/KaTrain.spec index 9521274..6de304e 100644 --- a/spec/KaTrain.spec +++ b/spec/KaTrain.spec @@ -1,6 +1,7 @@ # -*- mode: python ; coding: utf-8 -*- from kivy_deps import sdl2, glew from kivymd import hooks_path as kivymd_hooks_path +import subprocess block_cipher = None @@ -45,6 +46,8 @@ print("DATA FILTERED", len(a.datas)) console_names = {True:"DebugKaTrain",False:"KaTrain"} +powershell = subprocess.Popen(["powershell"], stdout=subprocess.PIPE, stdin=subprocess.PIPE) + for console, name in console_names.items(): @@ -89,4 +92,9 @@ for console, name in console_names.items(): console=console, icon="C:\\icon.ico", ) + powershell.stdin.write(f"Set-AuthenticodeSignature dist/{name}.exe -Certificate (Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert)\n".encode('ascii')) + powershell.stdin.write(f"Set-AuthenticodeSignature dist/{name}/{name}.exe -Certificate (Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert)\n".encode('ascii')) + powershell.stdin.flush() +while True: + print(powershell.stdout.readline()) \ No newline at end of file