diff --git a/.gitignore b/.gitignore index ea3ff0e..031afa5 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ tmp.pickle my logs callgrind.* +cpp/out +.vs # debug outdated_log.txt diff --git a/README.md b/README.md index 634cc85..3cb03d7 100644 --- a/README.md +++ b/README.md @@ -177,9 +177,10 @@ 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 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 left]** or **[z]**: Undo move. Hold alt for 5 moves at a time, or ctrl to skip to the start. +* **[arrow right]** or **[x]**: Redo move. Hold alt for 5 moves at a time, or ctrl to skip to the start. * **[arrow up/down]** Switch branch, as would be expected from the move tree. +* **[home/end]** Go to the beginning/end of the game. * **[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. * **[click on a move]**: See detailed statistics for a previous move, along with expected variation that was best instead of this move. diff --git a/katrain/KataGo/katago b/katrain/KataGo/katago index 99298c0..a85dce4 100755 Binary files a/katrain/KataGo/katago and b/katrain/KataGo/katago differ diff --git a/katrain/KataGo/katago.exe b/katrain/KataGo/katago.exe index b416e3c..e9b5f18 100644 Binary files a/katrain/KataGo/katago.exe and b/katrain/KataGo/katago.exe differ diff --git a/katrain/__main__.py b/katrain/__main__.py index 2b1c17a..9252395 100644 --- a/katrain/__main__.py +++ b/katrain/__main__.py @@ -194,14 +194,14 @@ class KaTrainGui(Screen, KaTrainBase): teaching_undo = cn.player and last_player.being_taught and cn.parent if ( teaching_undo - and cn.analysis_ready - and cn.parent.analysis_ready + and cn.analysis_complete + and cn.parent.analysis_complete and not cn.children and not self.game.end_result ): self.game.analyze_undo(cn) # not via message loop if ( - cn.analysis_ready + cn.analysis_complete and next_player.ai and not cn.children and not self.game.end_result @@ -352,7 +352,7 @@ class KaTrainGui(Screen, KaTrainBase): self.controls.timer.paused = True if not self.ai_settings_popup: self.ai_settings_popup = I18NPopup( - title_key="ai settings", size=[dp(600), dp(650)], content=ConfigAIPopup(self) + title_key="ai settings", size=[dp(750), dp(750)], content=ConfigAIPopup(self) ).__self__ self.ai_settings_popup.content.popup = self.ai_settings_popup self.ai_settings_popup.open() @@ -468,7 +468,8 @@ class KaTrainGui(Screen, KaTrainBase): return else: return - + ctrl_pressed = "ctrl" in modifiers + alt_pressed = "alt" in modifiers shortcuts = self.shortcuts if keycode[1] == "tab": self.play_mode.switch_ui_mode() @@ -476,26 +477,30 @@ class KaTrainGui(Screen, KaTrainBase): self.nav_drawer.set_state("toggle") elif keycode[1] == "spacebar": self.toggle_continuous_analysis() - elif keycode[1] == "b" and "ctrl" not in modifiers: + elif keycode[1] == "b" and ctrl_pressed: self.controls.timer.paused = not self.controls.timer.paused - elif keycode[1] in ["`", "~", "m"] and "ctrl" not in modifiers: + elif keycode[1] in ["`", "~", "m"] and ctrl_pressed: self.zen = (self.zen + 1) % 3 elif keycode[1] in ["left", "z"]: - self("undo", 1 + ("alt" in modifiers) * 9 + ("ctrl" in modifiers) * 999) + self("undo", 1 + alt_pressed * 4 + (ctrl_pressed and not alt_pressed) * 999) elif keycode[1] in ["right", "x"]: - self("redo", 1 + ("alt" in modifiers) * 9 + ("ctrl" in modifiers) * 999) - elif keycode[1] == "n" and "ctrl" in modifiers: + self("redo", 1 + alt_pressed * 4 + (ctrl_pressed and not alt_pressed) * 999) + elif keycode[1] == "home": + self("undo", 999) + elif keycode[1] == "end": + self("redo", 999) + elif keycode[1] == "n" and ctrl_pressed: self("new-game-popup") - elif keycode[1] == "l" and "ctrl" in modifiers: + elif keycode[1] == "l" and ctrl_pressed: self("analyze-sgf-popup") - elif keycode[1] == "s" and "ctrl" in modifiers: + elif keycode[1] == "s" and ctrl_pressed: self("output-sgf") - elif keycode[1] == "c" and "ctrl" in modifiers: + elif keycode[1] == "c" and ctrl_pressed: Clipboard.copy(self.game.root.sgf()) self.controls.set_status(i18n._("Copied SGF to clipboard."), STATUS_INFO) - elif keycode[1] == "v" and "ctrl" in modifiers: + elif keycode[1] == "v" and ctrl_pressed: self.load_sgf_from_clipboard() - elif keycode[1] in shortcuts.keys() and "ctrl" not in modifiers: + elif keycode[1] in shortcuts.keys() and not ctrl_pressed: shortcut = shortcuts[keycode[1]] if isinstance(shortcut, Widget): shortcut.trigger_action(duration=0) diff --git a/katrain/config.json b/katrain/config.json index 78094cb..ea3afd0 100644 --- a/katrain/config.json +++ b/katrain/config.json @@ -92,7 +92,17 @@ "max_points_lost": 1.75, "settled_weight": 1.0, "opponent_fac": 0.5, - "min_visits": 3 + "min_visits": 3, + "attach_penalty": 1, + "tenuki_penalty": 0.5 + }, + "ai:settle": { + "max_points_lost": 1.75, + "settled_weight": 1.0, + "opponent_fac": 0.5, + "min_visits": 3, + "attach_penalty": 1, + "tenuki_penalty": 0.5 }, "ai:p:weighted": { "weaken_fac": 1.25, diff --git a/katrain/core/ai.py b/katrain/core/ai.py index 3440d15..b15a1d6 100644 --- a/katrain/core/ai.py +++ b/katrain/core/ai.py @@ -18,7 +18,6 @@ from katrain.core.constants import ( AI_RANK, AI_SCORELOSS, AI_SCORELOSS_ELO, - AI_SIMPLE, AI_SIMPLE_OWNERSHIP, AI_STRATEGIES_PICK, AI_STRATEGIES_POLICY, @@ -33,6 +32,7 @@ from katrain.core.constants import ( OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_INFO, + AI_SETTLE_STONES, ) from katrain.core.game import Game, GameNode, Move from katrain.core.utils import var_to_grid @@ -164,9 +164,10 @@ def request_ai_analysis(game: Game, cn: GameNode, extra_settings: Dict) -> Optio error = False analysis = None - def set_analysis(a): + def set_analysis(a, partial_result): nonlocal analysis - analysis = a + if not partial_result: + analysis = a def set_error(a): nonlocal error @@ -205,14 +206,7 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, game.katrain.log(f"Error getting handicap-based move", OUTPUT_ERROR) ai_mode = AI_DEFAULT - if ai_mode == AI_SIMPLE: - simple_moves = ai_settings["simple_moves"] - simple_analysis = request_ai_analysis(game, cn, {"simpleMovesBias": simple_moves, "wideRootNoise": 0.10}) - if not simple_analysis: - game.katrain.log(f"Error getting simple-biased move", OUTPUT_ERROR) - ai_mode = AI_DEFAULT - - while not cn.analysis_ready: + while not cn.analysis_complete: time.sleep(0.01) game.engines[cn.next_player].check_alive(exception_if_dead=True) @@ -335,18 +329,6 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, candidate_ai_moves = cn.candidate_moves if ai_mode == AI_HANDICAP: candidate_ai_moves = handicap_analysis["moveInfos"] - if ai_mode == AI_SIMPLE: - candidate_ai_moves = simple_analysis["moveInfos"] - for data in candidate_ai_moves: - print( - "{order} {move}: visits {visits} utility {util} utilityLcb {lcb}".format( - visits=data["visits"], - order=data["order"], - move=data["move"], - util=data["utility"], - lcb=data["utilityLcb"], - ) - ) top_cand = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player) if top_cand.is_pass and ai_mode not in [ @@ -376,49 +358,87 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, 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." - elif ai_mode == AI_SIMPLE_OWNERSHIP: - - def settledness(d, player_fac): - return sum([abs(o) for o in d["ownership"] if player_fac * o > 0]) - + elif ai_mode in [AI_SIMPLE_OWNERSHIP, AI_SETTLE_STONES]: + stones_with_player = {(*s.coords, s.player) for s in game.stones} next_player_sign = cn.player_sign(cn.next_player) + if ai_mode == AI_SIMPLE_OWNERSHIP: + + def settledness(d, player_sign, player): + return sum([abs(o) for o in d["ownership"] if player_sign * o > 0]) + + else: + board_size_x, board_size_y = game.board_size + + def settledness(d, player_sign, player): + ownership_grid = var_to_grid(d["ownership"], (board_size_x, board_size_y)) + return sum( + [abs(ownership_grid[s.coords[0]][s.coords[1]]) for s in game.stones if s.player == player] + ) + + def is_attachment(move): + if move.is_pass: + return False + attach_opponent_stones = sum( + (move.coords[0] + dx, move.coords[1] + dy, cn.player) in stones_with_player + for dx in [-1, 0, 1] + for dy in [-1, 0, 1] + if abs(dx) + abs(dy) == 1 + ) + nearby_own_stones = sum( + (move.coords[0] + dx, move.coords[1] + dy, cn.next_player) in stones_with_player + for dx in [-2, 0, 1, 2] + for dy in [-2 - 1, 0, 1, 2] + if abs(dx) + abs(dy) <= 2 # allows clamps/jumps + ) + return attach_opponent_stones >= 1 and nearby_own_stones == 0 + + def is_tenuki(d): + return not d.is_pass and not any( + not node + or not node.move + or node.move.is_pass + or max(abs(last_c - cand_c) for last_c, cand_c in zip(node.move.coords, d.coords)) < 5 + for node in [cn, cn.parent] + ) + moves_with_settledness = sorted( [ ( - Move.from_gtp(d["move"], player=cn.next_player), - settledness(d, next_player_sign), - settledness(d, -next_player_sign), + move, + settledness(d, next_player_sign, cn.next_player), + settledness(d, -next_player_sign, cn.player), + is_attachment(move), + is_tenuki(move), d, ) for d in candidate_ai_moves if d["pointsLost"] < ai_settings["max_points_lost"] and "ownership" in d - and (d["order"] < 5 or d["visits"] >= ai_settings.get("min_visits", 1)) + and (d["order"] <= 1 or d["visits"] >= ai_settings.get("min_visits", 1)) + for move in [Move.from_gtp(d["move"], player=cn.next_player)] + if not (move.is_pass and d["pointsLost"] > 0.75) ], - key=lambda t: t[3]["pointsLost"] + key=lambda t: t[5]["pointsLost"] + + ai_settings["attach_penalty"] * t[3] + + ai_settings["tenuki_penalty"] * t[4] - ai_settings["settled_weight"] * (t[1] + ai_settings["opponent_fac"] * t[2]), ) if moves_with_settledness: cands = [ - f"{move.gtp()} ({d['pointsLost']:.1f} pt lost, {d['visits']} visits, {settled:.1f} settledness, {oppsettled:.1f} opponent settledness)" - for move, settled, oppsettled, d in moves_with_settledness[:5] + f"{move.gtp()} ({d['pointsLost']:.1f} pt lost, {d['visits']} visits, {settled:.1f} settledness, {oppsettled:.1f} opponent settledness{', attachment' if isattach else ''}{', tenuki' if istenuki else ''})" + for move, settled, oppsettled, isattach, istenuki, d in moves_with_settledness[:5] ] - ai_thoughts += f"Simple ownership strategy. Top 5 Candidates {', '.join(cands)} " + ai_thoughts += f"{ai_mode} strategy. Top 5 Candidates {', '.join(cands)} " aimove = moves_with_settledness[0][0] else: - game.katrain.log( - "No moves found - are you using an older KataGo with no per-move ownership info?", OUTPUT_ERROR - ) - aimove = top_cand + raise (Exception("No moves found - are you using an older KataGo with no per-move ownership info?")) else: - if ai_mode not in [AI_DEFAULT, AI_HANDICAP, AI_SIMPLE]: + if ai_mode not in [AI_DEFAULT, AI_HANDICAP]: 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 if ai_mode == AI_HANDICAP: ai_thoughts += f"Handicap strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move. PDA based score {cn.format_score(handicap_analysis['rootInfo']['scoreLead'])} and win rate {cn.format_winrate(handicap_analysis['rootInfo']['winrate'])}" - elif ai_mode == AI_SIMPLE: - ai_thoughts += f"Simple moves strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move. " else: ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move" game.katrain.log(f"AI thoughts: {ai_thoughts}", OUTPUT_DEBUG) diff --git a/katrain/core/constants.py b/katrain/core/constants.py index b34c05e..5dea88c 100644 --- a/katrain/core/constants.py +++ b/katrain/core/constants.py @@ -1,6 +1,6 @@ -VERSION = "1.5.1" +VERSION = "1.6.0" HOMEPAGE = "https://github.com/sanderland/katrain" -CONFIG_MIN_VERSION = "1.5.1" # keep config files from this version +CONFIG_MIN_VERSION = "1.6.0" # keep config files from this version OUTPUT_ERROR = -1 OUTPUT_KATAGO_STDERR = -0.5 @@ -33,20 +33,21 @@ AI_TENUKI = "ai:p:tenuki" AI_INFLUENCE = "ai:p:influence" AI_TERRITORY = "ai:p:territory" AI_RANK = "ai:p:rank" -AI_SIMPLE = "ai:disabled" AI_SIMPLE_OWNERSHIP = "ai:simple" +AI_SETTLE_STONES = "ai:settle" AI_CONFIG_DEFAULT = AI_RANK -AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_HANDICAP, AI_SIMPLE, AI_SCORELOSS, AI_JIGO] +AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_HANDICAP, AI_SCORELOSS, AI_SIMPLE_OWNERSHIP, AI_SETTLE_STONES, AI_JIGO] AI_STRATEGIES_PICK = [AI_PICK, AI_LOCAL, AI_TENUKI, AI_INFLUENCE, AI_TERRITORY, AI_RANK] 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_RANK, - AI_SIMPLE_OWNERSHIP, AI_HANDICAP, + AI_SIMPLE_OWNERSHIP, + AI_SETTLE_STONES, AI_SCORELOSS, AI_POLICY, AI_WEIGHTED, @@ -70,7 +71,8 @@ AI_STRENGTH = { # dan ranks, backup if model is missing. TODO: remove some? AI_INFLUENCE: -7, AI_TERRITORY: -7, AI_RANK: float("nan"), - AI_SIMPLE_OWNERSHIP: 3, + AI_SIMPLE_OWNERSHIP: 2, + AI_SETTLE_STONES: 2, } AI_OPTION_VALUES = { @@ -90,11 +92,22 @@ AI_OPTION_VALUES = { "automatic": "bool", "pda": [(x / 10, f"{'W' if x<0 else 'B'}+{abs(x/10):.1f}") for x in range(-30, 31)], "max_points_lost": [x / 10 for x in range(51)], - "settled_weight": [x / 4 for x in range(1, 17)], + "settled_weight": [x / 4 for x in range(0, 17)], "opponent_fac": [x / 10 for x in range(-20, 11)], "min_visits": range(1, 10), + "attach_penalty": [x / 10 for x in range(-10, 51)], + "tenuki_penalty": [x / 10 for x in range(-10, 51)], +} +AI_KEY_PROPERTIES = { + "kyu_rank", + "strength", + "weaken_fac", + "pick_frac", + "pick_n", + "automatic", + "max_points_lost", + "min_visits", } -AI_KEY_PROPERTIES = {"kyu_rank", "strength", "weaken_fac", "pick_frac", "pick_n", "automatic"} CALIBRATED_RANK_ELO = [ diff --git a/katrain/core/engine.py b/katrain/core/engine.py index 8e0f48d..490b97c 100644 --- a/katrain/core/engine.py +++ b/katrain/core/engine.py @@ -200,11 +200,13 @@ class KataGoEngine: elif "warning" in analysis: self.katrain.log(f"{analysis} received from KataGo", OUTPUT_DEBUG) else: - if not analysis.get("isDuringSearch", False): + partial_result = analysis.get("isDuringSearch", False) + if not partial_result: del self.queries[query_id] time_taken = time.time() - start_time self.katrain.log( - f"[{time_taken:.1f}][{query_id}] KataGo Analysis Received: {analysis.keys()}", OUTPUT_DEBUG, + f"[{time_taken:.1f}][{query_id}][{'....' if partial_result else 'done'}] KataGo Analysis Received: {analysis.keys()}", + OUTPUT_DEBUG, ) self.katrain.log(line, OUTPUT_EXTRA_DEBUG) try: @@ -256,7 +258,7 @@ class KataGoEngine: ownership: Optional[bool] = None, next_move: Optional[GameNode] = None, extra_settings: Optional[Dict] = None, - report_during_search: bool = False, + report_every: Optional[float] = None, ): nodes = analysis_node.nodes_from_root moves = [m for node in nodes for m in node.moves] @@ -305,5 +307,7 @@ class KataGoEngine: "moves": [[m.player, m.gtp()] for m in moves], "overrideSettings": {**settings, **(extra_settings or {})}, } - self.send_query(query, callback, error_callback, next_move, report_during_search) + if report_every is not None: + query["reportDuringSearchEvery"] = report_every + self.send_query(query, callback, error_callback, next_move) analysis_node.analysis_visits_requested = max(analysis_node.analysis_visits_requested, visits) diff --git a/katrain/core/game.py b/katrain/core/game.py index d127596..c3a4db9 100644 --- a/katrain/core/game.py +++ b/katrain/core/game.py @@ -86,7 +86,9 @@ class Game: def analyze_all_nodes(self, priority=0, analyze_fast=False): for node in self.root.nodes_in_tree: - node.analyze(self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast) + node.analyze( + self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast #, report_every=None + ) # -- move tree functions -- def _calculate_groups(self): @@ -337,13 +339,13 @@ class Game: min_visits = min(node.analysis_visits_requested for node in nodes) visits = min_visits + engine.config["max_visits"] for node in nodes: - node.analyze(engine, visits=visits, priority=-1_000_000, time_limit=False) + node.analyze(engine, visits=visits, priority=-1_000_000, time_limit=False, report_every=None) self.katrain.controls.set_status(i18n._("game re-analysis").format(visits=visits), STATUS_ANALYSIS) return elif mode == "sweep": board_size_x, board_size_y = self.board_size - if cn.analysis_ready: + if cn.analysis_exists: policy_grid = ( var_to_grid(self.current_node.policy, size=(board_size_x, board_size_y)) if self.current_node.policy @@ -369,7 +371,7 @@ class Game: self.katrain.controls.set_status(i18n._("sweep analysis").format(visits=visits), STATUS_ANALYSIS) priority = -1_000_000_000 elif mode in ["equalize", "alternative"]: - if not cn.analysis_ready: + if not cn.analysis_complete: self.katrain.controls.set_status(i18n._("wait-before-equalize"), STATUS_INFO, self.current_node) return @@ -387,13 +389,13 @@ class Game: for move in analyze_moves: if cn.analysis["moves"].get(move.gtp(), {"visits": 0})["visits"] < visits: cn.analyze( - engine, priority, visits=visits, refine_move=move, time_limit=False + engine, priority, visits=visits, refine_move=move, time_limit=False, report_every=None ) # explicitly requested so take as long as you need def analyze_undo(self, node): train_config = self.katrain.config("trainer") move = node.move - if node != self.current_node or node.auto_undo is not None or not node.analysis_ready or not move: + if node != self.current_node or node.auto_undo is not None or not node.analysis_complete or not move: return points_lost = node.points_lost thresholds = train_config["eval_thresholds"] diff --git a/katrain/core/game_node.py b/katrain/core/game_node.py index 62ad6ff..6a1b894 100644 --- a/katrain/core/game_node.py +++ b/katrain/core/game_node.py @@ -13,7 +13,7 @@ class GameNode(SGFNode): def __init__(self, parent=None, properties=None, move=None): super().__init__(parent=parent, properties=properties, move=move) - self.analysis = {"moves": {}, "root": None} + self.analysis = {"moves": {}, "root": None, "completed": False} self.ownership = None self.policy = None self.auto_undo = None # None = not analyzed. False: not undone (good move). True: undone (bad move) @@ -34,8 +34,8 @@ class GameNode(SGFNode): show_class = False if ( self.parent - and self.parent.analysis_ready - and self.analysis_ready + and self.parent.analysis_exists + and self.analysis_exists and (note or ((save_comments_player or {}).get(self.player, False) and show_class)) ): candidate_moves = self.parent.candidate_moves @@ -77,18 +77,18 @@ class GameNode(SGFNode): refine_move=None, analyze_fast=False, find_alternatives=False, - report_during_search=False, + report_every=0.25, ): engine.request_analysis( self, - lambda result: self.set_analysis(result, refine_move, find_alternatives), + lambda result, partial_result: self.set_analysis(result, refine_move, find_alternatives, partial_result), priority=priority, visits=visits, analyze_fast=analyze_fast, time_limit=time_limit, next_move=refine_move, find_alternatives=find_alternatives, - report_during_search=report_during_search, + report_every=report_every, ) def update_move_analysis(self, move_analysis, move_gtp): @@ -104,7 +104,9 @@ class GameNode(SGFNode): if cur["visits"] < move_analysis["visits"]: cur.update(move_analysis) - def set_analysis(self, analysis_json: Dict, refine_move: Optional[Move], alternatives_mode: bool): + def set_analysis( + self, analysis_json: Dict, refine_move: Optional[Move], alternatives_mode: bool, partial_result: bool = False + ): if refine_move: pvtail = analysis_json["moveInfos"][0]["pv"] if analysis_json["moveInfos"] else [] self.update_move_analysis( @@ -113,7 +115,7 @@ class GameNode(SGFNode): else: if alternatives_mode: for m in analysis_json["moveInfos"]: - m["order"] += 10 # offset for not making this top + m["order"] += 100 # 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 @@ -130,14 +132,20 @@ class GameNode(SGFNode): self.parent.update_move_analysis( analysis_json["rootInfo"], self.move.gtp() ) # update analysis in parent for consistency + is_normal_query = refine_move is None and not alternatives_mode + self.analysis["completed"] = self.analysis["completed"] or (is_normal_query and not partial_result) @property - def analysis_ready(self): + def analysis_exists(self): return self.analysis["root"] is not None + @property + def analysis_complete(self): + return self.analysis["completed"] and self.analysis["root"] is not None + @property def score(self) -> Optional[float]: - if self.analysis_ready: + if self.analysis_exists: return self.analysis["root"].get("scoreLead") def format_score(self, score=None): @@ -147,7 +155,7 @@ class GameNode(SGFNode): @property def winrate(self) -> Optional[float]: - if self.analysis_ready: + if self.analysis_exists: return self.analysis["root"].get("winrate") def format_winrate(self, win_rate=None): @@ -178,12 +186,12 @@ class GameNode(SGFNode): return "" text = i18n._("move").format(number=self.depth) + f": {single_move.player} {single_move.gtp()}\n" - if self.analysis_ready: + if self.analysis_exists: score = self.score if sgf: text += i18n._("Info:score").format(score=self.format_score(score)) + "\n" text += i18n._("Info:winrate").format(winrate=self.format_winrate()) + "\n" - if self.parent and self.parent.analysis_ready: + if self.parent and self.parent.analysis_exists: previous_top_move = self.parent.candidate_moves[0] if sgf or details: if previous_top_move["move"] != single_move.gtp(): @@ -210,14 +218,15 @@ class GameNode(SGFNode): text += policy_best_msg.format(move=pol_move, probability=pol_prob) + "\n" if self.auto_undo and sgf: text += i18n._("Info:teaching undo") + "\n" - top_pv = self.analysis_ready and self.candidate_moves[0].get("pv") + top_pv = self.analysis_exists and self.candidate_moves[0].get("pv") if top_pv: text += i18n._("Info:undo predicted PV").format(pv=f"{self.next_player}{' '.join(top_pv)}") + "\n" - if self.ai_thoughts and (sgf or details): - text += "\n" + i18n._("Info:AI thoughts").format(thoughts=self.ai_thoughts) else: text = i18n._("No analysis available") if sgf else i18n._("Analyzing move...") + if self.ai_thoughts and (sgf or details): + text += "\n" + i18n._("Info:AI thoughts").format(thoughts=self.ai_thoughts) + if "C" in self.properties: text += "\n[u]SGF Comments:[/u]\n" + "\n".join(self.properties["C"]) @@ -226,7 +235,7 @@ class GameNode(SGFNode): @property def points_lost(self) -> Optional[float]: single_move = self.move - if single_move and self.parent and self.analysis_ready and self.parent.analysis_ready: + if single_move and self.parent and self.analysis_exists and self.parent.analysis_exists: parent_score = self.parent.score score = self.score return self.player_sign(single_move.player) * (parent_score - score) @@ -238,8 +247,8 @@ class GameNode(SGFNode): single_move and self.parent and self.parent.parent - and self.analysis_ready - and self.parent.parent.analysis_ready + and self.analysis_exists + and self.parent.parent.analysis_exists ): parent_parent_score = self.parent.parent.score score = self.score @@ -251,7 +260,7 @@ class GameNode(SGFNode): @property def candidate_moves(self) -> List[Dict]: - if not self.analysis_ready: + if not self.analysis_exists: return [] if not self.analysis["moves"]: polmoves = self.policy_ranking diff --git a/katrain/gui/badukpan.py b/katrain/gui/badukpan.py index e5d48b4..05e987e 100644 --- a/katrain/gui/badukpan.py +++ b/katrain/gui/badukpan.py @@ -140,7 +140,7 @@ class BadukPanWidget(Widget): 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: + if nodes_here[-1].parent.analysis_exists: self.set_animating_pv(nodes_here[-1].parent.candidate_moves[0]["pv"], nodes_here[-1].parent) self.ghost_stone = None @@ -499,7 +499,7 @@ class BadukPanWidget(Widget): for child_node in current_node.children: move = child_node.move if move and move.coords is not None: - if child_node.analysis_ready: + if child_node.analysis_exists: self.active_pv_moves.append( (move.coords, [move.gtp()] + child_node.candidate_moves[0]["pv"], current_node) ) diff --git a/katrain/gui/controlspanel.py b/katrain/gui/controlspanel.py index 592afc5..246af4c 100644 --- a/katrain/gui/controlspanel.py +++ b/katrain/gui/controlspanel.py @@ -123,7 +123,7 @@ class ControlsPanel(BoxLayout): self.active_comment_node = current_node.children[-1] elif current_node.parent: self.active_comment_node = current_node.parent - elif both_players_are_robots and not current_node.analysis_ready and current_node.parent: + elif both_players_are_robots and not current_node.analysis_exists and current_node.parent: self.active_comment_node = current_node.parent lock_ai = katrain.config("trainer/lock_ai") and katrain.play_analyze_mode == MODE_PLAY @@ -134,7 +134,7 @@ class ControlsPanel(BoxLayout): teach=katrain.players_info[self.active_comment_node.player].being_taught, details=details ) - if self.active_comment_node.analysis_ready: + if self.active_comment_node.analysis_exists: self.stats.score = self.active_comment_node.format_score() or "" self.stats.winrate = self.active_comment_node.format_winrate() or "" self.stats.points_lost = self.active_comment_node.points_lost diff --git a/katrain/gui/popups.py b/katrain/gui/popups.py index f8808c0..0663a35 100644 --- a/katrain/gui/popups.py +++ b/katrain/gui/popups.py @@ -342,7 +342,7 @@ class ConfigAIPopup(QuickConfigGui): self.options_grid.clear_widgets() self.help_label.text = i18n._(strategy.replace("ai:", "aihelp:")) for k, v in sorted(mode_settings.items(), key=lambda kv: (kv[0] not in AI_KEY_PROPERTIES, kv[0])): - self.options_grid.add_widget(DescriptionLabel(text=k, size_hint_x=0.25)) + self.options_grid.add_widget(DescriptionLabel(text=k, size_hint_x=0.275)) if k in AI_OPTION_VALUES: values = AI_OPTION_VALUES[k] if values == "bool": diff --git a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo index 36b6916..d7f987d 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 91de790..f16310b 100644 --- a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po @@ -652,3 +652,37 @@ msgstr "Think for at least {num} seconds before playing." #. TODO - in minutes msgid "main time" msgstr "Main time (min.)" + +#. TODO +msgid "aihelp:simple" +msgstr "" +"Plays moves that lead to simplifying the board state. Of moves that lose at " +"most 'max_points_lost' points and with at least 'min_visits', prefer moves " +"that lose fewer points, settle own territory more (with importance " +"settled_weight), settle opponent's territory more (importance settled_weight" +" * opponent fac), avoid tenuki (more than 5 spaces, importance " +"tenuki_penalty), and avoid attachments (importance attachment_penalty). " +"Weights can be negative to make it complicate the game instead, and wide " +"root noise (in general settings) can be used to further play according to " +"the style at the cost of strength. " + +#. TODO +msgid "ai:simple" +msgstr "Simple Style" + +#. TODO +msgid "aihelp:settle" +msgstr "" +"Plays moves that lead to clarity on stones' status, favouring solid moves " +"rather than complex sabaki. Of moves that lose at most 'max_points_lost' " +"points and with at least 'min_visits', prefer moves that lose fewer points, " +"settle own stones more (with importance settled_weight), settle opponent's " +"stones more (importance settled_weight * opponent fac), avoid tenuki (more " +"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance" +" attachment_penalty). Weights can be negative to make it complicate the game" +" instead, and wide root noise (in general settings) can be used to further " +"play according to the style at the cost of strength. " + +#. TODO +msgid "ai:settle" +msgstr "Settle Style" diff --git a/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo index 34b36de..30525fe 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 93dc91b..78cc749 100644 --- a/katrain/i18n/locales/de/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/de/LC_MESSAGES/katrain.po @@ -696,8 +696,42 @@ msgstr "Minimal time use in byo-yomi (s)" #. TODO msgid "move too fast" -msgstr "Think for at least %d seconds before playing." +msgstr "Think for at least {num} seconds before playing." #. TODO - in minutes msgid "main time" msgstr "Main time (min.)" + +#. TODO +msgid "aihelp:simple" +msgstr "" +"Plays moves that lead to simplifying the board state. Of moves that lose at " +"most 'max_points_lost' points and with at least 'min_visits', prefer moves " +"that lose fewer points, settle own territory more (with importance " +"settled_weight), settle opponent's territory more (importance settled_weight" +" * opponent fac), avoid tenuki (more than 5 spaces, importance " +"tenuki_penalty), and avoid attachments (importance attachment_penalty). " +"Weights can be negative to make it complicate the game instead, and wide " +"root noise (in general settings) can be used to further play according to " +"the style at the cost of strength. " + +#. TODO +msgid "ai:simple" +msgstr "Simple Style" + +#. TODO +msgid "aihelp:settle" +msgstr "" +"Plays moves that lead to clarity on stones' status, favouring solid moves " +"rather than complex sabaki. Of moves that lose at most 'max_points_lost' " +"points and with at least 'min_visits', prefer moves that lose fewer points, " +"settle own stones more (with importance settled_weight), settle opponent's " +"stones more (importance settled_weight * opponent fac), avoid tenuki (more " +"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance" +" attachment_penalty). Weights can be negative to make it complicate the game" +" instead, and wide root noise (in general settings) can be used to further " +"play according to the style at the cost of strength. " + +#. TODO +msgid "ai:settle" +msgstr "Settle Style" diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo index 8ad3bbd..c55b7a7 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 92f8f8f..c92796e 100644 --- a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po @@ -571,6 +571,36 @@ msgstr "" "perspective. When `automatic` is set, KaTrain will find a suitable value " "automatically." +msgid "ai:simple" +msgstr "Simple Style" + +msgid "aihelp:simple" +msgstr "" +"Plays moves that lead to simplifying the board state. Of moves that lose at " +"most 'max_points_lost' points and with at least 'min_visits', prefer moves " +"that lose fewer points, settle own territory more (with importance " +"settled_weight), settle opponent's territory more (importance settled_weight" +" * opponent fac), avoid tenuki (more than 5 spaces, importance " +"tenuki_penalty), and avoid attachments (importance attachment_penalty). " +"Weights can be negative to make it complicate the game instead, and wide " +"root noise (in general settings) can be used to further play according to " +"the style at the cost of strength. " + +msgid "ai:settle" +msgstr "Settle Style" + +msgid "aihelp:settle" +msgstr "" +"Plays moves that lead to clarity on stones' status, favouring solid moves " +"rather than complex sabaki. Of moves that lose at most 'max_points_lost' " +"points and with at least 'min_visits', prefer moves that lose fewer points, " +"settle own stones more (with importance settled_weight), settle opponent's " +"stones more (importance settled_weight * opponent fac), avoid tenuki (more " +"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance" +" attachment_penalty). Weights can be negative to make it complicate the game" +" instead, and wide root noise (in general settings) can be used to further " +"play according to the style at the cost of strength. " + msgid "ai:jigo" msgstr "KataJigo" diff --git a/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo index eebeda8..2c8731b 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 d7f32a2..08ce3dc 100644 --- a/katrain/i18n/locales/es/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/es/LC_MESSAGES/katrain.po @@ -709,3 +709,28 @@ msgstr "Think for at least {num} seconds before playing." #. TODO - in minutes msgid "main time" msgstr "Main time (min.)" + +#. TODO +msgid "aihelp:simple" +msgstr "Plays moves that lead to simplifying the board state." + +#. TODO +msgid "ai:simple" +msgstr "Simple Style" + +#. TODO +msgid "aihelp:settle" +msgstr "" +"Plays moves that lead to clarity on stones' status, favouring solid moves " +"rather than complex sabaki. Of moves that lose at most 'max_points_lost' " +"points and with at least 'min_visits', prefer moves that lose fewer points, " +"settle own stones more (with importance settled_weight), settle opponent's " +"stones more (importance settled_weight * opponent fac), avoid tenuki (more " +"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance" +" attachment_penalty). Weights can be negative to make it complicate the game" +" instead, and wide root noise (in general settings) can be used to further " +"play according to the style at the cost of strength. " + +#. TODO +msgid "ai:settle" +msgstr "Settle Style" diff --git a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo index 09a9c96..a4e195f 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 28f1c6c..d535de0 100644 --- a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po @@ -733,3 +733,37 @@ msgstr "Think for at least {num} seconds before playing." #. TODO - in minutes msgid "main time" msgstr "Main time (min.)" + +#. TODO +msgid "aihelp:simple" +msgstr "" +"Plays moves that lead to simplifying the board state. Of moves that lose at " +"most 'max_points_lost' points and with at least 'min_visits', prefer moves " +"that lose fewer points, settle own territory more (with importance " +"settled_weight), settle opponent's territory more (importance settled_weight" +" * opponent fac), avoid tenuki (more than 5 spaces, importance " +"tenuki_penalty), and avoid attachments (importance attachment_penalty). " +"Weights can be negative to make it complicate the game instead, and wide " +"root noise (in general settings) can be used to further play according to " +"the style at the cost of strength. " + +#. TODO +msgid "ai:simple" +msgstr "Simple Style" + +#. TODO +msgid "aihelp:settle" +msgstr "" +"Plays moves that lead to clarity on stones' status, favouring solid moves " +"rather than complex sabaki. Of moves that lose at most 'max_points_lost' " +"points and with at least 'min_visits', prefer moves that lose fewer points, " +"settle own stones more (with importance settled_weight), settle opponent's " +"stones more (importance settled_weight * opponent fac), avoid tenuki (more " +"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance" +" attachment_penalty). Weights can be negative to make it complicate the game" +" instead, and wide root noise (in general settings) can be used to further " +"play according to the style at the cost of strength. " + +#. TODO +msgid "ai:settle" +msgstr "Settle Stones Style" diff --git a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo index 206ae2d..e1bad74 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 b4b9b4f..7901826 100644 --- a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po @@ -680,14 +680,37 @@ msgstr "" msgid "alternative analysis" msgstr "他の候補手を分析中" -#. TODO - ignore moves if timer is on and less than this is used +#. ignore moves if timer is on and less than this is used msgid "minimal time use" -msgstr "Minimal time use in byo-yomi (s)" +msgstr "早打ち禁止秒数" -#. TODO msgid "move too fast" -msgstr "Think for at least {num} seconds before playing." +msgstr "早打ちすぎです。{num}秒は考えてください。" -#. TODO - in minutes +#. in minutes msgid "main time" -msgstr "Main time (min.)" +msgstr "持ち時間(分)" + +msgid "aihelp:simple" +msgstr "" +"盤面の単純化をめざして, 次の基準で手を選びます (括弧内は重視度): 損する目数が少ない, 自分の確定地が多い (settled_weight), " +"相手も確定地が多い (settled_weight × opponent fac), 相手が打った手の近辺…縦横とも3間以内 " +"(tenuki_penalty), 相手の石に接触しない (attachment_penalty). ただし, 損が大きすぎたり " +"(max_points_lost目以上) 探索手数が少なすぎたり (min_visits手未満) する候補手は除きます. 重視度を負にすれば, " +"逆に盤面の複雑化をめざします. また, 一般設定のwide root noiseを大きくすれば, 強さは犠牲になりますが, " +"より前述の基準に忠実な打ち方をします." + +msgid "ai:simple" +msgstr "簡明派" + +msgid "aihelp:settle" +msgstr "" +"複雑なサバキよりも, 石の生死をはっきりさせる堅実な手を好みます. 具体的には, 次の基準で手を選びます (括弧内は重視度): 損する目数が少ない, " +"自分の生死が明確 (settled_weight), 相手の生死も明確 (settled_weight × opponent fac), " +"相手が打った手の近辺…縦横とも3間以内 (tenuki_penalty), 相手の石に接触しない (attachment_penalty). ただし, " +"損が大きすぎたり (max_points_lost目以上) 探索手数が少なすぎたり (min_visits手未満) する候補手は除きます. " +"重視度を負にすれば, 逆に盤面の複雑化をめざします. また, 一般設定のwide root noiseを大きくすれば, 強さは犠牲になりますが, " +"より前述の基準に忠実な打ち方をします." + +msgid "ai:settle" +msgstr "堅実派" diff --git a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo index 6739fb1..206bb8e 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 0e89d0b..ce14c9f 100644 --- a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po @@ -666,3 +666,34 @@ msgstr "Think for at least {num} seconds before playing." #. TODO - in minutes msgid "main time" msgstr "Main time (min.)" + +#. TODO +msgid "aihelp:simple" +msgstr "" +"Plays moves that lead to simplifying the board state. Of moves that lose at " +"most 'max_points_lost' points and with at least 'min_visits', prefer moves " +"that lose fewer points, settle own territory more (with importance " +"settled_weight), settle opponent's territory more (importance settled_weight" +" * opponent fac), and avoid attachments (importance attachment_penalty). " +"Weights can be negative to make it complicate the game instead. " + +#. TODO +msgid "ai:simple" +msgstr "Simple Style" + +#. TODO +msgid "aihelp:settle" +msgstr "" +"Plays moves that lead to clarity on stones' status, favouring solid moves " +"rather than complex sabaki. Of moves that lose at most 'max_points_lost' " +"points and with at least 'min_visits', prefer moves that lose fewer points, " +"settle own stones more (with importance settled_weight), settle opponent's " +"stones more (importance settled_weight * opponent fac), avoid tenuki (more " +"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance" +" attachment_penalty). Weights can be negative to make it complicate the game" +" instead, and wide root noise (in general settings) can be used to further " +"play according to the style at the cost of strength. " + +#. TODO +msgid "ai:settle" +msgstr "Settle Style" diff --git a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo index b62d3c0..9750b01 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 cc87c03..e0b1be1 100644 --- a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po @@ -696,3 +696,37 @@ msgstr "Think for at least {num} seconds before playing." #. TODO - in minutes msgid "main time" msgstr "Main time (min.)" + +#. TODO +msgid "aihelp:simple" +msgstr "" +"Plays moves that lead to simplifying the board state. Of moves that lose at " +"most 'max_points_lost' points and with at least 'min_visits', prefer moves " +"that lose fewer points, settle own territory more (with importance " +"settled_weight), settle opponent's territory more (importance settled_weight" +" * opponent fac), avoid tenuki (more than 5 spaces, importance " +"tenuki_penalty), and avoid attachments (importance attachment_penalty). " +"Weights can be negative to make it complicate the game instead, and wide " +"root noise (in general settings) can be used to further play according to " +"the style at the cost of strength. " + +#. TODO +msgid "ai:simple" +msgstr "Simple Style" + +#. TODO +msgid "aihelp:settle" +msgstr "" +"Plays moves that lead to clarity on stones' status, favouring solid moves " +"rather than complex sabaki. Of moves that lose at most 'max_points_lost' " +"points and with at least 'min_visits', prefer moves that lose fewer points, " +"settle own stones more (with importance settled_weight), settle opponent's " +"stones more (importance settled_weight * opponent fac), avoid tenuki (more " +"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance" +" attachment_penalty). Weights can be negative to make it complicate the game" +" instead, and wide root noise (in general settings) can be used to further " +"play according to the style at the cost of strength. " + +#. TODO +msgid "ai:settle" +msgstr "Settle Style" diff --git a/katrain/popups.kv b/katrain/popups.kv index 83c44b4..ad51904 100644 --- a/katrain/popups.kv +++ b/katrain/popups.kv @@ -469,7 +469,7 @@ textbox: textbox SelectionSlider: id: slider - size_hint: 2,1 + size_hint: 2.25,1 values: root.values on_change: textbox.text = str(slider.value) track_color: LIGHTGREY