diff --git a/dev/profile.py b/dev/profile.py index bd276c9..8f14e85 100644 --- a/dev/profile.py +++ b/dev/profile.py @@ -4,6 +4,10 @@ import yappi from katrain.__main__ import run_app +yappi.set_clock_type("cpu") yappi.start() -run_app() +try: + run_app() +except: + pass yappi.get_func_stats().save("callgrind.out." + datetime.now().isoformat(), "CALLGRIND") diff --git a/katrain/__main__.py b/katrain/__main__.py index 813f687..4c87a22 100644 --- a/katrain/__main__.py +++ b/katrain/__main__.py @@ -23,23 +23,16 @@ from kivy.storage.jsonstore import JsonStore from kivy.uix.popup import Popup from kivy.uix.screenmanager import Screen from kivymd.app import MDApp -from katrain.core.constants import * from katrain.core.ai import ai_move from katrain.core.utils import ( DEFAULT_LANGUAGE, - OUTPUT_DEBUG, - OUTPUT_ERROR, - OUTPUT_EXTRA_DEBUG, - OUTPUT_INFO, - OUTPUT_KATAGO_STDERR, find_package_resource, i18n, - MODE_PLAY, - switch_lang, ) +from katrain.core.constants import OUTPUT_ERROR, OUTPUT_KATAGO_STDERR, OUTPUT_INFO, OUTPUT_DEBUG, OUTPUT_EXTRA_DEBUG, MODE_PLAY 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.base_katrain import KaTrainBase from katrain.core.engine import KataGoEngine from katrain.core.game import Game, IllegalMoveException, KaTrainSGF from katrain.core.sgf_parser import Move, ParseError @@ -53,15 +46,15 @@ from katrain.gui.style import ENGINE_BUSY_COL, ENGINE_DOWN_COL, ENGINE_READY_COL __version__ = "1.1.0" -class KaTrainGui(Screen, KaTrainSettings): +class KaTrainGui(Screen, KaTrainBase): """Top level class responsible for tying everything together""" zen = BooleanProperty(False) + controls = ObjectProperty(None) def __init__(self, **kwargs): super().__init__(**kwargs) self.engine = None - self.game = None self.new_game_popup = None self.fileselect_popup = None @@ -109,7 +102,7 @@ class KaTrainGui(Screen, KaTrainSettings): def update_state(self, redraw_board=False): # is called after every message and on receiving analyses and config changes # AI and Trainer/auto-undo handlers cn = self.game.current_node - last_player, next_player = self.game.players[cn.player], self.game.players[cn.next_player] + last_player, next_player = self.players_info[cn.player], self.players_info[cn.next_player] if self.play_analyze_mode == MODE_PLAY: teaching_undo = cn.player and last_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: @@ -120,7 +113,7 @@ class KaTrainGui(Screen, KaTrainSettings): # Handle prisoners and next player display prisoners = self.game.prisoner_count top, bot = [w.__self__ for w in self.board_controls.circles] # no weakref - if self.game.next_player.player == "W": + if self.next_player_info.player == "W": top, bot = bot, top self.controls.players["W"].active = True self.controls.players["B"].active = False @@ -148,6 +141,14 @@ class KaTrainGui(Screen, KaTrainSettings): self.controls.update_evaluation() self.controls.update_timer(1) + def update_player(self, bw, **kwargs): + super().update_player(bw, **kwargs) + if self.controls: + self.controls.update_players() + self.update_state() + for player_setup_block in PlayerSetupBlock.INSTANCES: + player_setup_block.update_players(bw,self.players_info[bw]) + def set_note(self, note): self.game.current_node.note = note @@ -173,14 +174,12 @@ class KaTrainGui(Screen, KaTrainSettings): self.board_gui.animating_pv = None self.engine.on_new_game() # clear queries self.game = Game(self, self.engine, move_tree=move_tree, analyze_fast=analyze_fast) - for bw, player_widget in self.nav_drawer_contents.player_setup.players.items(): - self.game.players[bw].update(**player_widget.player_def) self.controls.graph.initialize_from_game(self.game.root) self.update_state(redraw_board=True) def _do_ai_move(self, node=None): if node is None or self.game.current_node == node: - mode = self.game.next_player.strategy + mode = self.next_player_info.strategy settings = self.config(f"ai/{mode}") if settings: ai_move(self.game, mode, settings) @@ -190,7 +189,7 @@ class KaTrainGui(Screen, KaTrainSettings): def _do_undo(self, n_times=1): if n_times == "smart": n_times = 1 - if self.play_analyze_mode == "play" and self.game.last_player.ai and self.game.next_player.human: + if self.play_analyze_mode == "play" and self.last_player_info.ai and self.next_player_info.human: n_times = 2 self.board_gui.animating_pv = None self.game.undo(n_times) @@ -206,7 +205,7 @@ class KaTrainGui(Screen, KaTrainSettings): def _do_play(self, coords): self.board_gui.animating_pv = None try: - self.game.play(Move(coords, player=self.game.next_player.player)) + self.game.play(Move(coords, player=self.next_player_info.player)) except IllegalMoveException as e: self.controls.set_status(f"Illegal Move: {str(e)}") @@ -377,7 +376,7 @@ class KaTrainApp(MDApp): def on_language(self, _instance, language): self.gui.log(f"Switching language to {language}", OUTPUT_INFO) - switch_lang(language) + i18n.switch_lang(language) def on_start(self): self.gui.start() diff --git a/katrain/config.json b/katrain/config.json index 997a889..f16db5a 100644 --- a/katrain/config.json +++ b/katrain/config.json @@ -60,71 +60,59 @@ "lock_ai": false }, "ai": { - "default": { + "ai:default": { "_help_right": "No settings available for Default KataGo AI, strength is mainly affected by `max_visits` and `model` in the main settings `engine` section.", "_help_left": "" }, - "jigo": { + "ai:jigo": { "target_score": 0.5, "_help_left": "Will try to win by `target_score`, without further restrictions.", "_help_right": "Also affected by engine settings such as `max_visits`." }, - "scoreloss": { + "ai:scoreloss": { "strength": 0.5 }, - "Policy": { + "ai:policy": { "opening_moves": 20 }, - "P:Weighted": { - "_help_right": "pick_override` determines when top move is chosen without randomness, and `lower_bound` determines the lower bound policy value that is allowed.", - "_help_left": "Plays move with probability=policy^(1/weaken_fac),i.e. `weaken_fac` influences how much more likely weaker moves are picked.", + "ai:p:weighted": { "pick_override": 1.0, "lower_bound": 0.001, "weaken_fac": 1.25 }, - "P:Pick": { + "ai:p:pick": { "pick_override": 0.95, "pick_n": 5, - "pick_frac": 0.33, - "_help_left": "Picks `pick_n + pick_frac * ` at random and plays the best one. Change `pick_frac` to make it see more moves.", - "_help_right": "Plays top move if policy value is above `pick_override` to avoid obvious mistakes." + "pick_frac": 0.33 }, - "P:Local": { + "ai:p:local": { "pick_override": 0.95, "stddev": 1.5, "pick_n": 15, - "pick_frac": 0.0, - "_help_left": "Samples `pick_n + pick_frac * ` near the last move and plays the best one.", - "_help_right": "Lower `stddev` makes it prefer closer moves." + "pick_frac": 0.0 }, - "P:Tenuki": { + "ai:p:tenuki": { "pick_override": 0.85, "stddev": 7.5, "pick_n": 5, "pick_frac": 0.5, - "endgame": 0.45, - "_help_left": "Samples `pick_n + pick_frac * ` away from the last move and plays the best one.", - "_help_right": "Increase `stddev` makes it prefer moves further away. Stops tenukiing after the 'endgame' fraction of the board is filled." + "endgame": 0.45 }, - "P:Influence": { + "ai:p:influence": { "pick_override": 0.95, "pick_n": 5, "pick_frac": 0.4, "threshold": 3.5, "line_weight": 10, - "endgame": 0.4, - "_help_left": "Samples `pick_n + pick_frac * ` and plays the best one, biased to above the `threshold` line.", - "_help_right": "Increase `line_weight` to penalize moves near the edge more. Stops strategy after the 'endgame' fraction of the board is filled." + "endgame": 0.4 }, - "P:Territory": { + "ai:p:territory": { "pick_override": 0.95, "pick_n": 5, "pick_frac": 0.4, "threshold": 3.5, "line_weight": 2, - "endgame": 0.4, - "_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." + "endgame": 0.4 } } } \ No newline at end of file diff --git a/katrain/core/ai.py b/katrain/core/ai.py index 8c160bd..e3234c0 100644 --- a/katrain/core/ai.py +++ b/katrain/core/ai.py @@ -4,7 +4,8 @@ import random import time from typing import Dict, List, Tuple -from katrain.core.utils import OUTPUT_DEBUG, OUTPUT_INFO, var_to_grid +from katrain.core.utils import var_to_grid +from katrain.core.constants import OUTPUT_INFO, OUTPUT_DEBUG from katrain.core.engine import EngineDiedException from katrain.core.game import Game, GameNode, Move diff --git a/katrain/core/base_katrain.py b/katrain/core/base_katrain.py new file mode 100644 index 0000000..45a5d87 --- /dev/null +++ b/katrain/core/base_katrain.py @@ -0,0 +1,111 @@ +from kivy import Config +from kivy.properties import ObjectProperty +from kivy.storage.jsonstore import JsonStore +import os, sys +from katrain.core.constants import * + +from katrain.core.utils import find_package_resource + + +class Player: + def __init__(self, player="B", player_type=PLAYER_HUMAN, player_subtype=PLAYING_NORMAL, periods_used=0): + self.player = player + self.update(player_type, player_subtype) + self.periods_used = periods_used + + def update(self, player_type=PLAYER_HUMAN, player_subtype=PLAYING_NORMAL): + self.player_type = player_type + self.player_subtype = player_subtype + + @property + def ai(self): + return self.player_type == PLAYER_AI + + @property + def human(self): + return self.player_type == PLAYER_HUMAN + + @property + def being_taught(self): + return self.player_type == PLAYER_HUMAN and self.player_subtype == PLAYING_TEACHING + + @property + def strategy(self): + return self.player_subtype if self.ai else AI_DEFAULT + + def __str__(self): + return f"{self.player_type} ({self.player_subtype})" + + +class KaTrainBase: + CONFIG_FILE = "katrain/config.json" + + """Settings, logging, and players functionality, so other classes like bots who need a katrain instance can be used without a GUI""" + + def __init__(self, **kwargs): + self.debug_level = 0 + self.game = None + + 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") + self.players_info = {"B": Player("B"), "W": Player("W")} + self.reset_players() + + def log(self, message, level=OUTPUT_INFO): + if level == OUTPUT_ERROR: + print(f"ERROR: {message}", file=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) + + + def update_player(self, bw, **kwargs): + self.players_info[bw].update(**kwargs) + + def reset_players(self): + self.update_player("B") + self.update_player("W") + for v in self.players_info.values(): + v.periods_used = 0 + + + @property + def last_player_info(self) -> Player: + return self.players_info[self.game.current_node.player] + + @property + def next_player_info(self) -> Player: + return self.players_info[self.game.current_node.next_player] + diff --git a/katrain/core/constants.py b/katrain/core/constants.py index d9d91bd..a2c3046 100644 --- a/katrain/core/constants.py +++ b/katrain/core/constants.py @@ -1,8 +1,17 @@ +HOMEPAGE = "https://github.com/sanderland/katrain" + PLAYER_HUMAN, PLAYER_AI = "player:human", "player:ai" PLAYER_TYPES = [PLAYER_HUMAN, PLAYER_AI] PLAYING_NORMAL, PLAYING_TEACHING = "game:normal", "game:teach" GAME_TYPES = [PLAYING_NORMAL, PLAYING_TEACHING] +MODE_PLAY, MODE_ANALYZE = "play", "analyze" + AI_DEFAULT = "default" AI_STRATEGIES = [AI_DEFAULT] +OUTPUT_ERROR = -1 +OUTPUT_KATAGO_STDERR = -0.5 +OUTPUT_INFO = 0 +OUTPUT_DEBUG = 1 +OUTPUT_EXTRA_DEBUG = 2 diff --git a/katrain/core/engine.py b/katrain/core/engine.py index ce2c63c..ffd6b14 100644 --- a/katrain/core/engine.py +++ b/katrain/core/engine.py @@ -6,7 +6,8 @@ import threading import time from typing import Callable, Optional -from katrain.core.utils import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_EXTRA_DEBUG, OUTPUT_KATAGO_STDERR, find_package_resource, i18n +from katrain.core.utils import find_package_resource, i18n +from katrain.core.constants import OUTPUT_ERROR, OUTPUT_KATAGO_STDERR, OUTPUT_DEBUG, OUTPUT_EXTRA_DEBUG from katrain.core.game_node import GameNode diff --git a/katrain/core/game.py b/katrain/core/game.py index 5879921..b761463 100644 --- a/katrain/core/game.py +++ b/katrain/core/game.py @@ -5,11 +5,11 @@ import threading from datetime import datetime from typing import Dict, List, Union, Optional -from katrain.core.utils import var_to_grid, OUTPUT_INFO, OUTPUT_DEBUG, i18n +from katrain.core.utils import var_to_grid, i18n +from katrain.core.constants import OUTPUT_INFO, OUTPUT_DEBUG, HOMEPAGE from katrain.core.engine import KataGoEngine from katrain.core.game_node import GameNode from katrain.core.sgf_parser import SGF, Move -from katrain.core.constants import * class IllegalMoveException(Exception): @@ -20,46 +20,15 @@ class KaTrainSGF(SGF): _NODE_CLASS = GameNode -class Player: - def __init__(self, player="B", player_type=PLAYER_HUMAN, player_subtype=PLAYING_NORMAL, periods_used=0): - self.player = player - self.update(player_type, player_subtype) - self.periods_used = periods_used - - def update(self, player_type, player_subtype): - self.player_type = player_type - self.player_subtype = player_subtype - - @property - def ai(self): - return self.player_type == PLAYER_AI - - @property - def human(self): - return self.player_type == PLAYER_HUMAN - - @property - def being_taught(self): - return self.player_type == PLAYER_HUMAN and self.player_subtype == PLAYING_TEACHING - - @property - def strategy(self): - return self.player_subtype if self.ai else AI_DEFAULT - - def __str__(self): - return f"{self.player_type} ({self.player_subtype})" - - class Game: """Represents a game of go, including an implementation of capture rules.""" - DEFAULT_PROPERTIES = {"GM": 1, "FF": 4, "AP": "KaTrain:https://github.com/sanderland/katrain"} + DEFAULT_PROPERTIES = {"GM": 1, "FF": 4, "AP": f"KaTrain:{HOMEPAGE}"} - def __init__(self, katrain, engine: Union[Dict, KataGoEngine], move_tree: GameNode = None, players: Dict = None, analyze_fast=False): + def __init__(self, katrain, engine: Union[Dict, KataGoEngine], move_tree: GameNode = 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.game_id = datetime.strftime(datetime.now(), "%Y-%m-%d %H %M %S") @@ -76,8 +45,6 @@ class Game: 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")) @@ -85,14 +52,6 @@ class Game: 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 - @property - def last_player(self) -> Player: - return self.players[self.current_node.player] - - @property - def next_player(self) -> Player: - return self.players[self.current_node.next_player] - def analyze_all_nodes(self, priority=0, analyze_fast=False): for node in self.root.nodes_in_tree: node.analyze(self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast) @@ -287,10 +246,8 @@ class Game: if eval_thresholds is None: eval_thresholds = self.katrain.config("trainer/eval_thresholds") - black, white = self.root.get_property("PB"), self.root.get_property("PW") - black = re.sub(r"['<>:\"/\\|?*]", "", black or "Black") - white = re.sub(r"['<>:\"/\\|?*]", "", white or "White") - game_name = f"katrain_{black} vs {white} {self.game_id}" + player_names = {bw: re.sub(r"['<>:\"/\\|?*]", "", self.root.get_property("P"+bw) or str(self.katrain.players_info[bw])) for bw in "BW" } + game_name = f"katrain_{player_names['B']} vs {player_names['W']} {self.game_id}" file_name = os.path.abspath(os.path.join(path, f"{game_name}.sgf")) os.makedirs(os.path.dirname(file_name), exist_ok=True) diff --git a/katrain/core/game_node.py b/katrain/core/game_node.py index 8da2fcb..85d1a0c 100644 --- a/katrain/core/game_node.py +++ b/katrain/core/game_node.py @@ -142,7 +142,6 @@ class GameNode(SGFNode): else: text += i18n._("Info:best move") + "\n" if previous_top_move.get("pv") and (sgf or hints): - print("PREV", previous_top_move) text += i18n._("Info:PV").format(pv=self.make_pv(single_move.player, previous_top_move["pv"], interactive)) + "\n" if sgf or hints or teach: diff --git a/katrain/core/settings_and_logging.py b/katrain/core/settings_and_logging.py deleted file mode 100644 index 91cff66..0000000 --- a/katrain/core/settings_and_logging.py +++ /dev/null @@ -1,62 +0,0 @@ -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}", file=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 0203f2e..3d80d7c 100644 --- a/katrain/core/utils.py +++ b/katrain/core/utils.py @@ -5,8 +5,6 @@ from typing import List, Tuple, Callable from kivy.lang import Observable import gettext -from kivymd.uix.textfield import MDTextField - from katrain.gui.style import DEFAULT_FONT try: @@ -14,14 +12,6 @@ try: except: import importlib_resources as pkg_resources -OUTPUT_ERROR = -1 -OUTPUT_KATAGO_STDERR = -0.5 -OUTPUT_INFO = 0 -OUTPUT_DEBUG = 1 -OUTPUT_EXTRA_DEBUG = 2 - -MODE_PLAY, MODE_ANALYZE = "play", "analyze" - def var_to_grid(array_var: List[Number], size: Tuple[int, int]) -> List[List[Number]]: """convert ownership/policy to grid format such that grid[y][x] is for move with coords x,y""" @@ -113,7 +103,3 @@ class Lang(Observable): DEFAULT_LANGUAGE = "en" i18n = Lang(DEFAULT_LANGUAGE) - - -def switch_lang(lang): - i18n.switch_lang(lang) diff --git a/katrain/fonts/digital-7.ttf b/katrain/fonts/digital-7.ttf deleted file mode 100644 index 5dbe6f9..0000000 Binary files a/katrain/fonts/digital-7.ttf and /dev/null differ diff --git a/katrain/gui.kv b/katrain/gui.kv index ef1a732..f34574a 100644 --- a/katrain/gui.kv +++ b/katrain/gui.kv @@ -119,7 +119,7 @@ : background_radius: self.height/3.5 -: +: background_color: BOX_BACKGROUND_COLOR : @@ -695,7 +695,7 @@ notebox.size_hint_y = int(args[1]['notes'])*0.66+1e-9 notebox.opacity = int(args[1]['notes']) options_height: root.tab_option_height - BGMDBoxLayout: + BGBoxLayout: background_color: BOX_BACKGROUND_COLOR orientation: 'vertical' ScrollableLabel: @@ -895,9 +895,9 @@ StyledSpinner: id: player_subtype value_refs: [] - on_text: root.set_player() + on_text: root.update_global_player_info() -: +: orientation: 'vertical' size_hint_y: None spacing: 3*CP_SPACING @@ -1023,7 +1023,7 @@ ScreenManager: Screen: name: 'gui' - BGMDBoxLayout: + BGBoxLayout: background_color: BACKGROUND_COLOR MDBoxLayout: orientation: 'vertical' diff --git a/katrain/gui/ai_settings.py b/katrain/gui/ai_settings.py index 56e1fbd..77c73dd 100644 --- a/katrain/gui/ai_settings.py +++ b/katrain/gui/ai_settings.py @@ -4,7 +4,7 @@ from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.gridlayout import MDGridLayout from kivymd.uix.label import MDLabel -from katrain.core.utils import OUTPUT_DEBUG, OUTPUT_ERROR +from katrain.core.constants import OUTPUT_ERROR, OUTPUT_DEBUG from katrain.gui.kivyutils import SizedButton, LightHelpLabel, ScaledLightLabel, StyledSpinner from katrain.gui.popups import QuickConfigGui, ConfigPopup, InputParseError diff --git a/katrain/gui/badukpan.py b/katrain/gui/badukpan.py index 6a1af5e..1f6cb4b 100644 --- a/katrain/gui/badukpan.py +++ b/katrain/gui/badukpan.py @@ -14,7 +14,8 @@ from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.floatlayout import MDFloatLayout from kivymd.uix.menu import MDDropdownMenu, RightContent -from katrain.core.utils import OUTPUT_DEBUG, evaluation_class, i18n, MODE_PLAY +from katrain.core.utils import evaluation_class, i18n +from katrain.core.constants import OUTPUT_DEBUG, MODE_PLAY from katrain.core.game import Move from katrain.gui.kivyutils import draw_circle, draw_text from katrain.core.utils import var_to_grid @@ -50,9 +51,12 @@ class BadukPanWidget(Widget): self.stone_size = 0 self.active_pv_moves = [] self.animating_pv = None - self.redraw_board_contents_trigger = Clock.create_trigger(self.draw_board_contents) self.last_mouse_pos = (0, 0) 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) Clock.schedule_interval(self.animate_pv, 0.1) # stone placement functions @@ -132,7 +136,7 @@ class BadukPanWidget(Widget): self.draw_hover_contents() # remove ghost # drawing functions - def on_size(self, *_args): + def redraw(self,*_args): self.draw_board() self.draw_board_contents() @@ -276,7 +280,7 @@ class BadukPanWidget(Widget): Rectangle(pos=(self.gridpos_x[x] - rsz / 2, self.gridpos_y[y] - rsz / 2), size=(rsz, rsz)) policy = current_node.policy - if not policy and current_node.parent and current_node.parent.policy and katrain.game.last_player.ai and katrain.game.next_player.ai: + if not policy and current_node.parent and current_node.parent.policy and katrain.last_player_info.ai and katrain.next_player_info.ai: policy = current_node.parent.policy # in the case of AI self-play we allow the policy to be one step out of date pass_btn = katrain.board_controls.pass_btn diff --git a/katrain/gui/controlspanel.py b/katrain/gui/controlspanel.py index 306ba7f..3dad15c 100644 --- a/katrain/gui/controlspanel.py +++ b/katrain/gui/controlspanel.py @@ -7,7 +7,8 @@ from kivy.uix.popup import Popup from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.floatlayout import MDFloatLayout -from katrain.core.utils import MODE_PLAY, MODE_ANALYZE, i18n +from katrain.core.utils import i18n +from katrain.core.constants import MODE_PLAY, MODE_ANALYZE class PlayAnalyzeSelect(MDFloatLayout): @@ -38,9 +39,9 @@ class ControlsPanel(BoxLayout): Clock.schedule_interval(self.update_timer, 0.07) def update_players(self, *_args): - for player, info in self.katrain.game.players.items(): - self.players[player].player_type = i18n._(info.player_type) - self.players[player].player_subtype = i18n._(info.player_subtype) + for bw, player_info in self.katrain.players_info.items(): + self.players[bw].player_type = i18n._(player_info.player_type) + self.players[bw].player_subtype = i18n._(player_info.player_subtype) def set_status(self, msg, at_node=None): self.status_msg = msg @@ -59,11 +60,11 @@ class ControlsPanel(BoxLayout): self.status.text = "" self.status_node = None - last_player_was_ai_playing_human = game.last_player.ai and game.next_player.human + last_player_was_ai_playing_human = katrain.last_player_info.ai and katrain.next_player_info.human self.active_comment_node = current_node if katrain.play_analyze_mode == MODE_PLAY and last_player_was_ai_playing_human: - if game.next_player.being_taught and current_node.children and current_node.children.auto_undo: + if katrain.next_player_info.being_taught and current_node.children and current_node.children.auto_undo: self.active_comment_node = current_node.children[-1] elif current_node.parent: self.active_comment_node = current_node.parent @@ -71,7 +72,7 @@ class ControlsPanel(BoxLayout): hints = katrain.analysis_controls.hints.active info = "" if current_node.move and not current_node.is_root: - info = self.active_comment_node.comment(teach=game.players[self.active_comment_node.player].being_taught, hints=hints) + info = self.active_comment_node.comment(teach=katrain.players_info[self.active_comment_node.player].being_taught, hints=hints) if self.active_comment_node.analysis_ready: self.stats.score = self.active_comment_node.format_score() or "" @@ -96,7 +97,7 @@ class ControlsPanel(BoxLayout): self.last_timer_update = (current_node, now) byo_len = max(1, self.katrain.config("timer/byo_length")) byo_num = max(1, self.katrain.config("timer/byo_periods")) - player = self.katrain.game.next_player + player = self.katrain.next_player_info ai = player.ai if not self.timer.paused and not ai: if last_update_node == current_node and not current_node.children: diff --git a/katrain/gui/kivyutils.py b/katrain/gui/kivyutils.py index 01d0264..07f9506 100644 --- a/katrain/gui/kivyutils.py +++ b/katrain/gui/kivyutils.py @@ -14,6 +14,7 @@ from kivy.properties import ( ObjectProperty, ) from kivy.uix.behaviors import ToggleButtonBehavior, ButtonBehavior +from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.uix.checkbox import CheckBox from kivy.uix.filechooser import FileChooserLayout, FileChooserListLayout @@ -170,7 +171,7 @@ class CircleWithText(Widget): min_size = NumericProperty(50) -class BGMDBoxLayout(MDBoxLayout, BackgroundMixin): +class BGBoxLayout(BoxLayout, BackgroundMixin): pass @@ -195,11 +196,17 @@ class StyledSpinner(Spinner): except (ValueError, IndexError): return 0, "", "" + def select_key(self, key): + try: + ix = self.value_refs.index(key) + self.text = self.values[ix] + except: + pass + def i18n_values(self, *_args): if self.value_refs: selected = self.selected[0] self.values = [i18n._(ref) for ref in self.value_refs] - print(self.selected, self.values, self.value_refs) self.text = self.values[selected] self.font_name = i18n.font_name self.update_dropdown_props() @@ -221,6 +228,18 @@ class StyledSpinner(Spinner): item.font_name = self.font_name +class PlayerSetupBlock(MDBoxLayout): + players = ObjectProperty(None) + INSTANCES = [] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + PlayerSetupBlock.INSTANCES.append(self) + + def update_players(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) + + class PlayerSetup(MDBoxLayout): player = OptionProperty("B", options=["B", "W"]) @@ -237,19 +256,20 @@ class PlayerSetup(MDBoxLayout): self.player_subtype_label.text = i18n._("aistrategy") self.player_subtype.value_refs = MDApp.get_running_app().gui.ai_strategies # TODO: self.player_subtype.text = self.player_subtype.values[0] - self.set_player() + self.update_global_player_info() @property - def player_def(self): + def player_type_dump(self): return {"player_type": self.player_type.selected[1], "player_subtype": self.player_subtype.selected[1]} - def set_player(self): + def update_widget(self, player_type, player_subtype): + self.player_type.select_key(player_type) # should trigger setup options + self.player_subtype.select_key(player_subtype) # should trigger setup options + + def update_global_player_info(self): katrain = MDApp.get_running_app().gui - game = katrain and katrain.game - if game: - game.players[self.player].update(**self.player_def) - katrain.controls.update_players() - katrain.update_state() + if katrain.game and katrain.game.current_node: + katrain.update_player(self.player, **self.player_type_dump) class PlayerInfo(MDBoxLayout, BackgroundMixin): @@ -260,7 +280,7 @@ class PlayerInfo(MDBoxLayout, BackgroundMixin): active = BooleanProperty(True) -class Timer(BGMDBoxLayout): +class Timer(BGBoxLayout): state = ListProperty([30, 5, 1]) diff --git a/katrain/gui/popups.py b/katrain/gui/popups.py index e006044..7618eaf 100644 --- a/katrain/gui/popups.py +++ b/katrain/gui/popups.py @@ -12,7 +12,7 @@ from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.selectioncontrol import MDCheckbox from kivymd.uix.textfield import MDTextField -from katrain.core.utils import OUTPUT_DEBUG, OUTPUT_ERROR +from katrain.core.constants import OUTPUT_ERROR, OUTPUT_DEBUG from katrain.core.engine import KataGoEngine from katrain.core.game import Game, GameNode from katrain.gui.kivyutils import StyledSpinner @@ -163,7 +163,7 @@ class ConfigTimerPopup(QuickConfigGui): def update_config(self, save_to_file=True): super().update_config(save_to_file=save_to_file) - for p in self.katrain.game.players.values(): + for p in self.katrain.players_info.values(): p.periods_used = 0 self.katrain.controls.timer.paused = True self.katrain.game.current_node.time_used = 0 @@ -183,6 +183,9 @@ class NewGamePopup(QuickConfigGui): self.katrain.engine.restart() self.katrain("new-game") + # for bw, player_widget in self.nav_drawer_contents.player_setup.players.items(): + # self.players[bw].update(**player_widget.player_def) + class ConfigPopup(QuickConfigGui): def __init__(self, katrain, popup: Popup, config: Dict, ignore_cats: Tuple = (), **kwargs): diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo index df12272..bf632aa 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 746e53a..09c403a 100644 --- a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po @@ -311,7 +311,7 @@ msgstr "" msgid "Engine died unexpectedly" msgstr "" "Engine died unexpectedly without sending output, possibly due to out of " -"memory: {e}" +"memory: {error}" #. AI names, help etc msgid "strength:kyu" @@ -320,7 +320,7 @@ msgstr "k" msgid "strength:dan" msgstr "d" -msgid "ainame:default" +msgid "ai:default" msgstr "KataGo" msgid "aihelp:default" @@ -328,7 +328,7 @@ msgstr "" "Full strength KataGo AI. Strength is mainly affected by `max_visits` and " "`model` in the general settings `engine` section." -msgid "ainame:jigo" +msgid "ai:jigo" msgstr "KataJigo" msgid "aihelp:jigo" @@ -336,7 +336,7 @@ msgstr "" "Will try to win by a set amount of points (default 0.5), without further " "restrictions." -msgid "ainame:jigo" +msgid "ai:jigo" msgstr "KataJigo" msgid "aihelp:jigo" @@ -344,7 +344,7 @@ msgstr "" "Will try to win by a set amount of points (default 0.5), without further " "restrictions." -msgid "ainame:scoreloss" +msgid "ai:scoreloss" msgstr "Score Loss" msgid "aihelp:scoreloss" @@ -352,7 +352,7 @@ msgstr "" "Plays moves that lead to more points lost with lower probability. Likely to " "play more varied/weaker with higher visits settings." -msgid "ainame:policy" +msgid "ai:policy" msgstr "Policy" msgid "aihelp:policy" @@ -360,3 +360,59 @@ msgstr "" "Plays the top move from the policy network, without any reading. Strength is" " mainly affected by `model` in engine settings. The opening moves setting " "affects how many moves in the opening are more random." + +msgid "ai:p:weighted" +msgstr "Policy Weighted" + +msgid "aihelp:p:weighted" +msgstr "" +"Plays moves according to policy probability, with `weaken_fac` influencing " +"how much more likely weaker moves are played. A high negative value plays " +"like the `Policy` AI.`pick_override` determines when top move is chosen " +"without randomness, and `lower_bound` determines the lower bound policy " +"value that is allowed." + +msgid "ai:p:pick" +msgstr "Blinded Policy" + +msgid "aihelp:p:pick" +msgstr "" +"Picks `pick_n + pick_frac * ` at random and plays the" +" best one. For `pick_frac=1` plays like the `Policy` AI. Plays top move if " +"policy value is above `pick_override` to avoid obvious mistakes." + +msgid "ai:p:local" +msgstr "Local Response Style" + +msgid "aihelp:p:local" +msgstr "" +"Picks `pick_n + pick_frac * ` weighted towards the " +"last move and plays the best one. Plays top move if policy value is above " +"`pick_override` to avoid obvious mistakes." + +msgid "ai:p:tenuki" +msgstr "Tenuki Style" + +msgid "aihelp:p:tenuki" +msgstr "" +"Picks `pick_n + pick_frac * ` weighted away from the " +"last move and plays the best one. Plays top move if policy value is above " +"`pick_override` to avoid obvious mistakes." + +msgid "ai:p:influence" +msgstr "Influential Style" + +msgid "aihelp:p:influence" +msgstr "" +"Picks moves biased above the `threshold` line and plays the best " +"one.Increase `line_weight` to penalize moves near the edge more. Stops " +"strategy after the 'endgame' fraction of the board is filled." + +msgid "ai:p:territory" +msgstr "Respond Locally" + +msgid "aihelp:p:territory" +msgstr "" +"Picks moves biased below the `threshold` line and plays the best " +"one.Increase `line_weight` to penalize moves near the center more. Stops " +"strategy after the 'endgame' fraction of the board is filled." diff --git a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo index eead6a6..8c6c1e5 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 ad0e281..5352582 100644 --- a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po @@ -398,3 +398,87 @@ msgstr "k" #. TODO msgid "strength:dan" msgstr "d" + +#. TODO +msgid "aihelp:p:weighted" +msgstr "" +"Plays moves according to policy probability, with `weaken_fac` influencing " +"how much more likely weaker moves are played. A high negative value plays " +"like the `Policy` AI.`pick_override` determines when top move is chosen " +"without randomness, and `lower_bound` determines the lower bound policy " +"value that is allowed." + +#. TODO +msgid "aihelp:p:pick" +msgstr "" +"Picks `pick_n + pick_frac * ` at random and plays the" +" best one. For `pick_frac=1` plays like the `Policy` AI. Plays top move if " +"policy value is above `pick_override` to avoid obvious mistakes." + +#. TODO +msgid "ai:p:influence" +msgstr "Influential Style" + +#. TODO +msgid "ai:p:local" +msgstr "Respond Locally" + +#. TODO +msgid "aihelp:p:tenuki" +msgstr "" +"Picks `pick_n + pick_frac * ` weighted away from the " +"last move and plays the best one. Plays top move if policy value is above " +"`pick_override` to avoid obvious mistakes." + +#. TODO +msgid "ai:scoreloss" +msgstr "Score Loss" + +#. TODO +msgid "aihelp:p:influence" +msgstr "" +"Picks moves biased above the `threshold` line and plays the best " +"one.Increase `line_weight` to penalize moves near the edge more. Stops " +"strategy after the 'endgame' fraction of the board is filled." + +#. TODO +msgid "ai:default" +msgstr "KataGo" + +#. TODO +msgid "ai:p:tenuki" +msgstr "Tenuki Style" + +#. TODO +msgid "ai:p:pick" +msgstr "Blinded Policy" + +#. TODO +msgid "ai:p:weighted" +msgstr "Policy Weighted" + +#. TODO +msgid "ai:policy" +msgstr "Policy" + +#. TODO +msgid "ai:jigo" +msgstr "KataJigo" + +#. TODO +msgid "aihelp:p:local" +msgstr "" +"Picks moves biased below the `threshold` line and plays the best " +"one.Increase `line_weight` to penalize moves near the center more. Stops " +"strategy after the 'endgame' fraction of the board is filled." + +#. TODO +msgid "aihelp:p:territory" +msgstr "" +"Picks moves biased below the `threshold` line and plays the best " +"one.Increase `line_weight` to penalize moves near the center more. Stops " +"strategy after the 'endgame' fraction of the board is filled." + +#. TODO +msgid "ai:p:territory" +msgstr "Respond Locally" diff --git a/katrain/i18n/locales/nl/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/nl/LC_MESSAGES/katrain.mo index 9de209e..189c2ae 100644 Binary files a/katrain/i18n/locales/nl/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/nl/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/nl/LC_MESSAGES/katrain.po b/katrain/i18n/locales/nl/LC_MESSAGES/katrain.po index c747d4c..0487226 100644 --- a/katrain/i18n/locales/nl/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/nl/LC_MESSAGES/katrain.po @@ -430,3 +430,87 @@ msgstr "k" #. TODO msgid "strength:dan" msgstr "d" + +#. TODO +msgid "aihelp:p:weighted" +msgstr "" +"Plays moves according to policy probability, with `weaken_fac` influencing " +"how much more likely weaker moves are played. A high negative value plays " +"like the `Policy` AI.`pick_override` determines when top move is chosen " +"without randomness, and `lower_bound` determines the lower bound policy " +"value that is allowed." + +#. TODO +msgid "aihelp:p:pick" +msgstr "" +"Picks `pick_n + pick_frac * ` at random and plays the" +" best one. For `pick_frac=1` plays like the `Policy` AI. Plays top move if " +"policy value is above `pick_override` to avoid obvious mistakes." + +#. TODO +msgid "ai:p:influence" +msgstr "Influential Style" + +#. TODO +msgid "ai:p:local" +msgstr "Respond Locally" + +#. TODO +msgid "aihelp:p:tenuki" +msgstr "" +"Picks `pick_n + pick_frac * ` weighted away from the " +"last move and plays the best one. Plays top move if policy value is above " +"`pick_override` to avoid obvious mistakes." + +#. TODO +msgid "ai:scoreloss" +msgstr "Score Loss" + +#. TODO +msgid "aihelp:p:influence" +msgstr "" +"Picks moves biased above the `threshold` line and plays the best " +"one.Increase `line_weight` to penalize moves near the edge more. Stops " +"strategy after the 'endgame' fraction of the board is filled." + +#. TODO +msgid "ai:default" +msgstr "KataGo" + +#. TODO +msgid "ai:p:tenuki" +msgstr "Tenuki Style" + +#. TODO +msgid "ai:p:pick" +msgstr "Blinded Policy" + +#. TODO +msgid "ai:p:weighted" +msgstr "Policy Weighted" + +#. TODO +msgid "ai:policy" +msgstr "Policy" + +#. TODO +msgid "ai:jigo" +msgstr "KataJigo" + +#. TODO +msgid "aihelp:p:local" +msgstr "" +"Picks moves biased below the `threshold` line and plays the best " +"one.Increase `line_weight` to penalize moves near the center more. Stops " +"strategy after the 'endgame' fraction of the board is filled." + +#. TODO +msgid "aihelp:p:territory" +msgstr "" +"Picks moves biased below the `threshold` line and plays the best " +"one.Increase `line_weight` to penalize moves near the center more. Stops " +"strategy after the 'endgame' fraction of the board is filled." + +#. TODO +msgid "ai:p:territory" +msgstr "Respond Locally" diff --git a/katrain/popups.kv b/katrain/popups.kv index 6f68b76..ce7dbcf 100644 --- a/katrain/popups.kv +++ b/katrain/popups.kv @@ -31,7 +31,6 @@ : font_size: INPUT_FONT_SIZE - on_text: print(self.size,self.font_size) : color: WHITE