diff --git a/INSTALL.md b/INSTALL.md index efd2e02..c4ed789 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -74,7 +74,7 @@ As you can see it scores about twice as high as the Intel UHD chip and KataGo ha openclDeviceToUseThread0 = 1 openclDeviceToUseThread1 = 2 ``` -* Run `python3 katrain.py` and confirm that KataGo is now using both devices, by +* Run `python3 -m katrain` and confirm that KataGo is now using both devices, by checking the output from the terminal, which should indicate two devices being used. For example: ``` Found 3 device(s) on platform 0 with type CPU or GPU or Accelerator diff --git a/README.md b/README.md index 6e64afc..2e2d1b6 100644 --- a/README.md +++ b/README.md @@ -125,19 +125,12 @@ In addition to shortcuts mentioned above, there are: * **[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. * **[double-click on a move]**: Navigate directly to that point in the game. -* **[Ctrl-v]**: Load SGF from clipboard and do a 'fast' analysis of the game (with a high priority normal analysis for the last move). -* **[Ctrl-c]**: Save SGF to clipboard. -* **[Ctrl-l]**: Load SGF from file and do a normal analysis. -* **[Ctrl-s]**: Save SGF with automated review to file. -* **[Ctrl-n]**: Load SGF from clipboard -* **[F5]**: Open settings - -## Configuration - -Configuration is stored in `config.json`. Most settings are now available to edit in the program, but some advanced options are not. -You can use `python katrain.py your_config_file.json` to use another config file instead. - -If you ever need to reset to the original settings, simply re-download the `config.json` file in this repository. +* **[Ctrl-V]**: Load SGF from clipboard and do a 'fast' analysis of the game (with a high priority normal analysis for the last move). +* **[Ctrl-C]**: Save SGF to clipboard. +* **[Ctrl-L]**: Load SGF from file and do a normal analysis. +* **[Ctrl-S]**: Save SGF with automated review to file. +* **[Ctrl-N]**: Load SGF from clipboard +* **[F5]**: Open general settings ### Settings Panel diff --git a/dev/crash.py b/dev/crash.py index e1e8f8d..f4239c9 100644 --- a/dev/crash.py +++ b/dev/crash.py @@ -28,7 +28,7 @@ class MenuOpener(BoxLayout): {"icon": icon, "text": text, "right_content_cls": AnalysisDropdownMenuRightContent(text=shortcut)} for icon, text, shortcut in zip(ANALYSIS_ICONS, ANALYSIS_OPTIONS, ANALYSIS_SHORTCUTS) ] - self.analysis_menu = AnalysisDropdownMenu(caller=self.analysis_button, items=menu_items, width_mult=6, use_icon_item=False, callback=lambda _: print(_)) + self.analysis_menu = AnalysisDropdownMenu(caller=self.analysis_button, items=menu_items, width_mult=6, use_icon_item=False, callback=lambda _: print(_),) def open_analysis_menu(self): self.analysis_menu.open() diff --git a/dev/test.py b/dev/test.py index 6e1d397..e707565 100644 --- a/dev/test.py +++ b/dev/test.py @@ -28,7 +28,7 @@ class MenuOpener(BoxLayout): {"icon": icon, "text": text, "right_content_cls": AnalysisDropdownMenuRightContent(text=shortcut)} for icon, text, shortcut in zip(ANALYSIS_ICONS, ANALYSIS_OPTIONS, ANALYSIS_SHORTCUTS) ] - self.analysis_menu = AnalysisDropdownMenu(caller=self.analysis_button, items=menu_items, width_mult=6, use_icon_item=False, callback=lambda _: print(_)) + self.analysis_menu = AnalysisDropdownMenu(caller=self.analysis_button, items=menu_items, width_mult=6, use_icon_item=False, callback=lambda _: print(_),) # self.analysis_menu.create_menu_items() # for item in self.analysis_menu.menu.ids.box.children: diff --git a/katrain/__main__.py b/katrain/__main__.py index ada642b..3738fe6 100644 --- a/katrain/__main__.py +++ b/katrain/__main__.py @@ -6,6 +6,7 @@ Config.set("input", "mouse", "mouse,multitouch_on_demand") # isort:skip # no r Config.set("graphics", "width", 1400) Config.set("graphics", "height", 1000) + import os import signal import sys @@ -36,8 +37,9 @@ from katrain.core.utils import ( MODE_PLAY, switch_lang, ) -from katrain.gui.popups import ConfigTeacherPopup, ConfigTimerPopup +from katrain.gui.popups import ConfigTeacherPopup, ConfigTimerPopup, I18NPopup from katrain.gui.ai_settings import ConfigAIPopupContents +from katrain.core.settings_and_logging import KaTrainSettings from katrain.core.engine import KataGoEngine from katrain.core.game import Game, IllegalMoveException, KaTrainSGF from katrain.core.sgf_parser import Move, ParseError @@ -47,19 +49,17 @@ from katrain.gui.graph import ScoreGraph from katrain.gui.kivyutils import * from katrain.gui.popups import ConfigPopup, LoadSGFPopup, NewGamePopup from katrain.gui.style import ENGINE_BUSY_COL, ENGINE_DOWN_COL, ENGINE_READY_COL -from katrain.gui.style import * __version__ = "1.1.0" -class KaTrainGui(Screen): +class KaTrainGui(Screen, KaTrainSettings): """Top level class responsible for tying everything together""" zen = BooleanProperty(False) def __init__(self, **kwargs): - super(KaTrainGui, self).__init__(**kwargs) - self.debug_level = 0 + super().__init__(**kwargs) self.engine = None self.game = None @@ -70,16 +70,13 @@ class KaTrainGui(Screen): self.teacher_settings_popup = None self.timer_settings_popup = None - self.logger = lambda message, level=OUTPUT_INFO: self.log(message, level) - self.config_file = self._load_config() - - self.debug_level = self.config("debug/level", OUTPUT_INFO) self.message_queue = Queue() self._keyboard = Window.request_keyboard(None, self, "") self._keyboard.bind(on_key_down=self._on_keyboard_down) def log(self, message, level=OUTPUT_INFO): + super().log(message, level) if level == OUTPUT_KATAGO_STDERR and "ERROR" not in self.controls.status_label.text: if "starting" in message.lower(): self.controls.set_status(f"KataGo engine starting...") @@ -92,34 +89,6 @@ class KaTrainGui(Screen): self.controls.set_status(f"KataGo engine ready.") if level == OUTPUT_ERROR or (level == OUTPUT_KATAGO_STDERR and "error" in message.lower()): self.controls.set_status(f"ERROR: {message}") - print(f"ERROR: {message}") - elif self.debug_level >= level: - print(message) - - def _load_config(self): - config_file = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else find_package_resource("katrain/config.json")) - try: - self.log(f"Using config file {config_file}", OUTPUT_INFO) - self._config_store = JsonStore(config_file, indent=4) - self._config = dict(self._config_store) - return config_file - except Exception as e: - self.log(f"Failed to load config {config_file}: {e}", OUTPUT_ERROR) - sys.exit(1) - - def save_config(self): - for k, v in self._config.items(): - self._config_store.put(k, **v) - - def config(self, setting, default=None): - try: - if "/" in setting: - cat, key = setting.split("/") - return self._config[cat].get(key, default) - else: - return self._config[setting] - except KeyError: - self.log(f"Missing configuration option {setting}", OUTPUT_ERROR) @property def play_analyze_mode(self): @@ -141,15 +110,15 @@ class KaTrainGui(Screen): # AI and Trainer/auto-undo handlers cn = self.game.current_node if self.play_analyze_mode == MODE_PLAY: - auto_undo = cn.player and "undo" in self.controls.player_mode(cn.player) - if auto_undo and cn.analysis_ready and cn.parent and cn.parent.analysis_ready and not cn.children and not self.game.ended: - self.game.analyze_undo(cn, self.config("trainer")) # not via message loop + teaching_undo = cn.player and self.game.players[cn.player].being_taught + if teaching_undo and cn.analysis_ready and cn.parent and cn.parent.analysis_ready and not cn.children and not self.game.ended: + self.game.analyze_undo(cn) # not via message loop if ( cn.analysis_ready and "ai" in self.controls.player_mode(cn.next_player).lower() and not cn.children and not self.game.ended - and not (auto_undo and cn.auto_undo is None) + and not (teaching_undo and cn.auto_undo is None) ): self._do_ai_move(cn) # cn mismatch stops this if undo fired. avoid message loop here or fires repeatedly. @@ -213,7 +182,7 @@ class KaTrainGui(Screen): def _do_new_game(self, move_tree=None, analyze_fast=False): self.board_gui.animating_pv = None self.engine.on_new_game() # clear queries - self.game = Game(self, self.engine, self.config("game"), move_tree=move_tree, analyze_fast=analyze_fast) + self.game = Game(self, self.engine, move_tree=move_tree, analyze_fast=analyze_fast) self.controls.graph.initialize_from_game(self.game.root) self.controls.periods_used = {"B": 0, "W": 0} self.update_state(redraw_board=True) @@ -252,6 +221,22 @@ class KaTrainGui(Screen): def _do_analyze_extra(self, mode): self.game.analyze_extra(mode) + def _do_new_game_popup(self): + self.controls.timer.paused = True + if not self.new_game_popup: + self.new_game_popup = I18NPopup(title_key="New Game title", size_hint=(0.5, 0.6), content=NewGamePopup(self)).__self__ + self.new_game_popup.content.popup = self.new_game_popup + self.new_game_popup.open() + + def _do_timer_popup(self): + self.controls.timer.paused = True + if not self.timer_settings_popup: + self.timer_settings_popup = I18NPopup(title_key="timer settings", size=[450, 450], content=ConfigTimerPopup(self)).__self__ + self.timer_settings_popup.content.popup = self.timer_settings_popup + self.timer_settings_popup.open() + + # todo pop + def _do_analyze_sgf_popup(self): if not self.fileselect_popup: self.fileselect_popup = Popup(title="Double Click SGF file to analyze", size_hint=(0.8, 0.8)).__self__ @@ -273,14 +258,6 @@ class KaTrainGui(Screen): popup_contents.filesel.on_submit = readfile self.fileselect_popup.open() - def _do_new_game_popup(self): - self.controls.timer.paused = True - if not self.new_game_popup: - self.new_game_popup = Popup(title="New Game", size_hint=(0.5, 0.6)).__self__ - popup_contents = NewGamePopup(self, self.new_game_popup, {k: v[0] for k, v in self.game.root.properties.items() if len(v) == 1}) - self.new_game_popup.add_widget(popup_contents) - self.new_game_popup.open() - def _do_config_popup(self): self.controls.timer.paused = True if not self.config_popup: @@ -299,17 +276,10 @@ class KaTrainGui(Screen): def _do_teacher_popup(self): self.controls.timer.paused = True if not self.teacher_settings_popup: - self.teacher_settings_popup = Popup(title="Edit Teacher Settings", size_hint=(0.7, 0.8)).__self__ + self.teacher_settings_popup = I18NPopup(title_key="Edit Teacher Settings", size_hint=(0.7, 0.8)).__self__ self.teacher_settings_popup.add_widget(ConfigTeacherPopup(self, self.teacher_settings_popup)) self.teacher_settings_popup.open() - def _do_timer_popup(self): - self.controls.timer.paused = True - if not self.timer_settings_popup: - self.timer_settings_popup = Popup(title="Edit Timer Settings", size_hint=(0.4, 0.4)).__self__ - self.timer_settings_popup.add_widget(ConfigTimerPopup(self, self.timer_settings_popup)) - self.timer_settings_popup.open() - def _do_output_sgf(self): for pl in Move.PLAYERS: if not self.game.root.get_property(f"P{pl}"): @@ -444,13 +414,16 @@ class KaTrainApp(MDApp): def run_app(): kv_file = find_package_resource("katrain/gui.kv") + popup_kv_file = find_package_resource("katrain/popups.kv") resource_add_path(os.path.split(kv_file)[0]) Builder.load_file(kv_file) + Builder.load_file(popup_kv_file) app = KaTrainApp() signal.signal(signal.SIGINT, app.signal_handler) try: app.run() - except Exception: + except Exception as e: + print(e) app.on_request_close() raise diff --git a/katrain/config.json b/katrain/config.json index 702cc23..facf6ff 100644 --- a/katrain/config.json +++ b/katrain/config.json @@ -13,24 +13,22 @@ "_enable_ownership": true }, "timer": { - "byo_length": 30, - "byo_num": 5 + "byo_length": 30, + "byo_periods": 5 }, - "sgf": { + "general": { "sgf_load": "~/Downloads", "sgf_save": "./sgfout", - "save_feedback": [ - true, - true, - true, - true, - true, - true - ] + "anim_pv_time": 0.5, + "debug_level": 3, + "language": "en" }, "game": { - "init_size": 19, - "init_komi": 6.5 + "size": "13", + "komi": 0.5, + "handicap": 2, + "rules": "chinese", + "clear_cache": true }, "trainer": { "num_undo_prompts": [ @@ -49,10 +47,17 @@ 0.5, 0 ], + "save_feedback": [ + true, + true, + true, + true, + true, + true + ], "eval_off_show_last": 3, "eval_show_ai": true, - "lock_ai": false, - "anim_pv_time": 0.5 + "lock_ai": false }, "ai": { "Default": { @@ -133,9 +138,5 @@ "_help_left": "Samples `pick_n + pick_frac * ` and plays the best one, biased to below the `threshold` line.", "_help_right": "Increase `line_weight` to penalize moves closer to the center more. Stops strategy after the 'endgame' fraction of the board is filled." } - }, - "debug": { - "level": 1, - "_hint_level": "0: Little console output.\n1: Some extra output\nfor error reporting." } } \ No newline at end of file diff --git a/katrain/core/engine.py b/katrain/core/engine.py index 30f4f14..ce2c63c 100644 --- a/katrain/core/engine.py +++ b/katrain/core/engine.py @@ -18,8 +18,8 @@ class KataGoEngine: """Starts and communicates with the KataGO analysis engine""" # TODO: we don't support suicide in game.py, so no "tt": "tromp-taylor", "nz": "new-zealand" - RULESETS = {"jp": "japanese", "cn": "chinese", "ko": "korean", "aga": "aga"} - RULESETS.update({v: v for v in RULESETS.values()}) + RULESETS_ABBR = [("jp", "japanese"), ("cn", "chinese"), ("ko", "korean"), ("aga", "aga")] + RULESETS = {fromkey: name for abbr, name in RULESETS_ABBR for fromkey in [abbr, name]} @staticmethod def get_rules(node): diff --git a/katrain/core/game.py b/katrain/core/game.py index 33ff723..092d121 100644 --- a/katrain/core/game.py +++ b/katrain/core/game.py @@ -49,15 +49,14 @@ class Player: class Game: """Represents a game of go, including an implementation of capture rules.""" - DEFAULT_PROPERTIES = {"GM": 1, "FF": 4, "RU": "JP", "AP": "KaTrain:https://github.com/sanderland/katrain"} + DEFAULT_PROPERTIES = {"GM": 1, "FF": 4, "AP": "KaTrain:https://github.com/sanderland/katrain"} - def __init__(self, katrain, engine: Union[Dict, KataGoEngine], config: Dict, move_tree: GameNode = None, players: Dict = None, analyze_fast=False): + def __init__(self, katrain, engine: Union[Dict, KataGoEngine], move_tree: GameNode = None, players: Dict = None, analyze_fast=False): self.katrain = katrain if not isinstance(engine, Dict): engine = {"B": engine, "W": engine} self.players = players or {"B": Player("B"), "W": Player("W")} self.engines = engine - self.config = config self.game_id = datetime.strftime(datetime.now(), "%Y-%m-%d %H %M %S") if move_tree: @@ -67,12 +66,18 @@ class Game: if handicap and not self.root.placements: self.place_handicap_stones(handicap) else: - board_size = config.get("init_size", 19) - self.komi = self.config.get("init_komi", 6.5) + board_size = katrain.config("game/size") + self.komi = katrain.config("game/komi") self.root = GameNode(properties={**Game.DEFAULT_PROPERTIES, **{"SZ": board_size, "KM": self.komi, "DT": self.game_id}}) + handicap = katrain.config("game/handicap") + if handicap: + self.place_handicap_stones(handicap) self.root.set_property("PW", str(self.players["W"])) self.root.set_property("PB", str(self.players["B"])) + if not self.root.get_property("RU"): + self.root.set_property("RU", katrain.config("game/rules")) + self.set_current_node(self.root) threading.Thread(target=lambda: self.analyze_all_nodes(-1_000_000, analyze_fast=analyze_fast), daemon=True).start() # return faster, but bypass Kivy Clock @@ -324,7 +329,8 @@ class Game: for move in analyze_moves: cn.analyze(engine, priority, visits=visits, refine_move=move, time_limit=False) # explicitly requested so take as long as you need - def analyze_undo(self, node, train_config): + 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: return diff --git a/katrain/core/game_node.py b/katrain/core/game_node.py index 75221c7..8da2fcb 100644 --- a/katrain/core/game_node.py +++ b/katrain/core/game_node.py @@ -4,7 +4,7 @@ from typing import Dict, List, Optional, Tuple from katrain.core.utils import evaluation_class, var_to_grid, i18n from katrain.core.sgf_parser import Move, SGFNode -from katrain.gui.style import PV_COLOR +from katrain.gui.style import INFO_PV_COLOR class GameNode(SGFNode): @@ -117,7 +117,7 @@ class GameNode(SGFNode): def make_pv(self, player, pv, interactive): pvtext = f"{player}{' '.join(pv)}" if interactive: - pvtext = f"[u][ref={pvtext}][color={PV_COLOR}]{pvtext}[/color][/ref][/u]" + pvtext = f"[u][ref={pvtext}][color={INFO_PV_COLOR}]{pvtext}[/color][/ref][/u]" return pvtext def comment(self, sgf=False, teach=False, hints=False, interactive=True): diff --git a/katrain/core/settings_and_logging.py b/katrain/core/settings_and_logging.py new file mode 100644 index 0000000..1addb4d --- /dev/null +++ b/katrain/core/settings_and_logging.py @@ -0,0 +1,62 @@ +from kivy import Config +from kivy.storage.jsonstore import JsonStore +import os, sys +from katrain.core.utils import ( + OUTPUT_ERROR, + OUTPUT_INFO, + OUTPUT_KATAGO_STDERR, + find_package_resource, + OUTPUT_DEBUG, + OUTPUT_EXTRA_DEBUG, +) + + +class KaTrainSettings: + CONFIG_FILE = "katrain/config.json" + + """Settings and logging functionality, so other classes who need a katrain instance can be used without a GUI""" + + def __init__(self, **kwargs): + self.debug_level = 0 + self.logger = lambda message, level=OUTPUT_INFO: self.log(message, level) + self.config_file = self._load_config() + self.debug_level = self.config("general/debug_level", OUTPUT_INFO) + + Config.set("kivy", "log_level", "error") + if self.debug_level >= OUTPUT_DEBUG: + Config.set("kivy", "log_enable", 1) + Config.set("kivy", "log_level", "warning") + + # if self.debug_level >= OUTPUT_EXTRA_DEBUG: + # Config.set("kivy", "log_level", "trace") + + def log(self, message, level=OUTPUT_INFO): + if level == OUTPUT_ERROR: + print(f"ERROR: {message}", sys.stderr) + elif self.debug_level >= level: + print(message) + + def _load_config(self): + config_file = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else find_package_resource(self.CONFIG_FILE)) + try: + self.log(f"Using config file {config_file}", OUTPUT_INFO) + self._config_store = JsonStore(config_file, indent=4) + self._config = dict(self._config_store) + return config_file + except Exception as e: + self.log(f"Failed to load config {config_file}: {e}", OUTPUT_ERROR) + sys.exit(1) + + def save_config(self): + for k, v in self._config.items(): + self._config_store.put(k, **v) + + def config(self, setting, default=None): + try: + if "/" in setting: + cat, key = setting.split("/") + return self._config[cat].get(key, default) + else: + return self._config[setting] + except KeyError: + self.log(f"Missing configuration option {setting}", OUTPUT_ERROR) diff --git a/katrain/core/utils.py b/katrain/core/utils.py index 95d3884..3702dd3 100644 --- a/katrain/core/utils.py +++ b/katrain/core/utils.py @@ -67,8 +67,6 @@ class Lang(Observable): self.switch_lang(lang) def _(self, text): - if text == "": - return "" # just for font change return self.ugettext(text) def fbind(self, name, func, *args): diff --git a/katrain/gui.kv b/katrain/gui.kv index 1bc2ec9..f5096ce 100644 --- a/katrain/gui.kv +++ b/katrain/gui.kv @@ -1,6 +1,5 @@ #:kivy 1.11.0 -#:import i18n __main__.i18n -#:import find_package_resource __main__.find_package_resource +#:import i18n katrain.core.utils.i18n #:set BUTTON_COLOR [0.23, 0.30, 0.35, 1] #:set TOOLTIP_COLOR [0.43, 0.50, 0.65, 1] @@ -9,12 +8,22 @@ #:set GRAPH_DOT_COLOR [0.85, 0.3, 0.3,1] #-- NEW -#:import PLAYER_TYPES __main__.PLAYER_TYPES -#:import RED __main__.RED -#:import WHITE __main__.WHITE + +# TODO: refactor + +#:set CP_SPACING 6 +#:set CP_SMALL_SPACING 3 +#:set CP_PADDING 6 + +#:import PLAYER_TYPES katrain.core.constants.PLAYER_TYPES +#:import RED katrain.gui.style.RED +#:import WHITE katrain.gui.style.WHITE +#:import BACKGROUND_COLOR katrain.gui.style.BACKGROUND_COLOR +#:import BOX_BACKGROUND_COLOR katrain.gui.style.BOX_BACKGROUND_COLOR +#:import TEXT_COLOR katrain.gui.style.TEXT_COLOR + #:set LIGHTGREY [0.7,0.7,0.7,1] - #:set BLACK [0.05,0.05,0.05,1] #:set GREY [0.5,0.5,0.5,1] #:set GREEN [0.1,0.8,0.1,1] @@ -27,12 +36,13 @@ #:set WINRATE_COLOR GREEN #:set BUTTON_INACTIVE_COLOR LIGHTGREY -#:set BACKGROUND_COLOR [36/255,48/255,62/255,1] -#:set BOX_BACKGROUND_COLOR [46/255,65/255,88/255,1] -#:set TEXT_COLOR WHITE + #:set NOTES_FONT_SIZE dp(18) + + +# for sizing help