diff --git a/katrain/__main__.py b/katrain/__main__.py index 24adeaf..61ec7c7 100644 --- a/katrain/__main__.py +++ b/katrain/__main__.py @@ -256,6 +256,7 @@ class KaTrainGui(Screen, KaTrainBase): player_info.name = move_tree.root.get_property("P" + bw) self.update_player(bw) self.controls.graph.initialize_from_game(self.game.root) + self.controls.rank_graph.initialize_from_game(self.game.root) self.update_state(redraw_board=True) def _do_ai_move(self, node=None): diff --git a/katrain/core/engine.py b/katrain/core/engine.py index 69065be..d3a400c 100644 --- a/katrain/core/engine.py +++ b/katrain/core/engine.py @@ -101,12 +101,15 @@ class KataGoEngine: self.shutdown(finish=False) self.start() - def check_alive(self, exception_if_dead=False): + def check_alive(self, os_error='', exception_if_dead=False): ok = self.katago_process and self.katago_process.poll() is None if not ok and exception_if_dead: - raise EngineDiedException( - f"Engine died (process {self.katago_process}, poll {self.katago_process and self.katago_process.poll()}) config {self.config}" - ) + if self.katago_process: + os_error += f"status {self.katago_process and self.katago_process.poll()}" + died_msg = i18n._("Engine died unexpectedly").format(error=os_error) + self.katrain.log(died_msg, OUTPUT_ERROR) + self.katago_process = None + raise EngineDiedException(died_msg) return ok def shutdown(self, finish=False): @@ -134,15 +137,22 @@ class KataGoEngine: self.katrain.log(line.decode(errors="ignore").strip(), OUTPUT_KATAGO_STDERR) except Exception as e: print("ERROR in processing KataGo stderr:", line, "Exception", e) - except: + else: + self.check_alive(exception_if_dead=True) + except Exception as e: + self.katrain.log(f"Exception in reading stdout {e}" , OUTPUT_DEBUG) return def _analysis_read_thread(self): while self.katago_process is not None: try: line = self.katago_process.stdout.readline() + if not line: + self.check_alive(exception_if_dead=True) except OSError as e: - raise EngineDiedException(i18n("Engine died unexpectedly").format(error=e)) + self.check_alive(os_error=str(e),exception_if_dead=True) + return + if b"Uncaught exception" in line: self.katrain.log(f"KataGo Engine Failed: {line.decode(errors='ignore')}", OUTPUT_ERROR) return @@ -193,7 +203,7 @@ class KataGoEngine: self.katago_process.stdin.write((json.dumps(query) + "\n").encode()) self.katago_process.stdin.flush() except OSError as e: - self.katrain.log(i18n._("Engine died unexpectedly").format(error=e), OUTPUT_ERROR) + self.check_alive(os_error=str(e),exception_if_dead=True) return # do not raise, since there's nothing to catch it def request_analysis( diff --git a/katrain/core/game.py b/katrain/core/game.py index 77f6ff5..3f715ca 100644 --- a/katrain/core/game.py +++ b/katrain/core/game.py @@ -7,10 +7,18 @@ from typing import Dict, List, Optional, Union from kivy.clock import Clock -from katrain.core.constants import HOMEPAGE, OUTPUT_DEBUG, OUTPUT_INFO, STATUS_ANALYSIS, STATUS_INFO, STATUS_TEACHING +from katrain.core.constants import ( + HOMEPAGE, + OUTPUT_DEBUG, + OUTPUT_INFO, + STATUS_ANALYSIS, + STATUS_INFO, + STATUS_TEACHING, + PLAYER_AI, +) from katrain.core.engine import KataGoEngine from katrain.core.game_node import GameNode -from katrain.core.lang import i18n +from katrain.core.lang import i18n, rank_label from katrain.core.sgf_parser import SGF, Move from katrain.core.utils import var_to_grid @@ -270,12 +278,14 @@ class Game: def player_name(player_info): return f"{i18n._(player_info.player_type)} ({i18n._(player_info.player_subtype)})" - player_names = { - bw: re.sub( - r"['<>:\"/\\|?*]", "", self.root.get_property("P" + bw) or player_name(self.katrain.players_info[bw]) - ) - for bw in "BW" - } + if "KaTrain" in self.root.get_property("AP", ""): + for bw in "BW": + self.root.set_property("P" + bw, player_name(self.katrain.players_info[bw])) + player_info = self.katrain.players_info[bw] + if player_info.player_type == PLAYER_AI: + self.root.set_property(bw + "R", rank_label(player_info.calculated_rank)) + + player_names = {bw: re.sub(r"['<>:\"/\\|?*]", "", self.root.get_property("P" + bw, 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/lang.py b/katrain/core/lang.py index af3299c..514c25d 100644 --- a/katrain/core/lang.py +++ b/katrain/core/lang.py @@ -85,4 +85,4 @@ def rank_label(rank): if rank > 0: return f"{rank:.0f}{i18n._('strength:dan')}" else: - return f"{1-rank:.0f}{i18n._('strength:kyu')}" \ No newline at end of file + return f"{1-rank:.0f}{i18n._('strength:kyu')}" diff --git a/katrain/gui/style.py b/katrain/gui/style.py index 1d84822..b26d34e 100644 --- a/katrain/gui/style.py +++ b/katrain/gui/style.py @@ -31,8 +31,8 @@ EVAL_COLORS = [ [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], + [0.67, 0.9, 0.18, 1], + [0.117, 0.588, 0, 1], ] EVAL_DOT_MAX_SIZE = 0.5 EVAL_DOT_MIN_SIZE = 0.25 diff --git a/katrain/gui/widgets/graph.py b/katrain/gui/widgets/graph.py index dc7ee6f..00ff4e3 100644 --- a/katrain/gui/widgets/graph.py +++ b/katrain/gui/widgets/graph.py @@ -4,23 +4,25 @@ import threading from kivy.lang import Builder from kivy.metrics import dp from kivy.properties import BooleanProperty, Clock, ListProperty, NumericProperty, StringProperty +from kivy.uix.widget import Widget from kivymd.app import MDApp from katrain.core.constants import OUTPUT_ERROR from katrain.core.lang import rank_label -from katrain.gui.kivyutils import BackgroundMixin -class Graph(BackgroundMixin): +class Graph(Widget): marker_font_size = NumericProperty(0) background_image = StringProperty("img/graph_bg.png") - + background_color = ListProperty([1, 1, 1, 1]) + highlighted_index = NumericProperty(0) nodes = ListProperty([]) hidden = BooleanProperty(False) def __init__(self, **kwargs): super().__init__(**kwargs) self._lock = threading.Lock() + self.redraw_on_highlight_change = True self.bind(pos=self.update_graph, size=self.update_graph) self.redraw_trigger = Clock.create_trigger(self.update_graph, 0.1) @@ -31,19 +33,24 @@ class Graph(BackgroundMixin): node = node.ordered_children[0] self.nodes.append(node) self.highlighted_index = 0 + self.redraw_trigger() def update_graph(self, *args): pass def update_value(self, node): with self._lock: + changed = (self.redraw_on_highlight_change and self.highlighted_index != node.depth) self.highlighted_index = index = node.depth self.nodes.extend([None] * max(0, index - (len(self.nodes) - 1))) + if self.nodes[index]!=node: + changed=True self.nodes[index] = node - if index > 1 and node.parent: # sometimes things go so fast + if index > 1 and node.parent: # sometimes there are gaps backfill, bfnode = index - 1, node.parent while bfnode is not None and self.nodes[backfill] != bfnode: self.nodes[backfill] = bfnode + changed = True backfill -= 1 bfnode = bfnode.parent @@ -51,11 +58,13 @@ class Graph(BackgroundMixin): node is None or not node.children or self.nodes[index + 1] != node.ordered_children[0] ): self.nodes = self.nodes[: index + 1] # on branch switching, don't show history from other branch + changed = True if index == len(self.nodes) - 1: # possibly just switched branch or the line above triggered while node.children: # add children back node = node.ordered_children[0] self.nodes.append(node) - self.redraw_trigger() + if changed: + self.redraw_trigger() class ScoreGraph(Graph): @@ -67,7 +76,6 @@ class ScoreGraph(Graph): score_dot_pos = ListProperty([0, 0]) winrate_dot_pos = ListProperty([0, 0]) - highlighted_index = NumericProperty(None) highlight_size = NumericProperty(dp(6)) score_scale = NumericProperty(5) @@ -173,9 +181,11 @@ class RankGraph(Graph): black_rank_points = ListProperty([]) white_rank_points = ListProperty([]) segment_length = NumericProperty(80) + RANK_CAP = 5 def __init__(self, **kwargs): super().__init__(**kwargs) + self.redraw_on_highlight_change = False self.calculate_trigger = Clock.create_trigger(lambda *args: self.rank_game(), 0.25) self.rank_by_player = {} @@ -189,11 +199,7 @@ class RankGraph(Graph): if not non_obvious_moves: return None num_legal, rank, value = zip(*non_obvious_moves) - rank = list(rank) - for (i, item) in enumerate(rank): - if item > num_legal[i] * 0.09: - rank[i] = num_legal[i] * 0.09 - rank = tuple(rank) + rank = [min(r, nl * 0.09) for r, nl in zip(rank, num_legal)] averagemod_rank = averagemod(rank) averagemod_len_legal = averagemod(num_legal) norm_avemod_len_legal = averagemod_len_legal / num_intersec @@ -209,9 +215,7 @@ class RankGraph(Graph): ) else: rank_kyu = -4 - if rank_kyu < -4: - rank_kyu = -4 - return 1 - rank_kyu # dan rank + return min(RankGraph.RANK_CAP,1 - rank_kyu) # dan rank @staticmethod def calculate_ranks(segment_stats, num_intersec): @@ -241,10 +245,11 @@ class RankGraph(Graph): ranks = {"B": [], "W": []} dx = self.segment_length // 4 - for segment_mid in range(0, len(nodes), dx): + for segment_mid in range(dx, len(nodes), dx): bounds = (max(0, segment_mid - half_seg), min(segment_mid + half_seg, len(nodes))) - for pl, rank in self.calculate_ranks(policy_stats[bounds[0] : bounds[1] + 1], num_intersec).items(): - if bounds[1] - bounds[0] > self.segment_length * 0.75: + num_analyzed = sum(num_mv is not None for num_mv, _, _, _ in policy_stats[bounds[0] : bounds[1] + 1]) + if num_analyzed >= self.segment_length * 0.75: + for pl, rank in self.calculate_ranks(policy_stats[bounds[0] : bounds[1] + 1], num_intersec).items(): ranks[pl].append((segment_mid, rank)) self.rank_by_player = ranks except Exception as e: @@ -270,15 +275,17 @@ class RankGraph(Graph): min_rank = math.floor(min(all_ranks)) max_rank = math.ceil(max(all_ranks)) + if max_rank==min_rank: + min_rank -= 1 if (max_rank - min_rank) % 2 != 0: # make midpoint whole integer - if abs(max_rank - max(all_ranks)) < abs(min(all_ranks) - min_rank): + if abs(max_rank - max(all_ranks)) < abs(min(all_ranks) - min_rank) and max_rank < self.RANK_CAP: max_rank += 1 else: min_rank -= 1 rank_range = max_rank - min_rank self.ids.mid_marker.text = rank_label((max_rank + min_rank) / 2) - self.ids.top_marker.text = rank_label(max_rank) + self.ids.top_marker.text = rank_label(max_rank) + ("+" if max_rank==self.RANK_CAP else "") self.ids.bottom_marker.text = rank_label(min_rank) graph_points = {} @@ -292,12 +299,16 @@ class RankGraph(Graph): ] self.black_rank_points = sum(graph_points["B"], []) self.white_rank_points = sum(graph_points["W"], []) - + else: + self.black_rank_points = [] + self.white_rank_points = [] Builder.load_string( """ #:set GRAPH_CENTER_COLOR [0.5,0.5,0.5] -#:set GRAPH_DOT_COLOR [0.85, 0.3, 0.3,1] +#:set GRAPH_DOT_COLOR [0.85,0.3,0.3,1] +#:set WINRATE_MARKER_COLOR [0.05, 0.7, 0.05, 1] +#:set SCORE_MARKER_COLOR [0.2, 0.6, 0.8, 1] #:import LIGHTER_BACKGROUND_COLOR katrain.gui.style.LIGHTER_BACKGROUND_COLOR #:import BOX_BACKGROUND_COLOR katrain.gui.style.BOX_BACKGROUND_COLOR @@ -307,11 +318,15 @@ Builder.load_string( #:import WHITE katrain.gui.style.WHITE #:import YELLOW katrain.gui.style.YELLOW - : - background_color: BOX_BACKGROUND_COLOR + background_color: BOX_BACKGROUND_COLOR marker_font_size: 0.1 * self.height - canvas: + canvas.before: + Color: + rgba: root.background_color + Rectangle: + size: self.size + pos: self.pos Color: rgba: [1,1,1,1] Rectangle: @@ -320,7 +335,7 @@ Builder.load_string( source: root.background_image : - canvas.after: + canvas: Color: rgba: SCORE_COLOR Line: @@ -351,39 +366,39 @@ Builder.load_string( # score ticks GraphMarkerLabel: font_size: root.marker_font_size - color: SCORE_COLOR + color: SCORE_MARKER_COLOR pos: root.x + root.width - self.width-1, root.pos[1]+root.height - self.font_size - 1 text: 'B+{}'.format(root.score_scale) opacity: int(root.show_score) GraphMarkerLabel: font_size: root.marker_font_size - color: SCORE_COLOR + color: SCORE_MARKER_COLOR pos: root.x + root.width - self.width-1, root.y + root.height*0.5 - self.height/2 + 2 text: i18n._('Jigo') opacity: int(root.show_score) GraphMarkerLabel: font_size: root.marker_font_size - color: SCORE_COLOR + color: SCORE_MARKER_COLOR pos: root.x + root.width - self.width-1, root.pos[1] text: 'W+' + str(int(root.score_scale)) opacity: int(root.show_score) # wr ticks GraphMarkerLabel: font_size: root.marker_font_size - color: WINRATE_COLOR + color: WINRATE_MARKER_COLOR pos: root.pos[0]+1, root.pos[1] + root.height - self.font_size - 1 text: "{}%".format(50 + root.winrate_scale) opacity: int(root.show_winrate) GraphMarkerLabel: font_size: root.marker_font_size - color: WINRATE_COLOR + color: WINRATE_MARKER_COLOR pos:root.pos[0]+1, root.pos[1] text: "{}%".format(50 - root.winrate_scale) opacity: int(root.show_winrate) : background_color: LIGHTER_BACKGROUND_COLOR - canvas.after: + canvas: Color: rgba: WHITE Line: diff --git a/katrain/i18n/locales/es/LC_MESSAGES/katrain.po b/katrain/i18n/locales/es/LC_MESSAGES/katrain.po index 14c7895..1f7e8a9 100644 --- a/katrain/i18n/locales/es/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/es/LC_MESSAGES/katrain.po @@ -635,4 +635,3 @@ msgstr "Estimated Strength" #. TODO - label in little popup for analyzing entire game msgid "reanalyze max visits" msgstr "Number of visits per move" - diff --git a/katrain/img/play.png b/katrain/img/play.png index 622e740..a81e182 100644 Binary files a/katrain/img/play.png and b/katrain/img/play.png differ