From 40ae451bef5ade269035e3abbc18324c30ba3805 Mon Sep 17 00:00:00 2001 From: Sander Land Date: Mon, 25 May 2020 17:12:58 +0200 Subject: [PATCH] tweak --- i18n.py | 23 ++++--- katrain/__main__.py | 52 ++++++++------- katrain/config.json | 119 +---------------------------------- katrain/core/common.py | 17 ++--- katrain/core/game.py | 48 ++++++++++++-- katrain/core/game_node.py | 2 - katrain/gui.kv | 59 ++++++++--------- katrain/gui/badukpan.py | 91 +++++++++++++++------------ katrain/gui/controlspanel.py | 90 +++++++++++--------------- katrain/gui/graph.py | 20 +++--- katrain/gui/kivyutils.py | 59 ++++++++++------- katrain/gui/style.py | 29 +++++++++ 12 files changed, 283 insertions(+), 326 deletions(-) create mode 100644 katrain/gui/style.py diff --git a/i18n.py b/i18n.py index 65cebff..b472d74 100644 --- a/i18n.py +++ b/i18n.py @@ -4,7 +4,7 @@ from collections import defaultdict localedir = "katrain/i18n/locales" locales = set(os.listdir(localedir)) -print("locales found:",locales) +print("locales found:", locales) strings_to_langs = defaultdict(dict) lang_to_strings = defaultdict(set) @@ -14,11 +14,11 @@ DEFAULT_LANG = "en" po = {} pofile = {} for lang in locales: - pofile[lang] = os.path.join(localedir, lang, 'LC_MESSAGES', 'katrain.po') + 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: - print(lang,'/',entry.msgid,'is TODO') + if "TODO" in entry.comment: + print(lang, "/", entry.msgid, "is TODO") else: strings_to_langs[entry.msgid][lang] = entry.msgstr lang_to_strings[lang].add(entry.msgid) @@ -26,16 +26,15 @@ for lang in locales: for lang in locales: for msgid in strings_to_langs.keys() - lang_to_strings[lang]: - if lang==DEFAULT_LANG: - print("Message id",msgid,"found as ",strings_to_langs[msgid],"but missing in default",DEFAULT_LANG) + if lang == DEFAULT_LANG: + print("Message id", msgid, "found as ", strings_to_langs[msgid], "but missing in default", DEFAULT_LANG) elif DEFAULT_LANG in strings_to_langs[msgid]: - print("Message id", msgid, "missing in ",lang,'-> Adding it from',DEFAULT_LANG) - entry = polib.POEntry(msgid=msgid,msgstr=strings_to_langs[msgid][DEFAULT_LANG],comment="TODO") + print("Message id", msgid, "missing in ", lang, "-> Adding it from", DEFAULT_LANG) + entry = polib.POEntry(msgid=msgid, msgstr=strings_to_langs[msgid][DEFAULT_LANG], comment="TODO") po[lang].append(entry) else: - print(f"MISSING IN DEFAULT AND {lang}",strings_to_langs[msgid]) + print(f"MISSING IN DEFAULT AND {lang}", strings_to_langs[msgid]) po[lang].save(pofile[lang]) - mofile = pofile[lang].replace('.po', '.mo') + mofile = pofile[lang].replace(".po", ".mo") po[lang].save_as_mofile(mofile) - print('Fixed',pofile[lang],'and converted ->',mofile) - + print("Fixed", pofile[lang], "and converted ->", mofile) diff --git a/katrain/__main__.py b/katrain/__main__.py index f1c7de7..29f1846 100644 --- a/katrain/__main__.py +++ b/katrain/__main__.py @@ -1,38 +1,46 @@ from kivy.config import Config # isort:skip -from kivy.properties import ObjectProperty - -ICON = "img/icon.png" -Config.set("kivy", "window_icon", ICON) # isort:skip # set icon -Config.set("input", "mouse", "mouse,multitouch_on_demand") # isort:skip # no red dots on right click - -import signal import os +import signal import sys import threading import traceback from queue import Queue -from kivy.lang import Builder -from kivy.resources import resource_add_path - from kivy.app import App -from kivymd.app import MDApp from kivy.core.clipboard import Clipboard +from kivy.lang import Builder +from kivy.properties import ObjectProperty +from kivy.resources import resource_add_path 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.ai import ai_move -from katrain.core.common import OUTPUT_INFO, OUTPUT_ERROR, OUTPUT_DEBUG, OUTPUT_EXTRA_DEBUG, OUTPUT_KATAGO_STDERR, find_package_resource, LANGUAGE +from katrain.core.common import ( + LANGUAGE, + OUTPUT_DEBUG, + OUTPUT_ERROR, + OUTPUT_EXTRA_DEBUG, + OUTPUT_INFO, + OUTPUT_KATAGO_STDERR, + Lang, + find_package_resource, +) from katrain.core.engine import KataGoEngine from katrain.core.game import Game, IllegalMoveException, KaTrainSGF from katrain.core.sgf_parser import Move, ParseError -from katrain.gui.kivyutils import * -from katrain.gui.graph import ScoreGraph -from katrain.gui.badukpan import BadukPanWidget, AnalysisControls, BadukPanControls +from katrain.gui.badukpan import AnalysisControls, BadukPanControls, BadukPanWidget from katrain.gui.controlspanel import ControlsPanel, RightButtonControls -from katrain.gui.popups import NewGamePopup, ConfigPopup, LoadSGFPopup -from katrain.core.common import Lang +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 + +ICON = "img/icon.png" +Config.set("kivy", "window_icon", ICON) # isort:skip # set icon +Config.set("input", "mouse", "mouse,multitouch_on_demand") # isort:skip # no red dots on right click + __version__ = "1.1.0" @@ -105,7 +113,6 @@ class KaTrainGui(Screen): if self.engine: return self.board_gui.trainer_config = self.config("trainer") - self.board_gui.ui_config = self.config("board_ui") self.engine = KataGoEngine(self, self.config("engine")) threading.Thread(target=self._message_loop_thread, daemon=True).start() self._do_new_game() @@ -139,11 +146,11 @@ class KaTrainGui(Screen): # update engine status dot if not self.engine or not self.engine.katago_process or self.engine.katago_process.poll() is not None: - self.controls.engine_status_col = self.config("board_ui/engine_down_col") + self.controls.engine_status_col = ENGINE_DOWN_COL elif len(self.engine.queries) == 0: - self.controls.engine_status_col = self.config("board_ui/engine_ready_col") + self.controls.engine_status_col = ENGINE_READY_COL else: - self.controls.engine_status_col = self.config("board_ui/engine_busy_col") + self.controls.engine_status_col = ENGINE_BUSY_COL # redraw if redraw_board: @@ -203,7 +210,7 @@ class KaTrainGui(Screen): def _do_play(self, coords): self.board_gui.animating_pv = None try: - self.game.play(Move(coords, player=self.game.next_player)) + self.game.play(Move(coords, player=self.game.next_player.player)) except IllegalMoveException as e: self.controls.set_status(f"Illegal Move: {str(e)}") @@ -331,7 +338,6 @@ class KaTrainGui(Screen): return True - class KaTrainApp(MDApp): gui = ObjectProperty(None) language = StringProperty(LANGUAGE) diff --git a/katrain/config.json b/katrain/config.json index aed7b4d..702cc23 100644 --- a/katrain/config.json +++ b/katrain/config.json @@ -51,7 +51,8 @@ ], "eval_off_show_last": 3, "eval_show_ai": true, - "lock_ai": false + "lock_ai": false, + "anim_pv_time": 0.5 }, "ai": { "Default": { @@ -133,122 +134,6 @@ "_help_right": "Increase `line_weight` to penalize moves closer to the center more. Stops strategy after the 'endgame' fraction of the board is filled." } }, - "board_ui": { - "anim_pv_time": 0.5, - "engine_down_col": [ - 0.8, - 0, - 0, - 1 - ], - "engine_busy_col": [ - 0.9, - 0.4, - 0.1, - 1 - ], - "engine_little_busy_col": [ - 0.85, - 0.89, - 0.3, - 1 - ], - "engine_almost_done_col": [ - 0.67, - 0.9, - 0.18, - 1 - ], - "engine_ready_col": [ - 0.117, - 0.588, - 0, - 1 - ], - "board_color": [0.85, 0.68, 0.40,1], - "starpoint_size": 0.1, - "stone_size": 0.475, - "eval_dot_max_size": 0.5, - "eval_dot_min_size": 0.25, - "visit_frac_small": 0.1, - "stones": { - "B": [ - 0.05, - 0.05, - 0.05 - ], - "W": [ - 0.95, - 0.95, - 0.95 - ] - }, - "outline": { - "B": [ - 0.3, - 0.3, - 0.3, - 0.5 - ], - "W": [ - 0.7, - 0.7, - 0.7, - 0.5 - ] - }, - "ghost_alpha": 0.5, - "top_move_x_alpha": 0.3, - "child_scale": 0.95, - "eval_colors": [ - [ - 0.447, - 0.129, - 0.42, - 1 - ], - [ - 0.8, - 0, - 0, - 1 - ], - [ - 0.9, - 0.4, - 0.1, - 1 - ], - [ - 0.85, - 0.89, - 0.3, - 1 - ], - [ - 0.67, - 0.9, - 0.18, - 1.0 - ], - [ - 0.117, - 0.588, - 0, - 1.0 - ] - ], - "line_color": [ - 0, - 0, - 0 - ], - "policy_color": [ - 0.9, - 0.2, - 0.8 - ] - }, "debug": { "level": 1, "_hint_level": "0: Little console output.\n1: Some extra output\nfor error reporting." diff --git a/katrain/core/common.py b/katrain/core/common.py index 120cb74..ef419cd 100644 --- a/katrain/core/common.py +++ b/katrain/core/common.py @@ -17,9 +17,7 @@ OUTPUT_INFO = 0 OUTPUT_DEBUG = 1 OUTPUT_EXTRA_DEBUG = 2 -PLAYER_HUMAN = 0 -PLAYER_HUMAN_TEACHING = 1 -PLAYER_AI = 2 +MODE_PLAY, MODE_ANALYZE = "play", "analyze" def var_to_grid(array_var: List[Any], size: Tuple[int, int]) -> List[List[Any]]: @@ -63,7 +61,8 @@ class Lang(Observable): self.switch_lang(self.lang) def _(self, text): - return self.ugettext(text) + str = self.ugettext(text) + return str def fbind(self, name, func, *args, **kwargs): if name == "_": @@ -74,7 +73,7 @@ class Lang(Observable): def funbind(self, name, func, *args, **kwargs): if name == "_": key = (func, args, kwargs) - print('funbind',key in self.observers) + print("funbind", key in self.observers) if key in self.observers: self.observers.remove(key) else: @@ -92,16 +91,20 @@ class Lang(Observable): try: func(args[0], None, None) except ReferenceError: - pass # proxy no longer exists + pass # proxy no longer exists + LANGUAGE = "nl" i18n = Lang(LANGUAGE) + class I18NLabel(Label): pass + class I18NCoreLabel(CoreLabel): pass + class I18NTextInput(TextInput): - pass \ No newline at end of file + pass diff --git a/katrain/core/game.py b/katrain/core/game.py index 7b7b9b8..f54781e 100644 --- a/katrain/core/game.py +++ b/katrain/core/game.py @@ -19,15 +19,47 @@ class KaTrainSGF(SGF): _NODE_CLASS = GameNode +PLAYER_HUMAN, PLAYER_AI = "Player", "AI" +PLAYING_NORMAL, PLAYING_TEACHING = "Normal game", "Teaching game" +AI_DEFAULT = "default" + + +class Player: + def __init__(self, player="B", player_type=PLAYER_HUMAN, player_subtype=PLAYING_NORMAL, periods_used=0): + self.player = player + self.player_type = player_type + self.player_subtype = player_subtype + self.periods_used = periods_used + + @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 + + 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, "RU": "JP", "AP": "KaTrain:https://github.com/sanderland/katrain"} - def __init__(self, katrain, engine: Union[Dict, KataGoEngine], config: Dict, move_tree: GameNode = None, analyze_fast=False): + def __init__(self, katrain, engine: Union[Dict, KataGoEngine], config: Dict, 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") @@ -42,10 +74,20 @@ class Game: board_size = config.get("init_size", 19) self.komi = self.config.get("init_komi", 6.5) self.root = GameNode(properties={**Game.DEFAULT_PROPERTIES, **{"SZ": board_size, "KM": self.komi, "DT": self.game_id}}) + self.root.set_property("PW", str(self.players["W"])) + self.root.set_property("PB", str(self.players["B"])) 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) @@ -181,10 +223,6 @@ class Game: def board_size(self): return self.root.board_size - @property - def next_player(self): - return self.current_node.next_player - @property def stones(self): return sum(self.chains, []) diff --git a/katrain/core/game_node.py b/katrain/core/game_node.py index 050322b..fd86f0c 100644 --- a/katrain/core/game_node.py +++ b/katrain/core/game_node.py @@ -94,7 +94,6 @@ class GameNode(SGFNode): def analysis_ready(self): return self.analysis["root"] is not None - @property def score(self) -> Optional[float]: if self.analysis_ready: @@ -179,7 +178,6 @@ class GameNode(SGFNode): score = self.score return self.player_sign(single_move.player) * (score - parent_parent_score) - @staticmethod def player_sign(player): return {"B": 1, "W": -1, None: 0}[player] diff --git a/katrain/gui.kv b/katrain/gui.kv index 073ef65..74da3fa 100644 --- a/katrain/gui.kv +++ b/katrain/gui.kv @@ -30,9 +30,6 @@ : font_name: i18n._('Roboto') -: - font_name: i18n._('Roboto') - : font_name: i18n._('Roboto') @@ -100,11 +97,11 @@ : # only wants height width: self.label.texture_size[0] + self.hor_padding * 2 - font_size: 0.75 * self.height / len(self.label.text.split('\n')) + font_size: 0.8 * self.height / len(self.label.text.split('\n')) : # only wants height width: self.label.texture_size[0] + self.hor_padding * 2 - font_size: 0.75 * self.height / len(self.label.text.split('\n')) + font_size: 0.8 * self.height / len(self.label.text.split('\n')) : background_color: BOX_BACKGROUND_COLOR @@ -253,19 +250,19 @@ : min_size: min(self.height,self.width) Image: - pos_hint: {'center_x': 0.5, 'center_y': 0.5} + id: image + pos: (root.pos[0] + (root.width - root.min_size)/2,root.pos[1] + (root.height - root.min_size)/2) size: (root.min_size,root.min_size) source: 'img/white.png' if root.player=='White' else 'img/black.png' mipmap: True - I18NLabel: + Label: + id: lbl text: root.text bold: True color: BLACK if root.player=='White' else WHITE - pos_hint: {'center_x': 0.5, 'center_y': 0.5} - size: (root.min_size,root.min_size) - halign: 'center' - valign: 'middle' - font_size: root.min_size * 1.2 / (1 + 1 * len(self.text)) # TODO: easier? + pos: (image.x, image.y+2) + size: image.size + font_size: root.min_size * 1.2 / (1 + 1 * len(self.text)) # UI components @@ -387,8 +384,6 @@ # ---------- components of right pane ------- - - : captures: 0 name: 'Player' @@ -548,26 +543,24 @@ opacity: int(root.show_winrate) - + orientation: 'vertical' background_color: BOX_BACKGROUND_COLOR - score: score - winrate: winrate - score_change: score_change + labels: {'score':score_label,'winrate':winrate_label,'points':pointloss_label} CensorableLabel: - id: winrate + id: winrate_label label: 'Win rate' - text: '' + text: root.winrate color: WINRATE_COLOR CensorableLabel: - id: score + id: score_label label: 'Score' - text: '' + text: root.score color: SCORE_COLOR CensorableLabel: - id: score_change - label: 'Points Lost' - text: '' + id: pointloss_label + label: 'Points Lost' if root.points_lost is None or root.points_lost > 0 else 'Points gained' + text: '{}: {:.1f}'.format(root.player,abs(root.points_lost)) if root.points_lost is not None else '...' color: POINTLOSS_COLOR : @@ -678,7 +671,7 @@ on_ref_press: root.katrain.board_gui.show_pv_from_comments(args[1]) size_hint: 1, None height: self.parent.height - status.height - note.height - 1 - TextInput: + I18NTextInput: id: note size_hint: 1, None on_text: root.katrain.set_note(self.text) @@ -765,6 +758,11 @@ font_size: root.font_size color: GREY + + + size_hint: 1,1 + font_size: 0.4 * self.height + : katrain: app.gui background_color: BOX_BACKGROUND_COLOR @@ -783,18 +781,15 @@ Line: rectangle: (*self.pos,*self.size) width: 1 - SizedMDFlatButton: + LangButton: text: i18n._('English') on_press: app.language = 'en' - size_hint: 1/3,1 - SizedMDFlatButton: + LangButton: text: i18n._('Dutch') on_press: app.language = 'nl' - size_hint: 1/3,1 - SizedMDFlatButton: + LangButton: text: i18n._('Korean') on_press: app.language = 'ko' - size_hint: 1/3,1 Label: MDBoxLayout: canvas.before: diff --git a/katrain/gui/badukpan.py b/katrain/gui/badukpan.py index 1dfc713..30f772d 100644 --- a/katrain/gui/badukpan.py +++ b/katrain/gui/badukpan.py @@ -11,17 +11,33 @@ from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.floatlayout import MDFloatLayout from kivymd.uix.menu import MDDropdownMenu -from katrain.core.common import OUTPUT_DEBUG, evaluation_class, PLAYER_AI, i18n +from katrain.core.common import OUTPUT_DEBUG, evaluation_class, i18n, MODE_PLAY from katrain.core.game import Move from katrain.gui.kivyutils import draw_circle, draw_text from katrain.core.common import var_to_grid from kivy.core.window import Window +from katrain.gui.style import ( + EVAL_COLORS, + LINE_COLOR, + POLICY_COLOR, + GHOST_ALPHA, + TOP_MOVE_ALPHA, + STONE_COLORS, + OUTLINE_COLORS, + CHILD_SCALE, + EVAL_DOT_MIN_SIZE, + EVAL_DOT_MAX_SIZE, + BOARD_COLOR, + STARPOINT_SIZE, + STONE_SIZE, + VISITS_FRAC_SMALL, +) + class BadukPanWidget(Widget): def __init__(self, **kwargs): super(BadukPanWidget, self).__init__(**kwargs) - self.ui_config = {} self.trainer_config = {} self.ghost_stone = [] self.gridpos_x = [] @@ -124,7 +140,7 @@ class BadukPanWidget(Widget): Line(circle=(self.gridpos_x[x], self.gridpos_y[y], stone_size), width=min(2, 0.035 * stone_size)) if evalcol: eval_radius = math.sqrt(evalscale) # scale area by evalscale - evalsize = self.stone_size * (self.ui_config["eval_dot_min_size"] + eval_radius * (self.ui_config["eval_dot_max_size"] - self.ui_config["eval_dot_min_size"])) + evalsize = self.stone_size * (EVAL_DOT_MIN_SIZE + eval_radius * (EVAL_DOT_MAX_SIZE - EVAL_DOT_MIN_SIZE)) draw_circle((self.gridpos_x[x], self.gridpos_y[y]), evalsize, evalcol) if innercol: @@ -132,12 +148,12 @@ class BadukPanWidget(Widget): Line(circle=(self.gridpos_x[x], self.gridpos_y[y], stone_size * 0.475 / 0.85), width=0.1 * stone_size) def eval_color(self, points_lost): - colors = self.ui_config["eval_colors"] + colors = EVAL_COLORS i = evaluation_class(points_lost, self.trainer_config["eval_thresholds"]) return colors[i] def draw_board(self, *_args): - if not self.ui_config: + if not (self.katrain and self.katrain.game): return katrain = self.katrain board_size_x, board_size_y = katrain.game.board_size @@ -154,18 +170,17 @@ class BadukPanWidget(Widget): board_height_with_margins = y_grid_spaces * self.grid_size extra_px_margin_x = (self.width - board_width_with_margins) / 2 extra_px_margin_y = (self.height - board_height_with_margins) / 2 - self.stone_size = self.grid_size * self.ui_config["stone_size"] + self.stone_size = self.grid_size * STONE_SIZE self.gridpos_x = [self.pos[0] + extra_px_margin_x + math.floor((grid_spaces_margin_x[0] + i) * self.grid_size + 0.5) for i in range(board_size_x)] self.gridpos_y = [self.pos[1] + extra_px_margin_y + math.floor((grid_spaces_margin_y[0] + i) * self.grid_size + 0.5) for i in range(board_size_y)] - Color(*self.ui_config["board_color"]) + Color(*BOARD_COLOR) Rectangle( pos=(self.gridpos_x[0] - self.grid_size * 1.5, self.gridpos_y[0] - self.grid_size * 1.5), size=(self.grid_size * x_grid_spaces, self.grid_size * y_grid_spaces) ) - line_color = self.ui_config["line_color"] - Color(*line_color) + Color(*LINE_COLOR) for i in range(board_size_x): Line(points=[(self.gridpos_x[i], self.gridpos_y[0]), (self.gridpos_x[i], self.gridpos_y[-1])]) for i in range(board_size_y): @@ -178,10 +193,10 @@ class BadukPanWidget(Widget): return [] return [star_point_pos - 1, size - star_point_pos] + ([int(size / 2)] if size % 2 == 1 and size > 7 else []) - starpt_size = self.grid_size * self.ui_config["starpoint_size"] + starpt_size = self.grid_size * STARPOINT_SIZE for x in star_point_coords(board_size_x): for y in star_point_coords(board_size_y): - draw_circle((self.gridpos_x[x], self.gridpos_y[y]), starpt_size, line_color) + draw_circle((self.gridpos_x[x], self.gridpos_y[y]), starpt_size, LINE_COLOR) # coordinates Color(0.25, 0.25, 0.25) @@ -192,10 +207,10 @@ class BadukPanWidget(Widget): draw_text(pos=(self.gridpos_x[0] - coord_offset, self.gridpos_y[i]), text=str(i + 1), font_size=self.grid_size / 1.5) def draw_board_contents(self, *_args): - if not self.ui_config: + if not (self.katrain and self.katrain.game): return - stone_color = self.ui_config["stones"] - outline_color = self.ui_config["outline"] + stone_color = STONE_COLORS + outline_color = OUTLINE_COLORS katrain = self.katrain board_size_x, board_size_y = katrain.game.board_size lock_ai = self.trainer_config.get("lock_ai", False) and katrain.controls.teaching_mode_enabled() @@ -212,7 +227,7 @@ class BadukPanWidget(Widget): for m in katrain.game.stones: has_stone[m.coords] = m.player - show_dots_for = {p: self.trainer_config["eval_show_ai"] or katrain.controls.player_mode(p) != PLAYER_AI for p in Move.PLAYERS} + show_dots_for = {p: self.trainer_config["eval_show_ai"] or katrain.game.players[p].human for p in Move.PLAYERS} nodes = katrain.game.current_node.nodes_from_root realized_points_lost = None for i, node in enumerate(nodes[::-1]): # reverse order! @@ -257,13 +272,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.controls.player_mode("B") == PLAYER_AI - and katrain.controls.player_mode("W") == PLAYER_AI - ): + if not policy and current_node.parent and current_node.parent.policy and katrain.game.last_player.ai and katrain.game.next_player.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 @@ -276,13 +285,13 @@ class BadukPanWidget(Widget): if policy_grid[y][x] > 0: polsize = 1.1 * math.sqrt(policy_grid[y][x]) policy_circle_color = ( - *self.ui_config["policy_color"], - self.ui_config["ghost_alpha"] + self.ui_config["top_move_x_alpha"] * (policy_grid[y][x] == best_move_policy), + *POLICY_COLOR, + GHOST_ALPHA + TOP_MOVE_ALPHA * (policy_grid[y][x] == best_move_policy), ) self.draw_stone(x, y, policy_circle_color, scale=polsize) polsize = math.sqrt(policy[-1]) with pass_btn.canvas.after: - draw_circle((pass_btn.pos[0] + pass_btn.width / 2, pass_btn.pos[1] + pass_btn.height / 2), polsize * pass_btn.height / 2, self.ui_config["policy_color"]) + draw_circle((pass_btn.pos[0] + pass_btn.width / 2, pass_btn.pos[1] + pass_btn.height / 2), polsize * pass_btn.height / 2, POLICY_COLOR) # pass circle passed = len(nodes) > 1 and current_node.is_pass @@ -301,21 +310,20 @@ class BadukPanWidget(Widget): self.draw_hover_contents() def draw_hover_contents(self, *_args): - ghost_alpha = self.ui_config["ghost_alpha"] + ghost_alpha = GHOST_ALPHA katrain = self.katrain game_ended = katrain.game.ended current_node = katrain.game.current_node player, next_player = current_node.player, current_node.next_player - stone_color = self.ui_config["stones"] - teaching = katrain.controls.teaching_mode_enabled() - lock_ai = self.trainer_config.get("lock_ai", False) and teaching + stone_color = STONE_COLORS + lock_ai = self.trainer_config.get("lock_ai", False) and self.katrain.controls.play_analyze_mode == MODE_PLAY self.canvas.after.clear() with self.canvas.after: self.active_pv_moves = [] # children of current moves in undo / review - alpha = self.ui_config["ghost_alpha"] + alpha = GHOST_ALPHA if katrain.analysis_controls.show_children.active: for child_node in current_node.children: points_lost = child_node.points_lost @@ -326,10 +334,9 @@ class BadukPanWidget(Widget): else: evalcol = copy.copy(self.eval_color(points_lost)) evalcol[3] = alpha - # if ((teaching and child_node.auto_undo) or katrain.controls.play_analyze_mode == "analyze") and child_node.analysis_ready: # TODO:? if child_node.analysis_ready: self.active_pv_moves.append((move.coords, [move.gtp()] + child_node.candidate_moves[0]["pv"], current_node)) - scale = self.ui_config["child_scale"] + scale = CHILD_SCALE self.draw_stone(move.coords[0], move.coords[1], (*stone_color[move.player][:3], alpha), None, None, evalcol, evalscale=scale, scale=scale) # hints or PV @@ -338,10 +345,10 @@ class BadukPanWidget(Widget): for i, move_dict in enumerate(hint_moves): move = Move.from_gtp(move_dict["move"]) if move.coords is not None: - alpha, scale = self.ui_config["ghost_alpha"], 1.0 + alpha, scale = GHOST_ALPHA, 1.0 if i == 0: - alpha += self.ui_config["top_move_x_alpha"] - elif move_dict["visits"] < self.ui_config["visit_frac_small"] * hint_moves[0]["visits"]: + alpha += TOP_MOVE_ALPHA + elif move_dict["visits"] < VISITS_FRAC_SMALL * hint_moves[0]["visits"]: scale = 0.8 if "pv" in move_dict: self.active_pv_moves.append((move.coords, move_dict["pv"], current_node)) @@ -356,7 +363,7 @@ class BadukPanWidget(Widget): animating_pv = self.animating_pv if animating_pv: pv, node, start_time, _ = animating_pv - delay = self.ui_config.get("anim_pv_time", 0.5) + delay = self.trainer_config.get("anim_pv_time", 0.5) up_to_move = (time.time() - start_time) / delay self.draw_pv(pv, node, up_to_move) @@ -367,7 +374,7 @@ class BadukPanWidget(Widget): def draw_pv(self, pv, node, up_to_move): katrain = self.katrain next_last_player = [node.next_player, node.player] - stone_color = self.ui_config["stones"] + stone_color = STONE_COLORS cn = katrain.game.current_node if node != cn and node.parent != cn: hide_node = cn @@ -401,18 +408,20 @@ class BadukPanWidget(Widget): def show_pv_from_comments(self, pv_str): self.set_animating_pv(pv_str[1:].split(" "), self.katrain.controls.active_comment_node.parent) + class AnalysisControls(MDBoxLayout): def __init__(self, **kwargs): super().__init__(**kwargs) - self.analysis_menu=None - Clock.schedule_once(self.build_menu,0) + self.analysis_menu = None + Clock.schedule_once(self.build_menu, 0) - def build_menu(self,_dt): + def build_menu(self, _dt): menu_items = [{"icon": "git", "text": f"Item {i}"} for i in range(5)] self.analysis_menu = MDDropdownMenu(caller=self.analysis_button, items=menu_items, width_mult=4) def open_analysis_menu(self): self.analysis_menu.open() + class BadukPanControls(MDFloatLayout): - pass \ No newline at end of file + pass diff --git a/katrain/gui/controlspanel.py b/katrain/gui/controlspanel.py index d48ee88..099b772 100644 --- a/katrain/gui/controlspanel.py +++ b/katrain/gui/controlspanel.py @@ -6,7 +6,7 @@ from kivy.uix.boxlayout import BoxLayout from kivy.uix.popup import Popup from kivymd.uix.boxlayout import MDBoxLayout -from katrain.core.common import PLAYER_HUMAN, PLAYER_AI, PLAYER_HUMAN_TEACHING +from katrain.core.common import MODE_PLAY, MODE_ANALYZE from katrain.gui.popups import ConfigTeacherPopup, ConfigTimerPopup from katrain.gui.ai_settings import ConfigAIPopupContents @@ -28,7 +28,6 @@ class ControlsPanel(BoxLayout): self.active_comment_node = None self.timer_settings_popup = None self.last_timer_update = (None, 0) - self.periods_used = {"B": 0, "W": 0} Clock.schedule_interval(self.update_timer, 0.07) def check_hide_show(self, *_args): @@ -42,66 +41,47 @@ class ControlsPanel(BoxLayout): @property def play_analyze_mode(self): - return "analyze" # ?? - - def player_mode(self, player): - return PLAYER_HUMAN - - def ai_mode(self, player): - return self.ai_mode_groups[player].text - - def teaching_mode_enabled(self): - return PLAYER_HUMAN_TEACHING in [self.player_mode("B"), self.player_mode("W")] - - def on_size(self, *args): - self.update_evaluation() + return MODE_ANALYZE # ?? # handles showing completed analysis and score graph def update_evaluation(self): katrain = self.katrain - current_node = katrain and katrain.game and katrain.game.current_node - - if current_node is not self.status_node and not (self.status is not None and self.status_node is None and current_node.is_root): # startup errors on root + game = katrain and katrain.game + if not game: + return + current_node, move = game.current_node, game.current_node.move + if game.current_node is not self.status_node and not (self.status is not None and self.status_node is None and game.current_node.is_root): # startup errors on root self.status.text = "" self.status_node = None - info = "" + both_players_are_robots = all(p.ai for p in game.players.values()) + last_player_was_ai_playing_human = game.last_player.ai and game.next_player.human - if current_node: - hints = katrain.analysis_controls.hints.active - move = current_node.move - both_players_are_robots = self.player_mode(current_node.player) == PLAYER_AI and self.player_mode(current_node.next_player) == PLAYER_AI - last_player_was_human_or_both_robots = current_node.player and (self.player_mode(current_node.player) != PLAYER_AI or both_players_are_robots) - last_player_was_ai_playing_human = ( - current_node.player and self.player_mode(current_node.player) == PLAYER_AI and self.player_mode(current_node.next_player) != PLAYER_AI - ) - if last_player_was_human_or_both_robots and not current_node.is_root and move: - info += current_node.comment(teach=self.player_mode(current_node.player) == PLAYER_HUMAN_TEACHING, hints=hints) - self.active_comment_node = current_node - elif last_player_was_ai_playing_human and current_node.parent: - info += current_node.parent.comment(teach=self.player_mode(current_node.next_player) == PLAYER_HUMAN_TEACHING, hints=hints) + self.active_comment_node = current_node + if self.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: + self.active_comment_node = current_node.children[-1] + elif current_node.parent: self.active_comment_node = current_node.parent - if current_node.analysis_ready: - self.stats.score.text = current_node.format_score() - self.stats.winrate.text = current_node.format_winrate() - if move and last_player_was_human_or_both_robots: # don't immediately hide this when an ai moves comes in - points_lost = current_node.points_lost - self.stats.score_change.label = f"Points lost" if points_lost and points_lost > 0 else f"Points gained" - self.stats.score_change.text = f"{move.player}: {abs(points_lost):.1f}" if points_lost else "-" - elif not last_player_was_ai_playing_human: - self.stats.score_change.label = f"Points lost" - self.stats.score_change.text = "-" - elif last_player_was_ai_playing_human and current_node.parent and current_node.parent.move: - points_lost = current_node.parent.points_lost - self.stats.score_change.label = f"Points lost" if points_lost and points_lost > 0 else f"Points gained" - self.stats.score_change.text = f"{current_node.parent.move.player}: {abs(points_lost):.1f}" if points_lost else "-" - elif both_players_are_robots and current_node.parent and current_node.parent.analysis_ready: - self.stats.score.text = current_node.parent.format_score() - self.stats.winrate.text = current_node.parent.format_winrate() + 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) - self.graph.update_value(current_node) - self.note.text = current_node.note + if self.active_comment_node.analysis_ready: + 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 + self.stats.player = self.active_comment_node.player + else: + self.stats.score = "" + self.stats.winrate = "" + self.stats.points_lost = None + self.stats.player = "" + + self.graph.update_value(current_node) + self.note.text = current_node.note self.info.text = info def configure_ais(self): @@ -122,10 +102,10 @@ class ControlsPanel(BoxLayout): last_update_node, last_update_time = self.last_timer_update now = time.time() self.last_timer_update = (current_node, now) - player = current_node.next_player byo_len = max(1, self.katrain.config("timer/byo_length")) byo_num = max(1, self.katrain.config("timer/byo_num")) - ai = self.player_mode(player) == PLAYER_AI + player = self.katrain.game.next_player + ai = player.ai if not self.timer.paused and not ai: if last_update_node == current_node and not current_node.children: current_node.time_used += now - last_update_time @@ -135,9 +115,9 @@ class ControlsPanel(BoxLayout): while time_remaining < 0: current_node.time_used -= byo_len time_remaining += byo_len - self.periods_used[player] += 1 + player.periods_used += 1 time_remaining = byo_len - current_node.time_used - periods_rem = byo_num - self.periods_used[player] + periods_rem = byo_num - player.periods_used self.timer.state = (time_remaining, periods_rem, ai) def configure_timer(self): diff --git a/katrain/gui/graph.py b/katrain/gui/graph.py index 2109c8c..09f262d 100644 --- a/katrain/gui/graph.py +++ b/katrain/gui/graph.py @@ -33,10 +33,10 @@ class ScoreGraph(BackgroundColor): self.nodes.append(node) self.highlighted_index = 0 - def show_graphs(self,keys): - self.show_score = keys['score'] - self.show_winrate = keys['winrate'] - self.show_pointloss = keys['points'] + def show_graphs(self, keys): + self.show_score = keys["score"] + self.show_winrate = keys["winrate"] + self.show_pointloss = keys["points"] def update_graph(self, *args): nodes = self.nodes @@ -45,21 +45,23 @@ class ScoreGraph(BackgroundColor): score_nn_values = [n.score for n in nodes if n and n.score] score_values_range = min(score_nn_values or [0]), max(score_nn_values or [0]) - winrate_values = [(n.winrate-0.5)*100 if n and n.winrate else math.nan for n in nodes] - winrate_nn_values = [(n.winrate-0.5)*100 for n in nodes if n and n.winrate] + winrate_values = [(n.winrate - 0.5) * 100 if n and n.winrate else math.nan for n in nodes] + winrate_nn_values = [(n.winrate - 0.5) * 100 for n in nodes if n and n.winrate] winrate_values_range = min(winrate_nn_values or [0]), max(winrate_nn_values or [0]) score_granularity = 5 winrate_granularity = 10 - self.score_scale = max(math.ceil( max(-score_values_range[0], score_values_range[1]) / score_granularity),1) * score_granularity + self.score_scale = max(math.ceil(max(-score_values_range[0], score_values_range[1]) / score_granularity), 1) * score_granularity self.winrate_scale = max(math.ceil(max(-winrate_values_range[0], winrate_values_range[1]) / winrate_granularity), 1) * winrate_granularity xscale = self.width / max(len(score_values) - 1, 15) available_height = self.height score_line_points = [[self.pos[0] + i * xscale, self.pos[1] + self.height / 2 + available_height / 2 * (val / self.score_scale)] for i, val in enumerate(score_values)] - winrate_line_points = [[self.pos[0] + i * xscale, self.pos[1] + self.height / 2 + available_height / 2 * (val / self.winrate_scale)] for i, val in enumerate(winrate_values)] + winrate_line_points = [ + [self.pos[0] + i * xscale, self.pos[1] + self.height / 2 + available_height / 2 * (val / self.winrate_scale)] for i, val in enumerate(winrate_values) + ] self.score_points = sum(score_line_points, []) - self.winrate_points = sum(winrate_line_points,[]) + self.winrate_points = sum(winrate_line_points, []) if self.highlighted_index is not None: self.highlighted_index = min(self.highlighted_index, len(score_values) - 1) diff --git a/katrain/gui/kivyutils.py b/katrain/gui/kivyutils.py index f772c09..a0cccff 100644 --- a/katrain/gui/kivyutils.py +++ b/katrain/gui/kivyutils.py @@ -23,13 +23,14 @@ from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.button import BasePressedButton, BaseFlatButton from kivymd.uix.floatlayout import MDFloatLayout from kivymd.uix.navigationdrawer import MDNavigationDrawer - -from katrain.core.common import i18n, I18NLabel, I18NTextInput, I18NCoreLabel +from kivy.core.text import Label as CoreLabel +from katrain.core.common import i18n, I18NTextInput, I18NLabel # # --- new mixins + class BackgroundColor(Widget): background_color = ListProperty([1, 1, 1, 0]) @@ -89,14 +90,16 @@ class SizedMDFlatRectangleToggleButton(SizedMDFlatRectangleButton, ToggleButtonB return self.state == "down" return self.state == "down" + class AutoSizedMDFlatRectangleButton(SizedMDFlatRectangleButton): hor_padding = NumericProperty(3) + class AutoSizedMDFlatRectangleToggleButton(SizedMDFlatRectangleToggleButton): hor_padding = NumericProperty(3) -class TransparentIconButton(CircularRippleBehavior,Button): +class TransparentIconButton(CircularRippleBehavior, Button): icon_size = ListProperty([25, 25]) icon = StringProperty("") @@ -123,7 +126,7 @@ class MyNavigationDrawer(MDNavigationDrawer): # in PR - closes NavDrawer on any return super().on_touch_up(touch) -class CircleWithText(MDFloatLayout): +class CircleWithText(Widget): text = StringProperty("0") player = OptionProperty("Black", options=["Black", "White"]) min_size = NumericProperty(50) @@ -156,7 +159,7 @@ class CollapsablePanel(MDBoxLayout): option_labels = ListProperty([]) option_default_active = ListProperty([]) option_colors = ListProperty([]) - closed_label = StringProperty('Closed Panel') + closed_label = StringProperty("Closed Panel") size_hint_y_open = NumericProperty(1) height_open = NumericProperty(None) @@ -170,44 +173,48 @@ class CollapsablePanel(MDBoxLayout): self.option_buttons = [] super().__init__(**kwargs) self.orientation = "vertical" - self.bind(options=self.build_options, option_colors=self.build_options, options_height=self.build_options, - option_default_active=self.build_options,options_left_padding=self.build_options) - self.bind(state=self.build,size_hint_y_open=self.build,height_open=self.build) + self.bind( + options=self.build_options, + option_colors=self.build_options, + options_height=self.build_options, + option_default_active=self.build_options, + options_left_padding=self.build_options, + ) + self.bind(state=self.build, size_hint_y_open=self.build, height_open=self.build) self.build_options() def build_options(self, *args, **kwargs): - self.header = MDBoxLayout(height=self.options_height,size_hint_y=None, - padding=[self.options_left_padding, 0, 0, 0], spacing=3) + self.header = MDBoxLayout(height=self.options_height, size_hint_y=None, padding=[self.options_left_padding, 0, 0, 0], spacing=3) self.option_buttons = [] option_labels = self.option_labels or [i18n._(f"tab:{opt}") for opt in self.options] for lbl, opt_col, active in zip(option_labels, self.option_colors, self.option_default_active): - button = AutoSizedMDFlatRectangleToggleButton(text=lbl,color=opt_col, height=self.options_height, - on_press=self.trigger_select, state="down" if active else "normal") + button = AutoSizedMDFlatRectangleToggleButton(text=lbl, color=opt_col, height=self.options_height, on_press=self.trigger_select, state="down" if active else "normal") self.option_buttons.append(button) - self.open_close_button = TransparentIconButton( # << / >> collapse button + self.open_close_button = TransparentIconButton( # << / >> collapse button icon=self.open_close_icon(), icon_size=[0.5 * self.options_height, 0.5 * self.options_height], - width=0.75*self.options_height,size_hint_x=None, - on_press=lambda *_args: self.set_state('toggle'), + width=0.75 * self.options_height, + size_hint_x=None, + on_press=lambda *_args: self.set_state("toggle"), ) - self.bind(state=lambda *_args: self.open_close_button.setter("icon")(None,self.open_close_icon())) + self.bind(state=lambda *_args: self.open_close_button.setter("icon")(None, self.open_close_icon())) self.build() def build(self, *args, **kwargs): self.header.clear_widgets() - if self.state=='open': + if self.state == "open": for button in self.option_buttons: self.header.add_widget(button) - self.header.add_widget(Label()) # spacer + self.header.add_widget(Label()) # spacer self.trigger_select() else: - self.header.add_widget(I18NLabel(text=i18n._(self.closed_I18NLabel),halign='right',height=self.options_height)) + self.header.add_widget(I18NLabel(text=i18n._(self.closed_label), halign="right", height=self.options_height)) self.header.add_widget(self.open_close_button) super().clear_widgets() super().add_widget(self.header) height, size_hint_y = 1, None - if self.state=='open' and self.contents: + if self.state == "open" and self.contents: super().add_widget(self.contents) if self.height_open: height = self.height_open @@ -215,7 +222,7 @@ class CollapsablePanel(MDBoxLayout): size_hint_y = self.size_hint_y_open else: height = self.header.height - self.height, self.size_hint_y = height,size_hint_y + self.height, self.size_hint_y = height, size_hint_y def open_close_icon(self): return self.open_icon if self.state == "open" else self.close_icon @@ -236,12 +243,18 @@ class CollapsablePanel(MDBoxLayout): def trigger_select(self, *_args): if self.state == "open": - self.dispatch("on_option_state",{opt:btn.state == "down" for opt,btn in zip(self.options,self.option_buttons)}) + self.dispatch("on_option_state", {opt: btn.state == "down" for opt, btn in zip(self.options, self.option_buttons)}) def on_option_state(self, options): pass +class StatsBox(MDBoxLayout, BackgroundColor): + winrate = StringProperty("...") + score = StringProperty("...") + points_lost = NumericProperty(None, allownone=True) + player = StringProperty("") + # --- not checked @@ -469,7 +482,7 @@ class LabelledIntInput(LabelledTextInput): def draw_text(pos, text, **kw): - label = I18NCoreLabel(text=text, bold=True, **kw) + label = CoreLabel(text=text, bold=True, font_name=i18n._("Roboto"), **kw) # label.refresh() Rectangle(texture=label.texture, pos=(pos[0] - label.texture.size[0] / 2, pos[1] - label.texture.size[1] / 2), size=label.texture.size) diff --git a/katrain/gui/style.py b/katrain/gui/style.py new file mode 100644 index 0000000..c036e6c --- /dev/null +++ b/katrain/gui/style.py @@ -0,0 +1,29 @@ +# eval dots +EVAL_COLORS = [ + [0.447, 0.129, 0.42, 1], + [0.8, 0, 0, 1], + [0.9, 0.4, 0.1, 1], + [0.85, 0.89, 0.3, 1], + [0.67, 0.9, 0.18, 1.0], + [0.117, 0.588, 0, 1.0], +] +EVAL_DOT_MAX_SIZE = 0.5 +EVAL_DOT_MIN_SIZE = 0.25 + +# board +LINE_COLOR = [0, 0, 0] +POLICY_COLOR = [0.9, 0.2, 0.8] +STARPOINT_SIZE = 0.1 +BOARD_COLOR = [0.85, 0.68, 0.40, 1] +STONE_SIZE = 0.475 +VISITS_FRAC_SMALL = 0.1 +STONE_COLORS = {"B": [0.05, 0.05, 0.05], "W": [0.95, 0.95, 0.95]} +OUTLINE_COLORS = {"B": [0.3, 0.3, 0.3, 0.5], "W": [0.7, 0.7, 0.7, 0.5]} +GHOST_ALPHA = 0.5 +TOP_MOVE_ALPHA = 0.3 +CHILD_SCALE = 0.95 + +# ponder dot +ENGINE_DOWN_COL = [0.8, 0, 0, 1] +ENGINE_BUSY_COL = [0.9, 0.4, 0.1, 1] +ENGINE_READY_COL = [0.117, 0.588, 0, 1]