diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e23f350..4989a61 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,7 +1,7 @@ # These are supported funding model platforms github: sanderland # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: katrainbaduk # Replace with a single Patreon username +patreon: # 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 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/CONTRIBUTIONS.md b/CONTRIBUTIONS.md index 9b314ca..8220031 100644 --- a/CONTRIBUTIONS.md +++ b/CONTRIBUTIONS.md @@ -52,6 +52,7 @@ Many thanks to these additional authors: * "fohristiwhirl" for the Gibo and NGF formats parsing code. * "kaorahi" for bug fixes and SGF parser improvements. * "ajkenny84" for the red-green colourblind theme. +* Lukasz Wierzbowski for the ability to paste urls for sgfs and helping fix alt-gr issues. ## Translators @@ -62,7 +63,7 @@ Many thanks to the following contributors for translations. * 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" +* Simplified Chinese: Qing Mu with contributions from "Medwin" and Viktor Lin * Japanese: "kaorahi" ## Additional thanks to diff --git a/INSTALL.md b/INSTALL.md index f5ba1d4..e2a52fd 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -53,7 +53,7 @@ Simply download and run, everything is included. ### Quick install guide -If you have a working Python 3.6/3.7 available, you should be able to simply: +If you have a working Python 3.6/3.7/3.8 available, you should be able to simply: * Run `pip3 install -U katrain` * Run the program by executing `katrain` in a terminal. diff --git a/LICENSE b/LICENSE index 0d8b8aa..c972dd9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,10 +1,11 @@ This repository includes: -1. Binaries for 'KataGo', which is Copyright 2019 David J Wu et al. +1. Binaries for 'KataGo', which is Copyright David J Wu et al. For on related licenses for these binaries and libraries see https://github.com/lightvector/KataGo 2. Icons from www.flaticon.com, used with permission with the following attributions: - Equalize icon and Thrash Icon: derived from work by bqlqn from www.flaticon.com +- Local search icon: derived from work by Pixel Perfect from www.flaticon.com - Other Menu icons and Flag icons: derived from work by Freepik from www.flaticon.com 3. The True Type Font DIGITAL-7 version 1.02 by Sizenko Alexander, which is free for non-commercial use. diff --git a/README.md b/README.md index 634cc85..ee5b446 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ This estimate should be reasonably accurate as long as you have not changed the * **Calibrated Rank Bot** was calibrated on various bots (e.g. GnuGo and Pachi at different strength settings) to play a balanced game from the opening to the endgame without making serious (DDK) blunders. Further discussion can be found [here](https://github.com/sanderland/katrain/issues/44) and [here](https://github.com/sanderland/katrain/issues/74). + * **Simple Style** Prefers moves that solidify both player's territory, leading to relatively simpler moves. * **ScoreLoss** is KataGo analyzing as usual, but choosing from potential moves depending on the expected score loss, leading to a varied style with mostly small mistakes. * **Policy** uses the top move from the policy network (it's 'shape sense' without reading). @@ -166,6 +167,7 @@ Keyboard shortcuts are shown with **[key]**. * **[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. + * **[g]**: Find local responses: Searches within 5 spaces of the last move only. * **[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. @@ -180,6 +182,8 @@ In addition to shortcuts mentioned above and those shown in the main menu: * **[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. +* **[n]** As in clicking the red arrow, go to one move before the next mistake (orange or worse) by a human player. +* **[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/i18n.py b/i18n.py index b1ca000..37f1dc0 100644 --- a/i18n.py +++ b/i18n.py @@ -26,7 +26,7 @@ for lang in locales: pofile[lang] = os.path.join(localedir, lang, "LC_MESSAGES", "katrain.po") po[lang] = polib.pofile(pofile[lang]) for entry in po[lang].translated_entries(): - if "TODO" in entry.comment: + if "TODO" in entry.comment and not "DEPRECATED" in entry.comment: todos[lang].append(entry) strings_to_langs[entry.msgid][lang] = entry strings_to_keys[entry.msgid][lang] = set(re.findall("{.*?}", entry.msgstr)) diff --git a/katrain/KataGo/analysis_config.cfg b/katrain/KataGo/analysis_config.cfg index 84a0130..3386251 100644 --- a/katrain/KataGo/analysis_config.cfg +++ b/katrain/KataGo/analysis_config.cfg @@ -109,6 +109,7 @@ nnMaxBatchSize = 96 # Other General GPU Settings------------------------------------------------------------------------------- + # Cache up to 2 ** this many neural net evaluations in case of transpositions in the tree. nnCacheSizePowerOfTwo = 20 # Size of mutex pool for nnCache is 2 ** this @@ -116,7 +117,6 @@ nnMutexPoolSizePowerOfTwo = 16 # Randomize board orientation when running neural net evals? nnRandomize = true - # TO USE MULTIPLE GPUS: # Set this to the number of GPUs you have and/or would like to use... # AND if it is more than 1, uncomment the appropriate CUDA or OpenCL section below. diff --git a/katrain/KataGo/boost_filesystem-vc142-mt-x64-1_72.dll b/katrain/KataGo/boost_filesystem-vc142-mt-x64-1_72.dll deleted file mode 100644 index 2d6950d..0000000 Binary files a/katrain/KataGo/boost_filesystem-vc142-mt-x64-1_72.dll and /dev/null differ 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..f65582a 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 502859e..e98a94a 100644 --- a/katrain/__main__.py +++ b/katrain/__main__.py @@ -30,11 +30,13 @@ Config.set("graphics", "width", max(400, int(WINDOW_X * WINDOW_SCALE_FAC))) Config.set("graphics", "height", max(400, int(WINDOW_Y * WINDOW_SCALE_FAC))) Config.set("input", "mouse", "mouse,multitouch_on_demand") +import re import signal import sys import threading import traceback from queue import Queue +import urllib3 import webbrowser from kivy.base import ExceptionHandler, ExceptionManager @@ -130,9 +132,9 @@ class KaTrainGui(Screen, KaTrainBase): return self.play_mode.mode def toggle_continuous_analysis(self): - self.idle_analysis = not self.idle_analysis - if not self.idle_analysis: + if self.idle_analysis: self.controls.set_status("", STATUS_INFO) + self.idle_analysis = not self.idle_analysis self.update_state() def start(self): @@ -157,7 +159,6 @@ class KaTrainGui(Screen, KaTrainBase): self.board_controls.mid_circles_container.clear_widgets() self.board_controls.mid_circles_container.add_widget(bot) self.board_controls.mid_circles_container.add_widget(top) - self.board_controls.branch.disabled = not cn.parent or len(cn.parent.children) <= 1 self.controls.players["W"].captures = prisoners["W"] self.controls.players["B"].captures = prisoners["B"] @@ -168,6 +169,7 @@ class KaTrainGui(Screen, KaTrainBase): self.board_controls.engine_status_col = ENGINE_READY_COL else: self.board_controls.engine_status_col = ENGINE_BUSY_COL + self.board_controls.queries_remaining = len(self.engine.queries) # redraw board/stones if redraw_board: @@ -193,21 +195,21 @@ 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 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) - Clock.schedule_once(self.board_gui.play_stone_sound, 0) + Clock.schedule_once(self.board_gui.play_stone_sound, 0.25) if len(self.engine.queries) == 0 and self.idle_analysis: self("analyze-extra", "extra", continuous=True) Clock.schedule_once(lambda _dt: self.update_gui(cn, redraw_board=redraw_board), -1) # trigger? @@ -218,7 +220,7 @@ class KaTrainGui(Screen, KaTrainBase): self.controls.update_players() self.update_state() for player_setup_block in PlayerSetupBlock.INSTANCES: - player_setup_block.update_players(bw, self.players_info[bw]) + player_setup_block.update_player_info(bw, self.players_info[bw]) def set_note(self, note): self.game.current_node.note = note @@ -293,6 +295,10 @@ class KaTrainGui(Screen, KaTrainBase): self.board_gui.animating_pv = None self.game.redo(n_times) + def _do_next_mistake(self): + self.board_gui.animating_pv = None + self.game.redo(999, stop_on_mistake=self.config("trainer/eval_thresholds")[-4]) + def _do_cycle_children(self, *args): self.board_gui.animating_pv = None self.game.cycle_children(*args) @@ -324,7 +330,7 @@ class KaTrainGui(Screen, KaTrainBase): self.controls.timer.paused = True if not self.timer_settings_popup: self.timer_settings_popup = I18NPopup( - title_key="timer settings", size=[dp(350), dp(400)], content=ConfigTimerPopup(self) + title_key="timer settings", size=[dp(600), dp(500)], content=ConfigTimerPopup(self) ).__self__ self.timer_settings_popup.content.popup = self.timer_settings_popup self.timer_settings_popup.open() @@ -351,12 +357,12 @@ 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() - def load_sgf_file(self, file, fast=False, rewind=False): + def load_sgf_file(self, file, fast=False, rewind=True): try: move_tree = KaTrainSGF.parse_file(file) except ParseError as e: @@ -388,6 +394,7 @@ class KaTrainGui(Screen, KaTrainBase): popup_contents.filesel.on_success = readfile popup_contents.filesel.on_submit = readfile self.fileselect_popup.open() + self.fileselect_popup.content.filesel.ids.list_view._trigger_update() def _do_output_sgf(self): msg = self.game.write_sgf(self.config("general/sgf_save")) @@ -399,6 +406,14 @@ class KaTrainGui(Screen, KaTrainBase): if not clipboard: self.controls.set_status(f"Ctrl-V pressed but clipboard is empty.", STATUS_INFO) return + + url_match = re.match(r"(?Phttps?://[^\s]+)", clipboard) + if url_match: + self.log("Recognized url: " + url_match.group(), OUTPUT_INFO) + http = urllib3.PoolManager() + response = http.request("GET", url_match.group()) + clipboard = response.data.decode("utf-8") + try: move_tree = KaTrainSGF.parse_sgf(clipboard) except Exception as exc: @@ -440,7 +455,9 @@ class KaTrainGui(Screen, KaTrainBase): "s": ("analyze-extra", "equalize"), "d": ("analyze-extra", "sweep"), "f": ("analyze-extra", "alternative"), + "g": ("analyze-extra", "local"), "p": ("play", None), + "n": ("next-mistake",), "down": ("switch-branch", 1), "up": ("switch-branch", -1), "f5": ("timer-popup",), @@ -466,7 +483,9 @@ class KaTrainGui(Screen, KaTrainBase): return else: return - + ctrl_pressed = "ctrl" in modifiers + alt_pressed = "alt" in modifiers + shift_pressed = "shift" in modifiers shortcuts = self.shortcuts if keycode[1] == "tab": self.play_mode.switch_ui_mode() @@ -474,26 +493,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 or shift_pressed) * 9 + (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 or shift_pressed) * 9 + (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 a8e5203..1a85486 100644 --- a/katrain/config.json +++ b/katrain/config.json @@ -17,11 +17,13 @@ "anim_pv_time": 0.5, "debug_level": 0, "lang": "en", - "version": "1.5.0" + "version": "1.6.0" }, "timer": { "byo_length": 30, "byo_periods": 5, + "minimal_use": 0, + "main_time": 0, "sound": true }, "game": { @@ -86,6 +88,22 @@ "ai:policy": { "opening_moves": 22.0 }, + "ai:simple": { + "max_points_lost": 1.75, + "settled_weight": 1.0, + "opponent_fac": 0.5, + "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, "pick_override": 1.0, diff --git a/katrain/core/ai.py b/katrain/core/ai.py index e39923a..b15a1d6 100644 --- a/katrain/core/ai.py +++ b/katrain/core/ai.py @@ -8,29 +8,31 @@ from katrain.core.constants import ( AI_DEFAULT, AI_HANDICAP, AI_INFLUENCE, + AI_INFLUENCE_ELO_GRID, AI_JIGO, AI_LOCAL, + AI_LOCAL_ELO_GRID, AI_PICK, + AI_PICK_ELO_GRID, AI_POLICY, AI_RANK, AI_SCORELOSS, + AI_SCORELOSS_ELO, + AI_SIMPLE_OWNERSHIP, AI_STRATEGIES_PICK, AI_STRATEGIES_POLICY, AI_STRENGTH, AI_TENUKI, + AI_TENUKI_ELO_GRID, AI_TERRITORY, + AI_TERRITORY_ELO_GRID, AI_WEIGHTED, + AI_WEIGHTED_ELO, + CALIBRATED_RANK_ELO, OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_INFO, - AI_WEIGHTED_ELO, - AI_SCORELOSS_ELO, - CALIBRATED_RANK_ELO, - AI_LOCAL_ELO_GRID, - AI_TENUKI_ELO_GRID, - AI_TERRITORY_ELO_GRID, - AI_INFLUENCE_ELO_GRID, - AI_PICK_ELO_GRID, + AI_SETTLE_STONES, ) from katrain.core.game import Game, GameNode, Move from katrain.core.utils import var_to_grid @@ -162,13 +164,14 @@ 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 - game.katrain.log("Error in PDA-based analysis", a) + game.katrain.log(f"Error in additional analysis query: {a}") error = True engine = game.engines[cn.player] @@ -203,7 +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 - 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) @@ -328,7 +331,10 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, candidate_ai_moves = handicap_analysis["moveInfos"] top_cand = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player) - if top_cand.is_pass and ai_mode not in [AI_DEFAULT, AI_HANDICAP]: # don't play suicidal to balance score + if top_cand.is_pass and ai_mode not in [ + AI_DEFAULT, + AI_HANDICAP, + ]: # don't play suicidal to balance score aimove = top_cand ai_thoughts += f"Top move is pass, so passing regardless of strategy. " else: @@ -352,6 +358,80 @@ 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 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, + 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"] <= 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[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{', attachment' if isattach else ''}{', tenuki' if istenuki else ''})" + for move, settled, oppsettled, isattach, istenuki, d in moves_with_settledness[:5] + ] + ai_thoughts += f"{ai_mode} strategy. Top 5 Candidates {', '.join(cands)} " + aimove = moves_with_settledness[0][0] + else: + 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]: game.katrain.log(f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO) diff --git a/katrain/core/base_katrain.py b/katrain/core/base_katrain.py index e8604a8..819a30c 100644 --- a/katrain/core/base_katrain.py +++ b/katrain/core/base_katrain.py @@ -49,13 +49,13 @@ class KaTrainBase: """Settings, logging, and players functionality, so other classes like bots who need a katrain instance can be used without a GUI""" - def __init__(self, force_package_config=False, debug_level=0, **kwargs): - self.debug_level = debug_level + def __init__(self, force_package_config=False, debug_level=None, **kwargs): + self.debug_level = debug_level or 0 self.game = None self.logger = lambda message, level=OUTPUT_INFO: self.log(message, level) self.config_file = self._load_config(force_package_config=force_package_config) - self.debug_level = debug_level or self.config("general/debug_level", OUTPUT_INFO) + self.debug_level = self.config("general/debug_level", OUTPUT_INFO) if debug_level is None else debug_level Config.set("kivy", "log_level", "warning") if self.debug_level >= OUTPUT_DEBUG: diff --git a/katrain/core/constants.py b/katrain/core/constants.py index 2fc1fb6..b40cbd6 100644 --- a/katrain/core/constants.py +++ b/katrain/core/constants.py @@ -1,6 +1,6 @@ -VERSION = "1.5.0" +VERSION = "1.6.0" HOMEPAGE = "https://github.com/sanderland/katrain" -CONFIG_MIN_VERSION = "1.4.0" # 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,10 +33,12 @@ AI_TENUKI = "ai:p:tenuki" AI_INFLUENCE = "ai:p:influence" AI_TERRITORY = "ai:p:territory" AI_RANK = "ai:p:rank" +AI_SIMPLE_OWNERSHIP = "ai:simple" +AI_SETTLE_STONES = "ai:settle" AI_CONFIG_DEFAULT = AI_RANK -AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_HANDICAP, AI_SCORELOSS, AI_JIGO] +AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_HANDICAP, AI_SCORELOSS, AI_SIMPLE_OWNERSHIP, AI_JIGO] # AI_SETTLE_STONES 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 @@ -44,6 +46,8 @@ AI_STRATEGIES_RECOMMENDED_ORDER = [ AI_DEFAULT, AI_RANK, AI_HANDICAP, + AI_SIMPLE_OWNERSHIP, + # AI_SETTLE_STONES, AI_SCORELOSS, AI_POLICY, AI_WEIGHTED, @@ -67,6 +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: 2, + AI_SETTLE_STONES: 2, } AI_OPTION_VALUES = { @@ -85,8 +91,23 @@ AI_OPTION_VALUES = { "threshold": [2, 2.5, 3, 3.5, 4, 4.5], "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(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 a975924..914ad34 100644 --- a/katrain/core/engine.py +++ b/katrain/core/engine.py @@ -13,6 +13,7 @@ from kivy.utils import platform from katrain.core.constants import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_EXTRA_DEBUG, OUTPUT_KATAGO_STDERR from katrain.core.game_node import GameNode from katrain.core.lang import i18n +from katrain.core.sgf_parser import Move from katrain.core.utils import find_package_resource @@ -85,10 +86,10 @@ class KataGoEngine: def start(self): try: self.katrain.log(f"Starting KataGo with {self.command}", OUTPUT_DEBUG) - startupinfo = None # stop command box popups on windows/pyinstaller + startupinfo = None if hasattr(subprocess, "STARTUPINFO"): startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # stop command box popups on win/pyinstaller self.katago_process = subprocess.Popen( self.command, startupinfo=startupinfo, @@ -107,6 +108,8 @@ class KataGoEngine: def on_new_game(self): self.base_priority += 1 + for query_id in list(self.queries.keys()): + self.terminate_query(query_id) self.queries = {} def restart(self): @@ -131,11 +134,14 @@ class KataGoEngine: raise EngineDiedException(died_msg) return ok + def wait_to_finish(self): + while self.queries and self.katago_process and self.katago_process.poll() is None: + time.sleep(0.1) + def shutdown(self, finish=False): process = self.katago_process if finish and process: - while self.queries and process.poll() is None: - time.sleep(0.1) + self.wait_to_finish() if process: self.katago_process = None process.terminate() @@ -196,14 +202,18 @@ class KataGoEngine: elif "warning" in analysis: self.katrain.log(f"{analysis} received from KataGo", OUTPUT_DEBUG) else: - del self.queries[query_id] + 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: - callback(analysis) + if callback and not analysis.get("noResults", False): + callback(analysis, partial_result) except Exception as e: self.katrain.log(f"Error in engine callback for query {query_id}: {e}", OUTPUT_ERROR) if getattr(self.katrain, "update_state", None): # easier mocking etc @@ -225,7 +235,10 @@ class KataGoEngine: self.katago_process.stdin.flush() except OSError as e: self.check_alive(os_error=str(e), exception_if_dead=True) - return # do not raise, since there's nothing to catch it + + def terminate_query(self, query_id): + if query_id is not None: + self.send_query({"action": "terminate", "terminateId": query_id}, None, None) def request_analysis( self, @@ -236,10 +249,12 @@ class KataGoEngine: analyze_fast: bool = False, time_limit=True, find_alternatives: bool = False, + find_local: bool = False, priority: int = 0, ownership: Optional[bool] = None, next_move: Optional[GameNode] = None, extra_settings: Optional[Dict] = None, + report_every: Optional[float] = None, ): nodes = analysis_node.nodes_from_root moves = [m for node in nodes for m in node.moves] @@ -253,6 +268,8 @@ class KataGoEngine: if analyze_fast and self.config.get("fast_visits"): visits = self.config["fast_visits"] + size_x, size_y = analysis_node.board_size + if find_alternatives: avoid = [ { @@ -261,10 +278,26 @@ class KataGoEngine: "untilDepth": 1, } ] + elif find_local: + distance = 5 + last_move = analysis_node.move + if last_move is None or last_move.is_pass: + return + avoid = [ + { + "moves": [ + Move((x, y)).gtp() + for x in range(0, size_x) + for y in range(0, size_y) + if max(abs(x - last_move.coords[0]), abs(y - last_move.coords[1])) > distance + ], + "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: settings["maxTime"] = self.config["max_time"] @@ -277,15 +310,19 @@ 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, + "includeMovesOwnership": 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 {})}, } + if report_every is not None: + query["reportDuringSearchEvery"] = report_every + if avoid: + query["avoidMoves"] = avoid 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 ee4077d..71f72f5 100644 --- a/katrain/core/game.py +++ b/katrain/core/game.py @@ -79,12 +79,14 @@ class Game: self.root.set_property("RU", katrain.config("game/rules")) self.set_current_node(self.root) + self.main_time_used = 0 threading.Thread( target=lambda: self.analyze_all_nodes(-1_000_000, analyze_fast=analyze_fast), daemon=True ).start() # return faster, but bypass Kivy Clock def analyze_all_nodes(self, priority=0, analyze_fast=False): for node in self.root.nodes_in_tree: + node.clear_analysis() node.analyze(self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast) # -- move tree functions -- @@ -183,12 +185,22 @@ class Game: cn = cn.parent self.set_current_node(cn) - def redo(self, n_times=1): + def redo(self, n_times=1, stop_on_mistake=None): cn = self.current_node # avoid race conditions - for _ in range(n_times): + for move in range(n_times): if cn.children: cn = cn.ordered_children[0] - self.set_current_node(cn) + if ( + move > 0 + and stop_on_mistake is not None + and cn.points_lost is not None + and cn.points_lost >= stop_on_mistake + and self.katrain.players_info[cn.player].player_type != PLAYER_AI + ): + self.set_current_node(cn.parent) + return + if stop_on_mistake is None: + self.set_current_node(cn) def cycle_children(self, direction): cn = self.current_node # avoid race conditions @@ -327,6 +339,7 @@ class Game: ) else: visits = cn.analysis_visits_requested + engine.config["max_visits"] + self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS) self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS) cn.analyze(engine, visits=visits, priority=-1_000, time_limit=False) return @@ -338,13 +351,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,32 +382,35 @@ 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 in ["equalize", "alternative"]: - if not cn.analysis_ready: + elif mode == "local": # also do a quick update on current candidates so it doesn't look too weird + self.katrain.controls.set_status(i18n._("local analysis"), STATUS_ANALYSIS) + cn.analyze(engine, priority=-500, time_limit=False, find_local="local") + return + elif mode in ["equalize", "alternative", "local"]: + if not cn.analysis_complete and mode != "local": 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()] - 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) + cn.analyze(engine, priority=-500, time_limit=False, find_alternatives="alternative") visits = engine.config["fast_visits"] - else: + else: # equalize 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 + analyze_moves = [Move.from_gtp(gtp, player=cn.next_player) for gtp, _ in cn.analysis["moves"].items()] else: raise ValueError("Invalid analysis mode") 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=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 0a73a3b..de7bc60 100644 --- a/katrain/core/game_node.py +++ b/katrain/core/game_node.py @@ -13,7 +13,6 @@ 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.ownership = None self.policy = None self.auto_undo = None # None = not analyzed. False: not undone (good move). True: undone (bad move) @@ -21,9 +20,13 @@ class GameNode(SGFNode): self.note = "" self.move_number = 0 self.time_used = 0 - self.analysis_visits_requested = 0 self.undo_threshold = random.random() # for fractional undos self.end_state = None + self.clear_analysis() + + def clear_analysis(self): + self.analysis_visits_requested = 0 + self.analysis = {"moves": {}, "root": None, "completed": False} def sgf_properties(self, save_comments_player=None, save_comments_class=None, eval_thresholds=None): properties = copy.copy(super().sgf_properties()) @@ -34,8 +37,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,16 +80,22 @@ class GameNode(SGFNode): refine_move=None, analyze_fast=False, find_alternatives=False, + find_local=False, + report_every=0.25, ): engine.request_analysis( self, - lambda result: self.set_analysis(result, refine_move, find_alternatives), + callback=lambda result, partial_result: self.set_analysis( + result, refine_move, find_alternatives or find_local, partial_result + ), priority=priority, visits=visits, analyze_fast=analyze_fast, time_limit=time_limit, next_move=refine_move, find_alternatives=find_alternatives, + find_local=find_local, + report_every=report_every, ) def update_move_analysis(self, move_analysis, move_gtp): @@ -102,24 +111,26 @@ 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], additional_moves: bool, partial_result: bool = False + ): 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: + if additional_moves: for m in analysis_json["moveInfos"]: - m["order"] += 10 # offset for not making this top - if refine_move is None and not alternatives_mode: + del m["order"] # avoid changing order + if refine_move is None and not additional_moves: 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") - if not alternatives_mode: + if not additional_moves: self.analysis["root"] = analysis_json["rootInfo"] if self.parent and self.move: analysis_json["rootInfo"]["pv"] = [self.move.gtp()] + ( @@ -128,14 +139,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 additional_moves + 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): @@ -145,7 +162,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): @@ -176,12 +193,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(): @@ -202,20 +219,21 @@ class GameNode(SGFNode): if currmove_pol_rank is not None: policy_rank_msg = i18n._("Info:policy rank") text += policy_rank_msg.format(rank=currmove_pol_rank, probability=currmove_pol_prob) + "\n" - if currmove_pol_rank is None or currmove_pol_rank != 1 and (sgf or details): + if currmove_pol_rank != 1 and policy_ranking and (sgf or details): policy_best_msg = i18n._("Info:policy best") pol_move, pol_prob = policy_ranking[0][1].gtp(), policy_ranking[0][0] 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"]) @@ -224,7 +242,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) @@ -236,8 +254,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 @@ -249,7 +267,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.kv b/katrain/gui.kv index 07219f9..9e876d5 100644 --- a/katrain/gui.kv +++ b/katrain/gui.kv @@ -32,7 +32,7 @@ #:set CP_SPACING dp(6) #:set CP_SMALL_SPACING dp(3) #:set CP_PADDING dp(6) -#:set NOTES_FONT_SIZE dp(18) +#:set NOTES_FONT_SIZE sp(18) #:set EPSILON 1e-9 @@ -126,7 +126,7 @@ pos: [root.pos[i] + (root.size[i] - root.icon_size[i])/2 for i in [0,1]] source: root.icon mipmap: True - color: [1,1,1,0.4] if root.disabled else [1,1,1,1] + color: [1,1,1,0.4] if root.disabled else root.color : @@ -171,8 +171,6 @@ size: image.size font_size: root.min_size * 1.2 / (1 + 1 * len(self.text)) - - # UI components : @@ -187,7 +185,6 @@ : katrain: app.gui - branch: branch circles: [black_circle,white_circle] mid_circles_container: mid_circles_container pass_btn: pass_btn @@ -209,7 +206,7 @@ on_press: root.katrain('undo',999) NavIconButton: icon: 'img/Previous-5.png' - on_press: root.katrain('undo',5) + on_press: root.katrain('undo',10) NavIconButton: icon: 'img/Previous.png' on_press: root.katrain('undo') @@ -233,20 +230,19 @@ on_press: root.katrain('redo') NavIconButton: icon: 'img/Next-5.png' - on_press: root.katrain('redo',5) + on_press: root.katrain('redo',10) NavIconButton: icon: 'img/Next-End.png' on_press: root.katrain('redo',999) NavIconButton: - id: branch - icon: 'img/Branch.png' - on_press: root.katrain('cycle-children',1) - disabled: True + icon: 'img/Next-Mistake.png' + color: [0.65,0.05,0.05,1] + on_press: root.katrain('next-mistake') ClickableLabel: pos_hint: {'center_x': 0.85,'center_y':0.5} size_hint: None, 1 width: self.height - canvas: + canvas.before: Color: rgba: YELLOW Ellipse: @@ -264,6 +260,9 @@ Line: circle: self.pos[0] + self.width/2, self.pos[1] + self.height/2, self.width/6 width: 1 + text: str(root.queries_remaining) if root.queries_remaining >= 3 else '' + color: BLACK + font_size: 0.5 * self.width / max(2,len(self.text)) on_press: app.gui.controls.set_status(i18n._('ai-ponder'),STATUS_INFO) @@ -357,14 +356,14 @@ spacing: CP_SPACING padding: CP_PADDING * 4, CP_PADDING * 2 paused: True - timeout: self.state[1] <= 0 + timeout: root.state[1] is not None and root.state[1] <= 0 MDBoxLayout: size_hint: 0.7,1 MDFloatLayout: size_hint: 0.4,1 TimerLabel: color: BACKGROUND_COLOR - text: '8:88' + text: '{}:88'.format('188' if root.state[0] >= 6000 else '88') font_size: 0.85*self.height pos_hint: {'right':1,'center_y':0.5} halign: 'right' @@ -372,27 +371,27 @@ TimerLabel: id: timer timeout: root.timeout - text: "{:01d}:{:02d}".format(int(root.state[0]+0.99)//60,int((root.state[0]+0.99) % 60) ) + text: "{:2d}:{:02d}".format(int(root.state[0]+0.99)//60,int((root.state[0]+0.99) % 60) ) font_size: 0.85*self.height pos_hint: {'right':1,'center_y':0.5} halign: 'right' valign: 'middle' TimerLabel: size_hint: 0.05,1 - text: "x" + text: "x" if root.state[1] is not None else "" font_size: self.height * 0.3 text_size: self.width, self.height * 0.95 halign: 'right' valign: 'top' timeout: root.timeout TimerLabel: - size_hint: 0.1,1 + size_hint: 0.075,1 font_size: self.height * 0.35 text_size: self.size halign: 'left' valign: 'top' id: periods - text: str(root.state[1]) + text: str(root.state[1]) if root.state[1] is not None else "" max_lines: 1 timeout: root.timeout MDBoxLayout: @@ -507,8 +506,6 @@ katrain: app.gui orientation: 'vertical' players: {'B':B_player,'W':W_player} - engine_status_col: [1,1,1,1] - engine_status_pondering: -1 timer:timer graph: graph stats: stats @@ -685,6 +682,7 @@ color: LIGHTGREY + : adaptive_width: True spacing: CP_SPACING @@ -700,15 +698,18 @@ 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: + on_left_press: checkbox.trigger_action() + TriStateMDCheckbox: id: checkbox disabled: root.disabled + tri_state: root.tri_state size_hint: None,0.5 pos_hint: {'center_y': 0.5} width: self.height active: root.default_active on_active: if app.gui: app.gui.update_state() + on_slashed: if app.gui: app.gui.update_state() + : on_close: app.gui.analysis_controls.close_dropdown() @@ -749,6 +750,11 @@ icon: 'img/Alternative.png' shortcut: 'f' on_action: root.katrain("analyze-extra", "alternative") + AnalysisMenuItem: + text: i18n._("analysis:local") + icon: 'img/local.png' + shortcut: 'g' + on_action: root.katrain("analyze-extra", "local") AnalysisMenuItem: text: i18n._("analysis:game") icon: 'img/analysis.png' @@ -792,6 +798,7 @@ AnalysisToggle: id: eval text: i18n._("analysis:dots") + tri_state: True AnalysisToggle: id: hints text: i18n._("analysis:topmoves") @@ -853,11 +860,12 @@ player_subtype_ai: player_subtype_ai MDBoxLayout: size_hint: 0.25, 1 - CircleWithText: + ClickableCircle: size_hint: 0.8, 0.8 pos_hint: {'center_x':0.5,'center_y':0.5} text: root.player[0] player: root.player + on_left_press: root.parent.swap_players() MDBoxLayout: size_hint: 0.5, 1 orientation: 'vertical' @@ -1059,14 +1067,14 @@ AnalysisControls: id: analysis_controls size_hint_y: None - height: 1 if root.zen > 0 else max(dp(50),min(dp(75),self.width / 13.5)) + height: 1 if root.zen > 0 else max(sp(50),min(sp(75),self.width / 13.5)) opacity: 0 if root.zen > 0 else 1 BadukPanWidget: id: board_gui BadukPanControls: id: board_controls size_hint_y: None - height: 1 if root.zen > 0 else max(dp(50),min(dp(75),self.width / 13.5)) + height: 1 if root.zen > 0 else max(sp(50),min(sp(75),self.width / 13.5)) opacity: 0 if root.zen > 0 else 1 MDBoxLayout: size_hint_x: None diff --git a/katrain/gui/badukpan.py b/katrain/gui/badukpan.py index 611e706..0c921f2 100644 --- a/katrain/gui/badukpan.py +++ b/katrain/gui/badukpan.py @@ -42,7 +42,7 @@ class BadukPanWidget(Widget): Window.bind(mouse_pos=self.on_mouse_pos) self.redraw_board_contents_trigger = Clock.create_trigger(self.draw_board_contents) self.redraw_trigger = Clock.create_trigger(self.redraw) - self.bind(size=self.redraw_trigger) + self.bind(size=self.redraw_trigger, pos=self.redraw_trigger) Clock.schedule_interval(self.animate_pv, 0.1) # stone placement functions @@ -106,8 +106,23 @@ class BadukPanWidget(Widget): return katrain = self.katrain if self.ghost_stone and ("button" not in touch.profile or touch.button == "left"): - katrain("play", self.ghost_stone) - self.play_stone_sound() + game = self.katrain and self.katrain.game + current_node = game and self.katrain.game.current_node + if ( + current_node + and not current_node.children + and not self.katrain.next_player_info.ai + and not self.katrain.controls.timer.paused + and self.katrain.play_analyze_mode == MODE_PLAY + and self.katrain.config("timer/main_time", 0) * 60 - game.main_time_used <= 0 + and current_node.time_used < self.katrain.config("timer/minimal_use", 0) + ): + self.katrain.controls.set_status( + i18n._("move too fast").format(num=self.katrain.config("timer/minimal_use", 0)), STATUS_TEACHING + ) + else: + katrain("play", self.ghost_stone) + self.play_stone_sound() elif not self.ghost_stone: xd, xp = self._find_closest(touch.x, self.gridpos_x) yd, yp = self._find_closest(touch.y, self.gridpos_y) @@ -125,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 @@ -257,6 +272,7 @@ class BadukPanWidget(Widget): current_node = katrain.game.current_node game_ended = katrain.game.end_result full_eval_on = katrain.analysis_controls.eval.active + all_dots_off = katrain.analysis_controls.eval.checkbox.slashed has_stone = {} drawn_stone = {} for m in katrain.game.stones: @@ -280,7 +296,9 @@ class BadukPanWidget(Widget): placements = node.placements for m in node.moves + placements: if has_stone.get(m.coords) and not drawn_stone.get(m.coords): # skip captures, last only for - move_eval_on = show_dots_for.get(m.player) and (i < show_n_eval or full_eval_on) + move_eval_on = ( + not all_dots_off and show_dots_for.get(m.player) and (i < show_n_eval or full_eval_on) + ) if move_eval_on and points_lost is not None: evalcol = self.eval_color(points_lost, show_dots_for_class) else: @@ -454,7 +472,7 @@ class BadukPanWidget(Widget): source="img/topmove.png", ) if self.trainer_config["text_point_loss"] and text_on: - if move_dict["pointsLost"] < 0.05: + if -0.05 < move_dict["pointsLost"] < 0.05: ptloss_text = "0.0" else: ptloss_text = f"{-move_dict['pointsLost']:+.1f}" @@ -484,7 +502,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) ) @@ -619,3 +637,4 @@ class AnalysisControls(MDBoxLayout): class BadukPanControls(MDFloatLayout): engine_status_col = ListProperty(ENGINE_DOWN_COL) engine_status_pondering = NumericProperty(-1) + queries_remaining = NumericProperty(0) diff --git a/katrain/gui/controlspanel.py b/katrain/gui/controlspanel.py index 245c0bd..246af4c 100644 --- a/katrain/gui/controlspanel.py +++ b/katrain/gui/controlspanel.py @@ -71,6 +71,8 @@ class ControlsPanel(BoxLayout): self.active_comment_node = None self.last_timer_update = (None, 0, False) self.beep = SoundLoader.load("sounds/countdownbeep.wav") + self.boing = SoundLoader.load("sounds/boing.wav") + self.boing.volume = 0.1 self.beep_start = 5.2 self.timer_interval = 0.07 @@ -121,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 @@ -132,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 @@ -149,11 +151,13 @@ class ControlsPanel(BoxLayout): self.info.text = info def update_timer(self, _dt): - current_node = self.katrain and self.katrain.game and self.katrain.game.current_node + game = self.katrain and self.katrain.game + current_node = game and self.katrain.game.current_node if current_node: last_update_node, last_update_time, beeping = self.last_timer_update new_beeping = beeping now = time.time() + main_time = self.katrain.config("timer/main_time", 0) * 60 byo_len = max(1, self.katrain.config("timer/byo_length")) byo_num = max(1, self.katrain.config("timer/byo_periods")) sounds_on = self.katrain.config("timer/sound") @@ -161,9 +165,16 @@ class ControlsPanel(BoxLayout): ai = player.ai used_period = False + min_use = self.katrain.config("timer/minimal_use", 0) + boing_at_remaining = byo_len - min_use + main_time_remaining = main_time - game.main_time_used + if not self.timer.paused and not ai and self.katrain.play_analyze_mode == MODE_PLAY: if last_update_node == current_node and not current_node.children: - current_node.time_used += now - last_update_time + if main_time_remaining > 0: + game.main_time_used += now - last_update_time + else: + current_node.time_used += now - last_update_time else: current_node.time_used = 0 new_beeping = False @@ -173,6 +184,7 @@ class ControlsPanel(BoxLayout): time_remaining += byo_len player.periods_used += 1 used_period = True + if ( self.beep_start - 2 * self.timer_interval < time_remaining < self.beep_start and player.periods_used < byo_num @@ -180,6 +192,18 @@ class ControlsPanel(BoxLayout): new_beeping = True elif time_remaining > self.beep_start: new_beeping = False + + if ( + min_use + and not new_beeping + and self.boing + and boing_at_remaining - self.timer_interval + < time_remaining + < boing_at_remaining + self.timer_interval + and player.periods_used < byo_num + ): + self.boing.play() + else: new_beeping = False @@ -198,4 +222,7 @@ class ControlsPanel(BoxLayout): self.last_timer_update = (current_node, now, new_beeping) - self.timer.state = (max(0, time_remaining), max(0, periods_rem), ai) + if main_time_remaining > 0: + self.timer.state = (main_time_remaining, None, ai) + else: + self.timer.state = (max(0, time_remaining), max(0, periods_rem), ai) diff --git a/katrain/gui/kivyutils.py b/katrain/gui/kivyutils.py index a2b953b..703ac55 100644 --- a/katrain/gui/kivyutils.py +++ b/katrain/gui/kivyutils.py @@ -23,6 +23,7 @@ from kivymd.uix.behaviors import CircularRippleBehavior, RectangularRippleBehavi from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.button import BaseFlatButton, BasePressedButton from kivymd.uix.navigationdrawer import MDNavigationDrawer +from kivymd.uix.selectioncontrol import MDCheckbox from kivymd.uix.textfield import MDTextField from katrain.core.constants import AI_STRATEGIES_RECOMMENDED_ORDER, GAME_TYPES, MODE_PLAY, PLAYER_AI @@ -112,6 +113,7 @@ class AutoSizedRectangleToggleButton(ToggleButtonMixin, AutoSizedRectangleButton class TransparentIconButton(CircularRippleBehavior, Button): + color = ListProperty([1, 1, 1, 1]) icon_size = ListProperty([25, 25]) icon = StringProperty("") disabled = BooleanProperty(False) @@ -342,9 +344,12 @@ class PlayerSetupBlock(MDBoxLayout): def swap_players(self): player_dump = {bw: p.player_type_dump for bw, p in self.players.items()} for bw in "BW": - self.update_players(bw, player_dump["B" if bw == "W" else "W"]) + self.update_player_params(bw, player_dump["B" if bw == "W" else "W"]) - def update_players(self, bw, player_info): # update sub widget based on gui state change + def update_player_params(self, bw, params): + self.players[bw].update_widget(**params) + + def update_player_info(self, bw, player_info): # update sub widget based on gui state change self.players[bw].update_widget(player_type=player_info.player_type, player_subtype=player_info.player_subtype) @@ -368,14 +373,36 @@ class Timer(BGBoxLayout): timeout = BooleanProperty(False) +class TriStateMDCheckbox(MDCheckbox): + tri_state = BooleanProperty(False) + slashed = BooleanProperty(False) + + def _do_press(self): + if not self.tri_state: + return super()._do_press() + if self.slashed: + self.state = "normal" + self.slashed = False + self.icon = "checkbox-blank-outline" + elif self.state == "down": + self.state = "normal" + self.slashed = True + self.icon = "checkbox-blank-off-outline" + else: + self.state = "down" + self.slashed = False + self.icon = "checkbox-marked-outline" + + class AnalysisToggle(MDBoxLayout): text = StringProperty("") default_active = BooleanProperty(False) font_name = StringProperty(DEFAULT_FONT) disabled = BooleanProperty(False) + tri_state = BooleanProperty(False) def trigger_action(self, *args, **kwargs): - return self.checkbox.trigger_action(*args, **kwargs) + return self.checkbox._do_press() def activate(self, *_args): self.checkbox.active = True @@ -559,6 +586,10 @@ class ClickableLabel(LeftButtonBehavior, Label): pass +class ClickableCircle(LeftButtonBehavior, CircleWithText): + pass + + class ScrollableLabel(ScrollView, BackgroundMixin): __events__ = ["on_ref_press"] outline_color = ListProperty([0, 0, 0, 0]) # mixin not working for some reason diff --git a/katrain/gui/popups.py b/katrain/gui/popups.py index 43a108c..ec65052 100644 --- a/katrain/gui/popups.py +++ b/katrain/gui/popups.py @@ -238,6 +238,7 @@ class ConfigTimerPopup(QuickConfigGui): p.periods_used = 0 self.katrain.controls.timer.paused = True self.katrain.game.current_node.time_used = 0 + self.katrain.game.main_time_used = 0 self.katrain.update_state() @@ -245,7 +246,7 @@ class NewGamePopup(QuickConfigGui): def __init__(self, katrain): super().__init__(katrain) for bw, info in katrain.players_info.items(): - self.player_setup.update_players(bw, info) + self.player_setup.update_player_info(bw, info) self.rules_spinner.value_refs = [name for abbr, name in katrain.engine.RULESETS_ABBR] @@ -259,6 +260,20 @@ class NewGamePopup(QuickConfigGui): self.katrain.update_player(bw, **player_setup.player_type_dump) self.katrain("new-game") + def update_game(self): + props = self.collect_properties(self) + root = self.katrain.game.root + changed = False + for k, v in [("RU", props["game/rules"]), ("KM", props["game/komi"])]: + current = root.get_property(k) + if current != v: + changed = True + self.katrain.game.root.set_property(k, v) + if changed: + self.katrain.engine.on_new_game() + self.katrain.game.analyze_all_nodes() + self.popup.dismiss() + def wrap_anchor(widget): anchor = AnchorLayout() @@ -341,7 +356,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": @@ -482,20 +497,20 @@ class ConfigPopup(QuickConfigGui): 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", - "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.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-gpu-opencl-windows-x64.zip", + "Eigen AVX2 (Modern CPUs) v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-cpu-eigen-avx2-windows-x64.zip", + "Eigen (CPU, Non-optimized) v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-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", - "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.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-gpu-opencl-linux-x64.zip", + "Eigen AVX2 (Modern CPUs) v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-cpu-eigen-avx2-linux-x64.zip", + "Eigen (CPU, Non-optimized) v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-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", + "CUDA v1.7.0 (Windows)": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-gpu-cuda10.2-windows-x64.zip", + "CUDA v1.7.0 (Linux)": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-gpu-cuda10.2-linux-x64.zip", }, } diff --git a/katrain/gui/widgets/movetree.py b/katrain/gui/widgets/movetree.py index 69e2292..e9f1d73 100644 --- a/katrain/gui/widgets/movetree.py +++ b/katrain/gui/widgets/movetree.py @@ -3,7 +3,7 @@ from collections import defaultdict from kivy.graphics.context_instructions import Color from kivy.graphics.vertex_instructions import Line, Rectangle from kivy.lang import Builder -from kivy.properties import ObjectProperty, Clock, NumericProperty, BooleanProperty +from kivy.properties import BooleanProperty, Clock, NumericProperty, ObjectProperty from kivy.uix.dropdown import DropDown from kivy.uix.scrollview import ScrollView from kivy.uix.widget import Widget @@ -11,15 +11,15 @@ from kivymd.app import MDApp from katrain.gui.kivyutils import BackgroundMixin, draw_circle, draw_text from katrain.gui.style import ( - WHITE, - STONE_COLORS, - LIGHTGREY, BACKGROUND_COLOR, - OUTLINE_COLORS, LIGHTER_BACKGROUND_COLOR, - YELLOW, - STONE_TEXT_COLORS, + LIGHTGREY, + OUTLINE_COLORS, RED, + STONE_COLORS, + STONE_TEXT_COLORS, + WHITE, + YELLOW, ) @@ -85,6 +85,17 @@ class MoveTreeCanvas(Widget): self.set_game_node(parent) self.is_open = False + def make_selected_node_main_branch(self): + + if self.menu_selected_node and self.menu_selected_node.parent: + node = self.menu_selected_node + while node.parent is not None: + node.parent.children.remove(node) + node.parent.children.insert(0, node) + node = node.parent + self.set_game_node(self.menu_selected_node) + self.is_open = False + def switch_branch(self, direction=1): pos = self.move_pos.get(self.scroll_view_widget.current_node) if not self.scroll_view_widget: @@ -96,7 +107,7 @@ class MoveTreeCanvas(Widget): self.set_game_node(same_x_moves[new_index][1]) def draw_move_tree(self, current_node): - if not self.scroll_view_widget: + if not self.scroll_view_widget or not current_node: return spacing = 5 @@ -109,6 +120,7 @@ class MoveTreeCanvas(Widget): stack = root.ordered_children[::-1] next_y_pos = defaultdict(int) # x pos -> max y pos children = defaultdict(list) # since AI self-play etc may modify the tree between layout and draw! + children[root] = root.ordered_children while stack: move = stack.pop() x = move.depth @@ -176,6 +188,9 @@ class MoveTree(ScrollView, BackgroundMixin): def delete_selected_node(self): self.move_tree_canvas.delete_selected_node() + def make_selected_node_main_branch(self): + self.move_tree_canvas.make_selected_node_main_branch() + def scroll_to_pixel(self, x, y): if not self._viewport: return @@ -219,5 +234,11 @@ Builder.load_string( shortcut: '' on_action: root.katrain.controls.move_tree.delete_selected_node() -background_color: LIGHTER_BACKGROUND_COLOR + MoveTreeDropdownItem: + text: i18n._("Make Main Branch") + icon: 'img/Branch.png' + shortcut: '' + on_action: root.katrain.controls.move_tree.make_selected_node_main_branch() + -background_color: LIGHTER_BACKGROUND_COLOR """ ) diff --git a/katrain/gui/widgets/selection_slider.py b/katrain/gui/widgets/selection_slider.py index d15ec14..82bb261 100644 --- a/katrain/gui/widgets/selection_slider.py +++ b/katrain/gui/widgets/selection_slider.py @@ -127,7 +127,7 @@ KV = """ Label: id: label text: slider.values[slider.index][1] - font_size: dp(12) + font_size: sp(12) lang_change_tracking: i18n._('') # for font halign: "center" color: root.thumb_color if root.active else [0, 0, 0, 0] diff --git a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo index 7254b38..e98a995 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 2622d3a..d1a2f98 100644 --- a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po @@ -521,7 +521,6 @@ msgstr "读秒声音开启" msgid "tab:info-details" msgstr "详细" -#. TODO: updated msgid "teaching undo message" msgstr "撤销第 {move} 手棋因为损失超过 {points_lost:.1f} 目. 悬停在该手棋 上方查看信息. 请再次尝试." @@ -640,3 +639,54 @@ msgstr "寻找其他走法" #. status message msgid "alternative analysis" msgstr "正在搜索其他走法" + +msgid "minimal time use" +msgstr "最少思考时间 (秒)" + +msgid "move too fast" +msgstr "请思考 {num} 秒再下棋" + +msgid "main time" +msgstr "保留时间 (分钟)" + +msgid "aihelp:simple" +msgstr "" +"简化局面的风格。 " +"所有损失最高'max_points_lost'目和至少'min_visits'计算深度的着法当中,偏向于损失较小,乙方空定型(偏向值settled_weight),对方空定型(偏向值settled_weight" +" * opponent " +"fac),避免脱先(5路以上,偏向值tenuki_penalty),避免碰(偏向值attachment_penalty)的手法。偏向值可以设为复数捣乱局面,宽根噪声(通用设置)可以棋力代价更加符合风格。" +" " + +msgid "ai:simple" +msgstr "简明风格" + +msgid "analysis:local" +msgstr "寻找局部走法" + +msgid "local analysis" +msgstr "正在寻找局部走法" + +msgid "change current game" +msgstr "" +"更新贴目\n" +"规则在当前对局" + +msgid "Make Main Branch" +msgstr "设为实战变化" + +#. TODO - DEPRECATED +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 - DEPRECATED +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 5c942fc..0d41741 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 722e7ce..376b311 100644 --- a/katrain/i18n/locales/de/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/de/LC_MESSAGES/katrain.po @@ -671,12 +671,12 @@ msgstr "" msgid "show point loss as text" 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." +"Zug {move} zurückgenommen, da er {points_lost:.1f} Punkte verliert. Bitte " +"versuche es erneut. Bewege den Mauszeiger über den Zug, um die erwartete " +"Widerlegung zu sehen, oder setze ein Häkchen bei “Erwartetes Gebiet” (t), um" +" die Bereiche des Bretts zu sehen, wo Punktverlust zu erwarten ist." #. resign the game msgid "resign" @@ -689,3 +689,49 @@ msgstr "Finde alternative Züge" #. - status message msgid "alternative analysis" msgstr "Suche nach alternativen Zügen" + +#. TODO - ignore moves if timer is on and less than this is used +msgid "minimal time use" +msgstr "Minimal time use in byo-yomi (s)" + +msgid "move too fast" +msgstr "Denke mindestens {num} Sekunden nach, bevor Du setzt." + +msgid "main time" +msgstr "Hauptbedenkzeit (Min.)" + +msgid "aihelp:simple" +msgstr "" +"Spielt Züge, die dazu zu einer Vereinfachung des Brettzustandes führen. Von " +"Zügen,die höchstens 'max_points_lost' Punkte verlieren und mit wenigstens " +"'min_visits' Visits, bevorzuge die Züge, die weniger Punkte verlieren und" +"eigenes Gebiet mehr sichern." + +msgid "ai:simple" +msgstr "Einfacher Stil" + +#. TODO - DEPRECATED +msgid "aihelp:settle" +msgstr "" +"Spielt Züge, die dazu zu einer Vereinfachung des Brettzustandes führen. Von " +"Zügen,die höchstens 'max_points_lost' Punkte verlieren und mit wenigstens " +"'min_visits' Visits, bevorzuge die Züge, die weniger Punkte verlieren, " +"eigenes Gebiet mehr sichern" + +#. TODO - DEPRECATED +msgid "ai:settle" +msgstr "Settle Style" + +msgid "analysis:local" +msgstr "Finde lokale Antworten" + +msgid "local analysis" +msgstr "Suche nach lokalen Antworten" + +msgid "change current game" +msgstr "" +"Aktualisiere Komi/Regeln\n" +" im aktuellen Spiel" + +msgid "Make Main Branch" +msgstr "Zum Hauptzweig machen" diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo index 9c7f8dc..246353d 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 c5b79e3..72393d1 100644 --- a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po @@ -106,6 +106,10 @@ msgstr "Deeper full game analysis" msgid "analysis:alternatives" msgstr "Find alternative moves" +#. Analyze again but allow only nearby moves +msgid "analysis:local" +msgstr "Find local responses" + #. Keep improving current position analysis when there is nothing else to do msgid "analysis:continuous" msgstr "Toggle continuous analysis" @@ -125,6 +129,10 @@ msgstr "Analyzing entire board using {visits} visits/move" msgid "alternative analysis" msgstr "Searching for alternative moves" +#. status message +msgid "local analysis" +msgstr "Searching for local responses" + #. status message msgid "equalizing analysis" msgstr "Equalizing analysis of candidate moves to {visits} visits" @@ -293,14 +301,25 @@ msgid "timer settings" msgstr "Adjust Timer Settings" msgid "byoyomi length" -msgstr "Time per Byo-yomi Period" +msgstr "Time per byo-yomi period (s)" msgid "byoyomi periods" -msgstr "Number of Periods" +msgstr "Number of periods" msgid "count down sound" msgstr "Sound on" +#. in minutes +msgid "main time" +msgstr "Main time (min.)" + +#. ignore moves if timer is on and less than this is used +msgid "minimal time use" +msgstr "Minimal time use in byo-yomi (s)" + +msgid "move too fast" +msgstr "Think for at least {num} seconds before playing." + #. submit button msgid "update timer" msgstr "Update Timer" @@ -351,6 +370,12 @@ msgstr "Use x:y (e.g. 19:9) for non-square size" msgid "new game" msgstr "Start New Game" +#. change komi etc in current game +msgid "change current game" +msgstr "" +"Update komi/rules\n" +"in active game" + # teacher settings from here msgid "teacher settings" msgstr "Edit Teacher Settings" @@ -560,6 +585,38 @@ 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 increase play according to " +"the style at the cost of strength. " + +#. DEPRECATED +msgid "ai:settle" +msgstr "Settle Style" + +#. DEPRECATED +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" @@ -691,6 +748,10 @@ msgstr "Number of visits per move" msgid "Delete Node" msgstr "Delete Node" +#. on move tree editing +msgid "Make Main Branch" +msgstr "Make Main Branch" + #. theme label msgid "theme" msgstr "Colour Theme" diff --git a/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo index 75031f1..e1efb06 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 1124967..63d6036 100644 --- a/katrain/i18n/locales/es/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/es/LC_MESSAGES/katrain.po @@ -697,3 +697,58 @@ msgstr "Find alternative moves" #. TODO - status message msgid "alternative analysis" msgstr "Searching for alternative moves" + +#. TODO - ignore moves if timer is on and less than this is used +msgid "minimal time use" +msgstr "Minimal time use in byo-yomi (s)" + +#. TODO +msgid "move too fast" +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 - DEPRECATED +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 - DEPRECATED +msgid "ai:settle" +msgstr "Settle Style" + +#. TODO - Analyze again but allow only nearby moves +msgid "analysis:local" +msgstr "Find local responses" + +#. TODO - status message +msgid "local analysis" +msgstr "Searching for local responses" + +#. TODO - change komi etc in current game +msgid "change current game" +msgstr "" +"Update komi/rules\n" +"in active game" + +#. TODO - on move tree editing +msgid "Make Main Branch" +msgstr "Make Main Branch" diff --git a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo index 83b7aa1..187adca 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 bb070f0..e039b95 100644 --- a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po @@ -93,7 +93,7 @@ msgid "analysis:extra" msgstr "Analyser davantage" msgid "analysis:equalize" -msgstr "Égaliser l’analyse des coups" +msgstr "Égaliser l'analyse des coups" msgid "analysis:sweep" msgstr "Analyser tous les possibles" @@ -105,7 +105,7 @@ msgid "analysis:continuous" msgstr "Lancer/Arrêter l'analyse continue" msgid "analysis:aimove" -msgstr "Jouer le coup de l’IA" +msgstr "Jouer le coup de l'IA" #. label in little popup for analyzing entire game msgid "reanalyze max visits" @@ -525,9 +525,10 @@ msgstr "" #. TODO: updated msgid "teaching undo message" msgstr "" -"Le coup {move} a été annulé car il perdait {points_lost:.1f} points. Vous " -"pouvez survoler le coup pour voir la réfutation. Veuillez jouer un nouveau " -"coup." +"Le coup {move} a été annulé car il perdait {points_lost:.1f} points.Vous " +"pouvez consulter les Prévisions territoriales (t) pour identifier les " +"régions concernées, et survoler le coup pour connaître sa réfutation.Pour " +"continuer la partie, veuillez jouer un nouveau coup." msgid "sweep analysis" msgstr "Analyse de tous les possibles ({visits} visites/coup)." @@ -721,3 +722,57 @@ msgstr "Proposer des coups différents" msgid "alternative analysis" msgstr "Recherche de coups différents." + +msgid "minimal time use" +msgstr "" +"Temps de réflexion \n" +"minimum (s/période)" + +msgid "move too fast" +msgstr "Réfléchissez au moins {num} secondes avant de jouer." + +msgid "main time" +msgstr "Temps principal (min)" + +msgid "aihelp:simple" +msgstr "" +"Par défaut, le moteur de jeu cherche à simplifier la position. \n" +"Il joue un coup parmi ceux considérés au moins 'min_visits' fois et perdant au plus 'max_points_lost'. \n" +"'settled_weight' le pousse à consolider ses territoires, et ceux adverses une fois multiplié par 'opponent_fac'. \n" +"'tenuki_penalty' le pousse à jouer localement ; 'attach_penalty' à éviter les coups de contact direct. \n" +"Quand elles sont possibles, les valeurs négatives favorisent au contraire un style de jeu compliqué. \n" +"Élargir le Champ des possibles (cf. Paramètres généraux) peaufine le style au détriment du niveau de jeu." + +msgid "ai:simple" +msgstr "Modéré" + +#. TODO - DEPRECATED +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 - DEPRECATED +msgid "ai:settle" +msgstr "Settle Stones Style" + +msgid "analysis:local" +msgstr "Proposer des coups locaux" + +msgid "local analysis" +msgstr "Recherche de coups locaux." + +msgid "change current game" +msgstr "" +"Mettre à jour\n" +"la partie en cours" + +msgid "Make Main Branch" +msgstr "Mettre en branche principale" diff --git a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo index 9c28dd2..1667cd0 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 705244c..bc6154c 100644 --- a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po @@ -679,3 +679,55 @@ msgstr "" #. status message msgid "alternative analysis" msgstr "他の候補手を分析中" + +#. ignore moves if timer is on and less than this is used +msgid "minimal time use" +msgstr "早打ち禁止秒数" + +msgid "move too fast" +msgstr "早打ちすぎです。{num}秒は考えてください。" + +#. in minutes +msgid "main time" +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 "堅実派" + +#. Analyze again but allow only nearby moves +msgid "analysis:local" +msgstr "近辺の応手に絞って分析" + +#. status message +msgid "local analysis" +msgstr "近辺の応手を分析中" + +msgid "change current game" +msgstr "" +"現対局のコミ・\n" +"ルールを変更" + +#. on move tree editing +msgid "Make Main Branch" +msgstr "こちらを主分岐に" diff --git a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo index c14eca3..d2aadef 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 eaf984b..30e1461 100644 --- a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po @@ -546,11 +546,6 @@ msgstr "" "`threshold`선 밑의 수를 더 많이 뽑은 후 그 중 최선의 수를 둡니다. `line_weight`을 증가시키면 바둑판 중앙에 있는" " 수를 더욱 꺼리게 됩니다. 판이 `endgame` 비율만큼 차면 평범하게 둡니다." -#. TODO: updated -msgid "teaching undo message" -msgstr "" -"{points_lost:.1f}집 손해이므로 {move}을/를 물렀습니다. 커서를 올려 상대의 대응책을 보십시오. 다시 두어 주세요." - msgid "aihelp:p:rank" msgstr "" "제한적인 착수 가능 목록에서 무작위로 다음 착수를 선택한 후 그 중 최선의 수를 둡니다. 더 강한 설정은 최선의 수를 더 큰 목록에서 " @@ -654,3 +649,58 @@ msgstr "다른 수 찾기" msgid "alternative analysis" msgstr "다른 수를 찾는 중입니다" + +msgid "teaching undo message" +msgstr "" +"{points_lost:.1f}집 손해이므로 {move}을/를 물렀습니다. 다시 두어 주세요. 힌트를 보려면, 커서를 올려 상대의 " +"대응책을 보거나, 어디서 집 손해를 보는지 집 모양을 통해 (t) 알아보십시오. " + +msgid "minimal time use" +msgstr "이보다 빨리 착수한 수 무시" + +msgid "move too fast" +msgstr "최소한 {num}초만큼 생각하고 착수해 주십시오" + +msgid "main time" +msgstr "제한 시간(분)" + +msgid "aihelp:simple" +msgstr "" +"국면을 단순화하는 수를 둡니다. 최대 'max_point_lost'집 손해를 보고 최소 'min_visits'번 탐색한 수 중에, 집 " +"손해가 적고, 자신의 집을 굳히며(settled_weight에 따라 조절), 상대의 집도 굳히고(settled_weight * " +"opponent fac에 따라 조절), 붙임수를 피하는(attachment_penalty에 따라 조절) 수를 둡니다. 가중치가 음수일 " +"때는 반대로 국면을 복잡하게 만듭니다." + +msgid "ai:simple" +msgstr "간명한 기풍" + +msgid "analysis:local" +msgstr "국지적인 응수 찾기" + +msgid "local analysis" +msgstr "국지적인 응수를 찾고 있습니다" + +msgid "change current game" +msgstr "" +"이 판에 대해\n" +"덤과 규칙 등 조절" + +#. TODO - DEPRECATED +msgid "ai:settle" +msgstr "Settle Style" + +#. TODO - DEPRECATED +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 "Make Main Branch" +msgstr "주 변화로 만들기" diff --git a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo index 5459d12..0340107 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 bfed209..2f68fa8 100644 --- a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po @@ -407,7 +407,7 @@ msgid "engine:max_time" msgstr "Максимальное время анализа" msgid "engine:wide_root_noise" -msgstr "Высокий уровень шума (увеличивает разнообразие рассматриваемых ходов)" +msgstr "Уровень шума (увеличивает разнообразие рассматриваемых ходов)" msgid "engine:wide_root_noise:hint" msgstr "" @@ -684,3 +684,57 @@ msgstr "Поискать альтернативные ходы" msgid "alternative analysis" msgstr "Поиск альтернативных ходов" + +msgid "minimal time use" +msgstr "Минимально потраченное время в бёёми (сек.)" + +msgid "move too fast" +msgstr "Подумайте как минимум {num} секунд." + +msgid "main time" +msgstr "Основное время (мин.)" + +msgid "aihelp:simple" +msgstr "" +"Делает ходы, упрощающие позицию на доске. Из ходов, теряющих не более чем " +"'max_points_lost' очков и имеющих как минимум 'min_visits', предпочитает " +"ходы, укрепляющие собственную территорию (коэффициент settled_weight), " +"укрепляющие территорию оппонента (коэффициент settled_weight * " +"opponent_fac), избегает тэнуки (более 5 пунктов, коэффициент " +"tenuki_penalty), и избегает прилипаний (коэффициент attachment_penalty). " +"Коэффициенты могут быть отрицательными, чтобы напротив сделать игру сложнее." +" 'Уровень шума' (в 'Настройках системы') может быть использован для " +"увеличения вариативности игры за счёт небольшой потери силы. " + +msgid "ai:simple" +msgstr "Простой стиль" + +msgid "aihelp:settle" +msgstr "" +"Делает ходы, ведущие к ясности в статусе камней, предпочитая надёжные ходы, " +"нежели сложное сабаки. Из ходов, теряющих не более чем 'max_points_lost' " +"очков и имеющих как минимум 'min_visits', предпочитает ходы, укрепляющие " +"собственную территорию (коэффициент settled_weight), укрепляющие территорию " +"оппонента (коэффициент settled_weight * opponent_fac), избегает тэнуки " +"(более 5 пунктов, коэффициент tenuki_penalty), и избегает прилипаний " +"(коэффициент attachment_penalty). Коэффициенты могут быть отрицательными, " +"чтобы напротив сделать игру сложнее. 'Уровень шума' (в 'Настройках системы')" +" может быть использован для увеличения вариативности игры за счёт небольшой " +"потери силы. " + +msgid "ai:settle" +msgstr "Надёжный стиль" + +msgid "analysis:local" +msgstr "Найти локальные ответы" + +msgid "local analysis" +msgstr "Поиск локальных ответов" + +msgid "change current game" +msgstr "" +"Изменить коми/правила\n" +"в текущей игре" + +msgid "Make Main Branch" +msgstr "Сделать главной веткой" diff --git a/katrain/img/Next-Mistake.png b/katrain/img/Next-Mistake.png new file mode 100644 index 0000000..a1ccfb7 Binary files /dev/null and b/katrain/img/Next-Mistake.png differ diff --git a/katrain/img/local.png b/katrain/img/local.png new file mode 100644 index 0000000..cc4506a Binary files /dev/null and b/katrain/img/local.png differ diff --git a/katrain/popups.kv b/katrain/popups.kv index c9c5265..59fd232 100644 --- a/katrain/popups.kv +++ b/katrain/popups.kv @@ -8,8 +8,8 @@ #:set CP_SMALL_SPACING 3 #:set CP_PADDING 6 -#:set INPUT_FONT_SIZE dp(25) -#:set DESC_FONT_SIZE dp(20) +#:set INPUT_FONT_SIZE sp(25) +#:set DESC_FONT_SIZE sp(20) #:set LIGHTGREY [0.7,0.7,0.7,1] #:import BACKGROUND_COLOR katrain.gui.style.BACKGROUND_COLOR @@ -32,7 +32,7 @@ line_color_focus: TEXT_COLOR : - font_size: dp(16) + font_size: sp(16) multiline: False helper_text_mode: "on_error" helper_text: i18n._("file not found") @@ -179,13 +179,18 @@ orientation: 'horizontal' GridLayout: cols: 2 - rows: 4 + rows: 5 spacing: CP_SPACING DescriptionLabel: text: i18n._("general:sgf_save") AnchorLayout: LabelledPathInput: input_property: "general/sgf_save" + DescriptionLabel: + text: i18n._('count down sound') + AnchorLayout: + LabelledCheckBox: + input_property: "timer/sound" DescriptionLabel: text: i18n._("general:anim_pv_time") AnchorLayout: @@ -250,7 +255,7 @@ AutoSizedRoundedRectangleButton: padding_x: 15 size_hint: None,None - height: dp(40) + height: sp(40) text: i18n._("update settings") on_press: root.update_config(True) @@ -319,9 +324,15 @@ : MDGridLayout: cols: 2 - rows: 3 + rows: 4 spacing: 1.5 * CP_SPACING size_hint: 1,2.5 + DescriptionLabel: + text: i18n._('main time') + AnchorLayout: + LabelledIntInput: + text: "10" + input_property: "timer/main_time" DescriptionLabel: text: i18n._('byoyomi length') AnchorLayout: @@ -335,10 +346,11 @@ text: "5" input_property: "timer/byo_periods" DescriptionLabel: - text: i18n._('count down sound') + text: i18n._('minimal time use') AnchorLayout: - LabelledCheckBox: - input_property: "timer/sound" + LabelledIntInput: + text: "0" + input_property: "timer/minimal_use" AnchorLayout: size_hint: 1,1 AutoSizedRoundedRectangleButton: @@ -443,13 +455,22 @@ input_property: 'game/clear_cache' SmallDescriptionLabel: text: i18n._('avoids replaying') - AnchorLayout: - size_hint: 1,1 - AutoSizedRoundedRectangleButton: - padding_x: 15 - size_hint: None,0.5 - text: i18n._("new game") - on_press: root.update_config(True) + BoxLayout: + size_hint: 1,0.75 + AnchorLayout: + size_hint: 0.5, 1 + AutoSizedRoundedRectangleButton: + padding_x: 15 + size_hint: None,0.5 + _font_size: self.height*0.35 + text: i18n._("change current game") + on_press: root.update_game() + AnchorLayout: + AutoSizedRoundedRectangleButton: + padding_x: 15 + size_hint: None,0.66 + text: i18n._("new game") + on_press: root.update_config(True) : @@ -457,7 +478,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 diff --git a/katrain/sounds/boing.wav b/katrain/sounds/boing.wav new file mode 100644 index 0000000..2cee3df Binary files /dev/null and b/katrain/sounds/boing.wav differ diff --git a/setup.py b/setup.py index f5f16d2..2e114ee 100644 --- a/setup.py +++ b/setup.py @@ -41,16 +41,19 @@ setup( install_requires=[ "wheel", "setuptools", + "urllib3", "importlib_resources ;python_version<'3.7'", - "pygame;platform_system=='Darwin'", # some mac versions need this for kivy "cython>=0.24,<=0.29.14,!=0.27,!=0.27.2", # kivy wants this + "kivy==2.0.0rc2;platform_system=='Darwin'", # rc3 failing on mac + "kivy>=2.0.0rc2;platform_system!='Darwin'", # just use the latest on linux? + "pygame;platform_system=='Darwin'", # some mac versions need this for kivy "kivy_deps.glew;platform_system=='Windows'", "kivy_deps.sdl2;platform_system=='Windows'", "kivy_deps.gstreamer;platform_system=='Windows'", - "kivy==2.0.0rc2", # rc3 failing on mac "kivymd>=0.104.1", "screeninfo;platform_system!='Darwin'", # for screen resolution, has problems on macos ], + dependency_links=["https://kivy.org/downloads/simple/"], python_requires=">=3.6, <4", entry_points={"console_scripts": ["katrain=katrain.__main__:run_app"]}, classifiers=[