diff --git a/README.md b/README.md index e6f128e..04c931e 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ In addition to shortcuts mentioned above and those shown in the main menu: * I am looking for contributors of more translations of both this manual and the program itself. The best way to help with this is to contact me on discord. * You also can contact me on [discord](https://discord.gg/AjTPFpN) (Sander#3278), [KakaoTalk](https://open.kakao.com/o/gTsMJCac) or [Reddit](http://reddit.com/u/sanderbaduk) to get help, discuss improvements, or simply show your appreciation. -* Donations for improving and promoting KaTrain are taken supported through [Liberapay](https://liberapay.com/KaTrain/). +* Donations for improving and promoting KaTrain are taken supported through [Liberapay](https://liberapay.com/KaTrain/) or [Github Sponsors](https://github.com/sponsors/sanderland). diff --git a/katrain/__main__.py b/katrain/__main__.py index db57470..a60d4c7 100644 --- a/katrain/__main__.py +++ b/katrain/__main__.py @@ -225,7 +225,7 @@ class KaTrainGui(Screen, KaTrainBase): def _do_new_game(self, move_tree=None, analyze_fast=False): mode = self.play_analyze_mode if (move_tree is None and mode != MODE_PLAY) or (move_tree is None and mode != MODE_ANALYZE): - self.play_mode.switch_ui_mode() # for new game, go to play, for loaded, analyze + self.play_mode.switch_ui_mode() # for new game, go to play, for loaded, analyze 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) @@ -253,9 +253,13 @@ class KaTrainGui(Screen, KaTrainBase): self.board_gui.animating_pv = None self.game.redo(n_times) + def _do_cycle_children(self, *args): + self.board_gui.animating_pv = None + self.game.cycle_children(*args) + def _do_switch_branch(self, *args): self.board_gui.animating_pv = None - self.game.switch_branch(*args) + self.controls.move_tree.switch_branch(*args) def _do_play(self, coords): self.board_gui.animating_pv = None @@ -367,7 +371,11 @@ class KaTrainGui(Screen, KaTrainBase): self.log("Imported game from clipboard.", OUTPUT_INFO) def on_touch_up(self, touch): - if self.board_gui.collide_point(*touch.pos) or self.board_controls.collide_point(*touch.pos): + if ( + self.board_gui.collide_point(*touch.pos) + or self.board_controls.collide_point(*touch.pos) + or self.controls.move_tree.collide_point(*touch.pos) + ): if touch.button == "scrollup": self("redo") elif touch.button == "scrolldown": diff --git a/katrain/core/game.py b/katrain/core/game.py index 182a2fd..58f1e81 100644 --- a/katrain/core/game.py +++ b/katrain/core/game.py @@ -86,7 +86,9 @@ class Game: try: for node in self.current_node.nodes_from_root: for m in node.move_with_placements: - self._validate_move_and_update_chains(m, True) # ignore ko since we didn't know if it was forced + self._validate_move_and_update_chains( + m, True + ) # ignore ko since we didn't know if it was forced except IllegalMoveException as e: raise Exception(f"Unexpected illegal move ({str(e)})") @@ -163,7 +165,6 @@ class Game: cn = self.current_node # avoid race conditions for _ in range(n_times): if not cn.is_root: - cn.parent.set_favourite_child(cn) cn = cn.parent self.set_current_node(cn) @@ -171,18 +172,14 @@ class Game: cn = self.current_node # avoid race conditions for _ in range(n_times): if cn.children: - cn = cn.favourite_child + cn = cn.order_children[0] self.set_current_node(cn) - def switch_branch(self, direction, cycle=False): + def cycle_children(self, direction): cn = self.current_node # avoid race conditions if cn.parent and len(cn.parent.children) > 1: - ordered_children = GameNode.order_children(cn.parent.children) - ix = ordered_children.index(cn) + direction - if cycle: - ix = (ix + len(ordered_children)) % len(ordered_children) - elif ix < 0 or ix >= len(ordered_children): - return + ordered_children = cn.parent.ordered_children + ix = (ordered_children.index(cn) + len(ordered_children) + direction) % len(ordered_children) self.set_current_node(ordered_children[ix]) def place_handicap_stones(self, n_handicaps): diff --git a/katrain/core/game_node.py b/katrain/core/game_node.py index d832775..f3eae5b 100644 --- a/katrain/core/game_node.py +++ b/katrain/core/game_node.py @@ -22,10 +22,7 @@ class GameNode(SGFNode): self.move_number = 0 self.time_used = 0 self.analysis_visits_requested = 0 - self.undo_threshold = ( - random.random() - ) # for fractional undos, store the random threshold in the move itself for consistency - self._favourite_child = None + self.undo_threshold = random.random() # for fractional undos def sgf_properties(self, save_comments_player=None, save_comments_class=None, eval_thresholds=None): properties = copy.copy(super().sgf_properties()) @@ -70,16 +67,6 @@ class GameNode(SGFNode): children, key=lambda c: 0.5 if c.auto_undo is None else int(c.auto_undo) ) # analyzed/not undone main, non-teach second, undone last - def set_favourite_child(self, c): - self._favourite_child = c - - @property - def favourite_child(self) -> Optional["GameNode"]: - if self._favourite_child: - return self._favourite_child - elif self.children: - return self.children[0] - # various analysis functions def analyze(self, engine, priority=0, visits=None, time_limit=True, refine_move=None, analyze_fast=False): if visits and not refine_move: diff --git a/katrain/core/sgf_parser.py b/katrain/core/sgf_parser.py index 68624c4..8574139 100644 --- a/katrain/core/sgf_parser.py +++ b/katrain/core/sgf_parser.py @@ -91,6 +91,10 @@ class SGFNode: """For hooking into in a subclass and overriding branch order.""" return children + @property + def ordered_children(self): + return self.order_children(self.children) + @staticmethod def _escape_value(value): return re.sub(r"([\]\\])", r"\\\1", value) if isinstance(value, str) else value # escape \ and ] @@ -122,7 +126,7 @@ class SGFNode: if len(item.children) == 1: stack.append(item.children[0]) elif item.children: - stack += sum([[")", c, "("] for c in self.order_children(item.children)[::-1]], []) + stack += sum([[")", c, "("] for c in item.ordered_children[::-1]], []) return sgf_str def add_list_property(self, property: str, values: List): diff --git a/katrain/gui.kv b/katrain/gui.kv index bc2d44f..8cf7ab8 100644 --- a/katrain/gui.kv +++ b/katrain/gui.kv @@ -236,7 +236,7 @@ NavIconButton: id: branch icon: 'img/Branch.png' - on_press: root.katrain('switch-branch',1,True) + on_press: root.katrain('cycle-children',1) disabled: True ClickableLabel: pos_hint: {'center_x': 0.85,'center_y':0.5} diff --git a/katrain/gui/popups.py b/katrain/gui/popups.py index 81db4d0..9d44d9d 100644 --- a/katrain/gui/popups.py +++ b/katrain/gui/popups.py @@ -310,7 +310,7 @@ class AIPopup(QuickConfigGui): class ConfigPopup(QuickConfigGui): def build_and_set_properties(self, *_args): super().build_and_set_properties() - #self.check_models() + # self.check_models() def check_models(self, *args): # WIP try: diff --git a/katrain/gui/widgets/graph.py b/katrain/gui/widgets/graph.py index f6d9351..c746d2e 100644 --- a/katrain/gui/widgets/graph.py +++ b/katrain/gui/widgets/graph.py @@ -5,6 +5,7 @@ from kivy.properties import BooleanProperty, ListProperty, NumericProperty, Cloc from kivymd.app import MDApp from katrain.gui.kivyutils import BackgroundMixin +import threading class ScoreGraph(BackgroundMixin): @@ -31,6 +32,7 @@ class ScoreGraph(BackgroundMixin): def __init__(self, **kwargs): super().__init__(**kwargs) + self._lock = threading.Lock() self.bind(pos=self.update_graph, size=self.update_graph) def on_touch_down(self, touch): @@ -123,23 +125,24 @@ class ScoreGraph(BackgroundMixin): self.winrate_dot_pos = winrate_dot_point def update_value(self, node): - self.highlighted_index = index = node.depth - self.nodes.extend([None] * max(0, index - (len(self.nodes) - 1))) - self.nodes[index] = node - if index > 1 and node.parent: # sometimes things go so fast - backfill, node = index-1, node.parent - while self.nodes[backfill] is None: - self.nodes[backfill] = node - backfill -= 1 - node = node.parent + with self._lock: + self.highlighted_index = index = node.depth + self.nodes.extend([None] * max(0, index - (len(self.nodes) - 1))) + self.nodes[index] = node + if index > 1 and node.parent: # sometimes things go so fast + backfill, bfnode = index - 1, node.parent + while self.nodes[backfill] is None: + self.nodes[backfill] = bfnode + backfill -= 1 + bfnode = bfnode.parent - if index + 1 < len(self.nodes) and (node is None or self.nodes[index + 1] not in node.children): - self.nodes = self.nodes[: index + 1] # on branch switching, don't show history from other branch - if index == len(self.nodes) - 1: # possibly just switched branch - while node.children: # add children back - node = node.children[0] - self.nodes.append(node) - Clock.schedule_once(self.update_graph, 0) + if index + 1 < len(self.nodes) and (node is None or self.nodes[index + 1] not in node.children): + self.nodes = self.nodes[: index + 1] # on branch switching, don't show history from other branch + if index == len(self.nodes) - 1: # possibly just switched branch + while node.children: # add children back + node = node.children[0] + self.nodes.append(node) + Clock.schedule_once(self.update_graph, 0) Builder.load_string( diff --git a/katrain/gui/widgets/movetree.py b/katrain/gui/widgets/movetree.py index 18c3e1b..be5aa78 100644 --- a/katrain/gui/widgets/movetree.py +++ b/katrain/gui/widgets/movetree.py @@ -23,9 +23,18 @@ from katrain.gui.style import ( class MoveTreeCanvas(Widget): - move_xy_pos = DictProperty({}) + scroll_view_widget = ObjectProperty(None) move_size = NumericProperty(5) - move_tree = ObjectProperty(None) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.move_pos = {} + self.move_xy_pos = {} + + def set_game_node(self, node): + katrain = MDApp.get_running_app().gui + katrain.game.set_current_node(node) + katrain.update_state() def on_touch_up(self, touch): if touch.button == "left": @@ -33,31 +42,40 @@ class MoveTreeCanvas(Widget): self.move_xy_pos.items(), key=lambda n_xy: abs(n_xy[1][0] - touch.x) + abs(n_xy[1][1] - touch.y) ) if max(abs(x - touch.x), abs(y - touch.y)) <= (self.move_size / 2): - katrain = MDApp.get_running_app().gui - katrain.game.set_current_node(node) - katrain.update_state() + self.set_game_node(node) + + def switch_branch(self, direction=1): + pos = self.move_pos.get(self.scroll_view_widget.current_node) + if not self.scroll_view_widget: + return + same_x_moves = sorted([(y, n) for n, (x, y) in self.move_pos.items() if x == pos[0]]) + new_index = next((i for i, (y, n) in enumerate(same_x_moves) if y == pos[1]), 0) + direction + if new_index < 0 or new_index >= len(same_x_moves): + return + self.set_game_node(same_x_moves[new_index][1]) def draw_move_tree(self, current_node): - if not self.move_tree: + if not self.scroll_view_widget: return + spacing = 5 moves_vert = 3 - self.move_size = (self.move_tree.height - (moves_vert + 1) * spacing) / moves_vert + self.move_size = (self.scroll_view_widget.height - (moves_vert + 1) * spacing) / moves_vert root = current_node.root - move_pos = {root: (0, 0)} - stack = GameNode.order_children(root.children)[::-1] + self.move_pos = {root: (0, 0)} + stack = root.ordered_children[::-1] next_y_pos = defaultdict(int) # x pos -> max y pos - children = defaultdict(list) # since AI self-play etc may modify the tree between layout and draw! + children = defaultdict(list) # since AI self-play etc may modify the tree between layout and draw! while stack: move = stack.pop() x = move.depth - y = max(next_y_pos[x], move_pos[move.parent][1]) + y = max(next_y_pos[x], self.move_pos[move.parent][1]) next_y_pos[x] = y + 1 next_y_pos[x - 1] = max(next_y_pos[x], next_y_pos[x - 1]) - move_pos[move] = (x, y) - children[move] = GameNode.order_children(move.children) + self.move_pos[move] = (x, y) + children[move] = move.ordered_children for c in children[move][::-1]: # stack, so push top child last to process first stack.append(c) @@ -69,13 +87,13 @@ class MoveTreeCanvas(Widget): def coord_pos(coord): return (coord + 0.5) * (spacing + self.move_size) + spacing / 2 - self.width = coord_pos(max(x + 0.5 for x, y in move_pos.values())) - self.height = coord_pos(max(y + 0.5 for x, y in move_pos.values())) + self.width = coord_pos(max(x + 0.5 for x, y in self.move_pos.values())) + self.height = coord_pos(max(y + 0.5 for x, y in self.move_pos.values())) def xy_pos(x, y): return coord_pos(x), self.height - coord_pos(y) - self.move_xy_pos = {n: xy_pos(x, y) for n, (x, y) in move_pos.items()} + self.move_xy_pos = {n: xy_pos(x, y) for n, (x, y) in self.move_pos.items()} with self.canvas: self.canvas.clear() @@ -96,7 +114,7 @@ class MoveTreeCanvas(Widget): Color(*STONE_COLORS["W" if node.player == "B" else "B"]) draw_text(pos=pos, text=text, font_size=self.move_size * 1.75 / (1 + 1 * len(text)), font_name="Roboto") - self.move_tree.scroll_to_pixel(*self.move_xy_pos[current_node]) + self.scroll_view_widget.scroll_to_pixel(*self.move_xy_pos[current_node]) class MoveTree(ScrollView, BackgroundMixin): @@ -109,6 +127,9 @@ class MoveTree(ScrollView, BackgroundMixin): ) self.bind(current_node=self.redraw_tree_trigger, size=self.redraw_tree_trigger) + def switch_branch(self, direction): + self.move_tree_canvas.switch_branch(direction) + def scroll_to_pixel(self, x, y): if not self._viewport: return @@ -126,8 +147,9 @@ Builder.load_string( : background_color: BOX_BACKGROUND_COLOR move_tree_canvas: move_tree_canvas + scroll_distance: 0 # scroll wheel is for forward/backward MoveTreeCanvas: - move_tree: root + scroll_view_widget: root id: move_tree_canvas size_hint: None, None """