def draw_pv(self, pv, node, up_to_move): katrain = self.katrain next_last_player = [node.next_player, node.player] stone_color = STONE_COLORS cn = katrain.game.current_node if node != cn and node.parent != cn: hide_node = cn while hide_node and hide_node.move and hide_node != node: if not hide_node.move.is_pass: self.draw_stone(*hide_node.move.coords, [0.85, 0.68, 0.40, 0.8]) # board coloured dot hide_node = hide_node.parent for i, gtpmove in enumerate(pv): if i > up_to_move: return move_player = next_last_player[i % 2] opp_player = next_last_player[1 - i % 2] coords = Move.from_gtp(gtpmove).coords if coords is None: # tee-hee sizefac = katrain.board_controls.pass_btn.size[1] / 2 / self.stone_size board_coords = [ katrain.board_controls.pass_btn.pos[0] + katrain.board_controls.pass_btn.size[0] + self.stone_size * sizefac, katrain.board_controls.pass_btn.pos[1] + katrain.board_controls.pass_btn.size[1] / 2, ] else: board_coords = (self.gridpos_x[coords[0]], self.gridpos_y[coords[1]]) sizefac = 1 draw_circle(board_coords, self.stone_size * sizefac, stone_color[move_player]) Color(*stone_color[opp_player]) draw_text(pos=board_coords, text=str(i + 1), font_size=self.grid_size * sizefac / 1.45, font_name="Roboto")
def draw_pv(self, pv, node, up_to_move): katrain = self.katrain next_last_player = [ node.next_player, Move.opponent_player(node.next_player) ] cn = katrain.game.current_node if node != cn and node.parent != cn: hide_node = cn while hide_node and hide_node.move and hide_node != node: if not hide_node.move.is_pass: pos = (self.gridpos_x[hide_node.move.coords[0]], self.gridpos_y[hide_node.move.coords[1]]) draw_circle(pos, self.stone_size, [0.85, 0.68, 0.40, 0.8]) hide_node = hide_node.parent for i, gtpmove in enumerate(pv): if i > up_to_move: return move_player = next_last_player[i % 2] coords = Move.from_gtp(gtpmove).coords if coords is None: # tee-hee sizefac = katrain.board_controls.pass_btn.size[ 1] / 2 / self.stone_size board_coords = [ katrain.board_controls.pass_btn.pos[0] + katrain.board_controls.pass_btn.size[0] + self.stone_size * sizefac, katrain.board_controls.pass_btn.pos[1] + katrain.board_controls.pass_btn.size[1] / 2, ] else: board_coords = (self.gridpos_x[coords[0]], self.gridpos_y[coords[1]]) sizefac = 1 stone_size = self.stone_size * sizefac Color(1, 1, 1, 1) Rectangle( # not sure why the -1 here, but seems to center better pos=(board_coords[0] - stone_size - 1, board_coords[1] - stone_size), size=(2 * stone_size + 1, 2 * stone_size + 1), texture=cached_texture(Theme.STONE_TEXTURE[move_player]), ) Color(*Theme.PV_TEXT_COLORS[move_player]) draw_text(pos=board_coords, text=str(i + 1), font_size=self.grid_size * sizefac / 1.45, font_name="Roboto")
def draw_move_tree(self, current_node, insert_node): if not self.scroll_view_widget or not current_node: return spacing = 5 moves_vert = 3 self.move_size = (self.scroll_view_widget.min_height - (moves_vert + 1) * spacing) / moves_vert root = current_node.root def children_with_shortcuts(move): shortcuts = move.shortcuts_to via = {v: m for m, v in shortcuts} # children that are shortcut return [ m if m not in via else via[m] for m in move.ordered_children ] self.move_pos = {root: (0, 0)} stack = children_with_shortcuts(root)[::-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[root] = [*stack] while stack: move = stack.pop() if move.shortcut_from: parent = move.shortcut_from else: parent = move.parent if parent: x = self.move_pos[parent][0] + 1 else: x = 0 y = max(next_y_pos[x], self.move_pos[parent][1]) next_y_pos[x] = y + 1 next_y_pos[x - 1] = max(next_y_pos[x], next_y_pos[x - 1]) self.move_pos[move] = (x, y) children[move] = children_with_shortcuts(move) stack += children[ move][::-1] # stack, so push top child last to process first def draw_stone(pos, player, special_color=None): draw_circle(pos, self.move_size / 2 - 0.5, (special_color or Theme.STONE_COLORS[player])) Color(*Theme.MOVE_TREE_STONE_OUTLINE_COLORS[player]) Line(circle=(*pos, self.move_size / 2), width=1) 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 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 self.move_pos.items() } special_nodes = { current_node: Theme.MOVE_TREE_CURRENT, self.menu_selected_node: Theme.MOVE_TREE_SELECTED } if insert_node: special_nodes[ insert_node.parent] = Theme.MOVE_TREE_INSERT_NODE_PARENT insert_path = current_node while insert_path != insert_node.parent and insert_path.parent: special_nodes[insert_path] = (Theme.MOVE_TREE_INSERT_CURRENT if insert_path == current_node else Theme.MOVE_TREE_INSERT_OTHER) insert_path = insert_path.parent with self.canvas: self.canvas.clear() Color(*Theme.MOVE_TREE_LINE) for node, (x, y) in self.move_xy_pos.items(): for ci, c in enumerate(children[node]): cx, cy = self.move_xy_pos[c] Line(points=[x, y, x, cy, cx, cy], width=1) for node, pos in self.move_xy_pos.items(): if node in special_nodes: Color(*special_nodes[node]) Rectangle( pos=[ c - self.move_size / 2 - spacing / 2 for c in self.move_xy_pos[node] ], size=(self.move_size + spacing, self.move_size + spacing), ) placements = node.placements + node.clear_placements special_node = Theme.MOVE_TREE_COLLAPSED if node.shortcut_from or placements else None draw_stone(pos, node.player, special_node) text = "+" if placements else str(node.depth) Color( *Theme.STONE_COLORS["W" if node.player == "B" and not node. shortcut_from else "B"]) draw_text(pos=pos, text=text, font_size=self.move_size * 1.75 / (1 + 1 * len(text)), font_name="Roboto") if current_node in self.move_xy_pos: self.scroll_view_widget.scroll_to_pixel( *self.move_xy_pos[current_node])
def draw_board_contents(self, *_args): if not (self.katrain and self.katrain.game): return 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[ "lock_ai"] and self.katrain.play_analyze_mode == MODE_PLAY show_n_eval = self.trainer_config["eval_off_show_last"] self.canvas.clear() with self.canvas: # stones current_node = katrain.game.current_node game_ended = katrain.game.ended full_eval_on = katrain.analysis_controls.eval.active has_stone = {} drawn_stone = {} for m in katrain.game.stones: has_stone[m.coords] = m.player show_dots_for = { p: self.trainer_config["eval_show_ai"] or katrain.players_info[p].human for p in Move.PLAYERS } show_dots_for_class = self.trainer_config["show_dots"] nodes = katrain.game.current_node.nodes_from_root realized_points_lost = None katrain.config("trainer/show_dots") for i, node in enumerate(nodes[::-1]): # reverse order! points_lost = node.points_lost evalsize = 1 if points_lost and realized_points_lost: if points_lost <= 0.5 and realized_points_lost <= 1.5: evalsize = 0 else: evalsize = min( 1, max(0, realized_points_lost / points_lost)) for m in node.move_with_placements: if has_stone.get(m.coords) and not drawn_stone.get( m.coords): # skip captures, last only for move_eval_on = show_dots_for.get( m.player) and (i < show_n_eval or full_eval_on) if move_eval_on and points_lost is not None: evalcol = self.eval_color(points_lost, show_dots_for_class) else: evalcol = None inner = stone_color[m.opponent] if i == 0 else None drawn_stone[m.coords] = m.player self.draw_stone( m.coords[0], m.coords[1], stone_color[m.player], outline_color[m.player], inner, evalcol, evalsize, ) realized_points_lost = node.parent_realized_points_lost if katrain.game.current_node.is_root and katrain.debug_level >= 3: # secret ;) for y in range(0, board_size_y): evalcol = self.eval_color(16 * y / board_size_y) self.draw_stone(0, y, stone_color["B"], outline_color["B"], None, evalcol, y / (board_size_y - 1)) self.draw_stone(1, y, stone_color["B"], outline_color["B"], stone_color["W"], evalcol, 1) self.draw_stone(2, y, stone_color["W"], outline_color["W"], None, evalcol, y / (board_size_y - 1)) self.draw_stone(3, y, stone_color["W"], outline_color["W"], stone_color["B"], evalcol, 1) self.draw_stone(4, y, [*evalcol[:3], 0.5], scale=0.8) # ownership - allow one move out of date for smooth animation ownership = current_node.ownership or ( current_node.parent and current_node.parent.ownership) if katrain.analysis_controls.ownership.active and ownership and not lock_ai: ownership_grid = var_to_grid(ownership, (board_size_x, board_size_y)) rsz = self.grid_size * 0.2 for y in range(board_size_y - 1, -1, -1): for x in range(board_size_x): ix_owner = "B" if ownership_grid[y][x] > 0 else "W" if ix_owner != (has_stone.get((x, y), -1)): Color(*stone_color[ix_owner], abs(ownership_grid[y][x])) 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.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 pass_btn.canvas.after.clear() if katrain.analysis_controls.policy.active and policy and not lock_ai: policy_grid = var_to_grid(policy, (board_size_x, board_size_y)) best_move_policy = max(*policy) for y in range(board_size_y - 1, -1, -1): for x in range(board_size_x): if policy_grid[y][x] > 0: polsize = 1.1 * math.sqrt(policy_grid[y][x]) policy_circle_color = ( *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, POLICY_COLOR, ) # pass circle passed = len(nodes) > 1 and current_node.is_pass if passed: if game_ended: text = katrain.game.manual_score or i18n._( "board-game-end") else: text = i18n._("board-pass") Color(0.45, 0.05, 0.45, 0.7) center = (self.gridpos_x[int(board_size_x / 2)], self.gridpos_y[int(board_size_y / 2)]) size = min(self.width, self.height) * 0.22 Ellipse(pos=(center[0] - size / 2, center[1] - size / 2), size=(size, size)) Color(0.85, 0.85, 0.85) draw_text(pos=center, text=text, font_size=size * 0.25, halign="center", outline_color=[0.95, 0.95, 0.95]) self.draw_hover_contents()
def draw_board(self, *_args): if not (self.katrain and self.katrain.game): return katrain = self.katrain board_size_x, board_size_y = katrain.game.board_size max_board_size = max(board_size_x, board_size_y) self.canvas.before.clear() with self.canvas.before: # set up margins and grid lines grid_spaces_margin_x = [1.5, 0.75] # left, right grid_spaces_margin_y = [1.5, 0.75] # bottom, top x_grid_spaces = board_size_x - 1 + sum(grid_spaces_margin_x) y_grid_spaces = board_size_y - 1 + sum(grid_spaces_margin_y) self.grid_size = min(self.width / x_grid_spaces, self.height / y_grid_spaces) board_width_with_margins = x_grid_spaces * self.grid_size 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 * 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(*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), ) 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): Line(points=[( self.gridpos_x[0], self.gridpos_y[i]), (self.gridpos_x[-1], self.gridpos_y[i])]) # star points def star_point_coords(size): star_point_pos = 3 if size <= 11 else 4 if size < 7: 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 * 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) # coordinates Color(0.25, 0.25, 0.25) coord_offset = self.grid_size * 1.5 / 2 for i in range(board_size_x): draw_text( pos=(self.gridpos_x[i], self.gridpos_y[0] - coord_offset), text=Move.GTP_COORD[i], font_size=self.grid_size / 1.5, font_name="Roboto", ) for i in range(board_size_y): draw_text( pos=(self.gridpos_x[0] - coord_offset, self.gridpos_y[i]), text=str(i + 1), font_size=self.grid_size / 1.5, font_name="Roboto", )
def draw_hover_contents(self, *_args): ghost_alpha = Theme.GHOST_ALPHA katrain = self.katrain game_ended = katrain.game.end_result current_node = katrain.game.current_node next_player = current_node.next_player board_size_x, board_size_y = katrain.game.board_size if len(self.gridpos_x) < board_size_x or len(self.gridpos_y) < board_size_y: return # race condition with self.canvas.after: self.canvas.after.clear() self.active_pv_moves = [] # hints or PV hint_moves = [] if ( katrain.analysis_controls.hints.active and not katrain.analysis_controls.policy.active and not game_ended ): hint_moves = current_node.candidate_moves elif katrain.controls.status_state[1] == STATUS_TEACHING: # show score hint for teaching undo hint_moves = [ m for m in current_node.candidate_moves for c in current_node.children if c.move and c.auto_undo and c.move.gtp() == m["move"] ] top_move_coords = None child_moves = {c.move.gtp() for c in current_node.children if c.move} if hint_moves: low_visits_threshold = katrain.config("trainer/low_visits", 25) top_moves_show = [ opt for opt in [ katrain.config("trainer/top_moves_show"), katrain.config("trainer/top_moves_show_secondary"), ] if opt in TOP_MOVE_OPTIONS and opt != TOP_MOVE_NOTHING ] for move_dict in hint_moves: move = Move.from_gtp(move_dict["move"]) if move.coords is not None: engine_best_move = move_dict.get("order", 99) == 0 scale = Theme.HINT_SCALE text_on = True alpha = Theme.HINTS_ALPHA if ( move_dict["visits"] < low_visits_threshold and not engine_best_move and not move_dict["move"] in child_moves ): scale = Theme.UNCERTAIN_HINT_SCALE text_on = False alpha = Theme.HINTS_LO_ALPHA if "pv" in move_dict: self.active_pv_moves.append((move.coords, move_dict["pv"], current_node)) else: katrain.log(f"PV missing for move_dict {move_dict}", OUTPUT_DEBUG) evalsize = self.stone_size * scale evalcol = self.eval_color(move_dict["pointsLost"]) if text_on and top_moves_show: # remove grid lines using a board colored circle draw_circle( (self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]]), self.stone_size * scale * 0.98, Theme.APPROX_BOARD_COLOR, ) Color(*evalcol[:3], alpha) Rectangle( pos=(self.gridpos_x[move.coords[0]] - evalsize, self.gridpos_y[move.coords[1]] - evalsize), size=(2 * evalsize, 2 * evalsize), texture=cached_texture(Theme.TOP_MOVE_TEXTURE), ) if text_on and top_moves_show: # TODO: faster if not sized? keys = {"size": self.grid_size / 3, "smallsize": self.grid_size / 3.33} player_sign = current_node.player_sign(next_player) if len(top_moves_show) == 1: fmt = "[size={size:.0f}]{" + top_moves_show[0] + "}[/size]" else: fmt = ( "[size={size:.0f}]{" + top_moves_show[0] + "}[/size]\n[size={smallsize:.0f}]{" + top_moves_show[1] + "}[/size]" ) keys[TOP_MOVE_DELTA_SCORE] = ( "0.0" if -0.05 < move_dict["pointsLost"] < 0.05 else f"{-move_dict['pointsLost']:+.1f}" ) # def fmt_maybe_missing(arg,sign,digits=1): # return str(round(sign*arg,digits)) if arg is not None else "N/A" keys[TOP_MOVE_SCORE] = f"{player_sign * move_dict['scoreLead']:.1f}" winrate = move_dict["winrate"] if player_sign == 1 else 1 - move_dict["winrate"] keys[TOP_MOVE_WINRATE] = f"{winrate*100:.1f}" keys[TOP_MOVE_DELTA_WINRATE] = f"{-move_dict['winrateLost']:+.1%}" keys[TOP_MOVE_VISITS] = format_visits(move_dict["visits"]) # keys[TOP_MOVE_UTILITY] = fmt_maybe_missing( move_dict.get('utility'),player_sign,2) # keys[TOP_MOVE_UTILITYLCB] = fmt_maybe_missing(move_dict.get('utilityLcb'),player_sign,2) # keys[TOP_MOVE_SCORE_STDDEV] =fmt_maybe_missing(move_dict.get('scoreStdev'),1) Color(*Theme.HINT_TEXT_COLOR) draw_text( pos=(self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]]), text=fmt.format(**keys), font_name="Roboto", markup=True, line_height=0.85, halign="center", ) if engine_best_move: top_move_coords = move.coords Color(*Theme.TOP_MOVE_BORDER_COLOR) Line( circle=( self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]], self.stone_size - dp(1.2), ), width=dp(1.2), ) # children of current moves in undo / review if katrain.analysis_controls.show_children.active: for child_node in current_node.children: move = child_node.move if move and move.coords is not None: if child_node.analysis_exists: self.active_pv_moves.append( (move.coords, [move.gtp()] + child_node.candidate_moves[0]["pv"], current_node) ) if move.coords != top_move_coords: # for contrast dashed_width = 18 Color(*Theme.NEXT_MOVE_DASH_CONTRAST_COLORS[child_node.player]) Line( circle=( self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]], self.stone_size - dp(1.2), ), width=dp(1.2), ) else: dashed_width = 10 Color(*Theme.STONE_COLORS[child_node.player]) for s in range(0, 360, 30): Line( circle=( self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]], self.stone_size - dp(1.2), s, s + dashed_width, ), width=dp(1.2), ) if self.selecting_region_of_interest and len(self.region_of_interest) == 4: x1, x2, y1, y2 = self.region_of_interest self.draw_roi_box([min(x1, x2), max(x1, x2), min(y1, y2), max(y1, y2)], width=dp(2)) else: # hover next move ghost stone if self.ghost_stone: self.draw_stone(*self.ghost_stone, next_player, alpha=ghost_alpha) animating_pv = self.animating_pv if animating_pv: pv, node, start_time, _ = animating_pv up_to_move = self.get_animate_pv_index() self.draw_pv(pv, node, up_to_move) if getattr(self.katrain.game, "region_of_interest", None): self.draw_roi_box(self.katrain.game.region_of_interest, width=dp(1.25)) # pass circle if current_node.is_pass or game_ended: if game_ended: text = game_ended katrain.controls.timer.paused = True else: text = i18n._("board-pass") Color(*Theme.PASS_CIRCLE_COLOR) center = (self.gridpos_x[int(board_size_x / 2)], self.gridpos_y[int(board_size_y / 2)]) size = min(self.width, self.height) * 0.227 Ellipse(pos=(center[0] - size / 2, center[1] - size / 2), size=(size, size)) Color(*Theme.PASS_CIRCLE_TEXT_COLOR) draw_text(pos=center, text=text, font_size=size * 0.25, halign="center")
def draw_board_contents(self, *_args): if not (self.katrain and self.katrain.game): return katrain = self.katrain board_size_x, board_size_y = katrain.game.board_size if len(self.gridpos_x) < board_size_x or len(self.gridpos_y) < board_size_y: return # race condition show_n_eval = self.trainer_config.get("eval_on_show_last", 3) with self.canvas: self.canvas.clear() # stones current_node = katrain.game.current_node all_dots_off = not katrain.analysis_controls.eval.active has_stone = {} drawn_stone = {} for m in katrain.game.stones: has_stone[m.coords] = m.player show_dots_for = { p: self.trainer_config["eval_show_ai"] or katrain.players_info[p].human for p in Move.PLAYERS } show_dots_for_class = self.trainer_config["show_dots"] nodes = katrain.game.current_node.nodes_from_root realized_points_lost = None for i, node in enumerate(nodes[::-1]): # reverse order! points_lost = node.points_lost evalscale = 1 if points_lost and realized_points_lost: if points_lost <= 0.5 and realized_points_lost <= 1.5: evalscale = 0 else: evalscale = min(1, max(0, realized_points_lost / points_lost)) placements = node.placements for m in node.moves + placements: if has_stone.get(m.coords) and not drawn_stone.get(m.coords): # skip captures, last only for move_eval_on = not all_dots_off and show_dots_for.get(m.player) and i < show_n_eval if move_eval_on and points_lost is not None: evalcol = self.eval_color(points_lost, show_dots_for_class) else: evalcol = None inner = Theme.STONE_COLORS[m.opponent] if i == 0 and m not in placements else None drawn_stone[m.coords] = m.player self.draw_stone( x=m.coords[0], y=m.coords[1], player=m.player, innercol=inner, evalcol=evalcol, evalscale=evalscale, ) realized_points_lost = node.parent_realized_points_lost if katrain.game.current_node.is_root and katrain.debug_level >= 3: # secret ;) for y in range(0, board_size_y): evalcol = self.eval_color(16 * y / board_size_y) self.draw_stone(0, y, "B", evalcol=evalcol, evalscale=y / (board_size_y - 1)) self.draw_stone(1, y, "B", innercol=Theme.STONE_COLORS["W"], evalcol=evalcol) self.draw_stone(2, y, "W", evalcol=evalcol, evalscale=y / (board_size_y - 1)) self.draw_stone(3, y, "W", innercol=Theme.STONE_COLORS["B"], evalcol=evalcol) # ownership - allow one move out of date for smooth animation ownership = current_node.ownership or (current_node.parent and current_node.parent.ownership) if katrain.analysis_controls.ownership.active and ownership: rsz = self.grid_size * 0.2 if ( current_node.children and katrain.controls.status_state[1] == STATUS_TEACHING and current_node.children[-1].auto_undo and current_node.children[-1].ownership ): # loss loss_grid = var_to_grid( [a - b for a, b in zip(current_node.children[-1].ownership, ownership)], (board_size_x, board_size_y), ) for y in range(board_size_y - 1, -1, -1): for x in range(board_size_x): loss = max(0, (-1 if current_node.children[-1].move.player == "B" else 1) * loss_grid[y][x]) if loss > 0: Color(*Theme.EVAL_COLORS[self.trainer_config["theme"]][1][:3], loss) Rectangle( pos=(self.gridpos_x[x] - rsz / 2, self.gridpos_y[y] - rsz / 2), size=(rsz, rsz) ) else: ownership_grid = var_to_grid(ownership, (board_size_x, board_size_y)) for y in range(board_size_y - 1, -1, -1): for x in range(board_size_x): ix_owner = "B" if ownership_grid[y][x] > 0 else "W" if ix_owner != (has_stone.get((x, y), -1)): Color(*Theme.STONE_COLORS[ix_owner][:3], abs(ownership_grid[y][x])) 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.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 pass_btn.canvas.after.clear() if katrain.analysis_controls.policy.active and policy: policy_grid = var_to_grid(policy, (board_size_x, board_size_y)) best_move_policy = max(*policy) colors = Theme.EVAL_COLORS[self.trainer_config["theme"]] text_lb = 0.01 * 0.01 for y in range(board_size_y - 1, -1, -1): for x in range(board_size_x): move_policy = policy_grid[y][x] if move_policy < 0: continue pol_order = max(0, 5 + int(math.log10(max(1e-9, move_policy - 1e-9)))) if move_policy > text_lb: draw_circle( (self.gridpos_x[x], self.gridpos_y[y]), self.stone_size * Theme.HINT_SCALE * 0.98, Theme.APPROX_BOARD_COLOR, ) scale = 0.95 else: scale = 0.5 draw_circle( (self.gridpos_x[x], self.gridpos_y[y]), Theme.HINT_SCALE * self.stone_size * scale, (*colors[pol_order][:3], Theme.POLICY_ALPHA), ) if move_policy > text_lb: Color(*Theme.HINT_TEXT_COLOR) draw_text( pos=(self.gridpos_x[x], self.gridpos_y[y]), text=f"{100 * move_policy :.2f}"[:4] + "%", font_name="Roboto", font_size=self.grid_size / 4, halign="center", ) if move_policy == best_move_policy: Color(*Theme.TOP_MOVE_BORDER_COLOR[:3], Theme.POLICY_ALPHA) Line( circle=( self.gridpos_x[x], self.gridpos_y[y], self.stone_size - dp(1.2), ), width=dp(2), ) with pass_btn.canvas.after: move_policy = policy[-1] pol_order = 5 - int(-math.log10(max(1e-9, move_policy - 1e-9))) if pol_order >= 0: draw_circle( (pass_btn.pos[0] + pass_btn.width / 2, pass_btn.pos[1] + pass_btn.height / 2), pass_btn.height / 2, (*colors[pol_order][:3], Theme.GHOST_ALPHA), ) self.redraw_hover_contents_trigger()
def draw_board(self, *_args): if not (self.katrain and self.katrain.game): return katrain = self.katrain board_size_x, board_size_y = katrain.game.board_size with self.canvas.before: self.canvas.before.clear() # set up margins and grid lines if self.draw_coords_enabled: grid_spaces_margin_x = [1.5, 0.75] # left, right grid_spaces_margin_y = [1.5, 0.75] # bottom, top else: # no coordinates means remove the offset grid_spaces_margin_x = [0.75, 0.75] # left, right grid_spaces_margin_y = [0.75, 0.75] # bottom, top x_grid_spaces = board_size_x - 1 + sum(grid_spaces_margin_x) y_grid_spaces = board_size_y - 1 + sum(grid_spaces_margin_y) self.grid_size = min(self.width / x_grid_spaces, self.height / y_grid_spaces) board_width_with_margins = x_grid_spaces * self.grid_size 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 * Theme.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) ] if katrain.game.insert_mode: Color(*Theme.INSERT_BOARD_COLOR_TINT) # dreamy else: Color(*Theme.BOARD_COLOR_TINT) # image is a bit too light Rectangle( pos=( self.gridpos_x[0] - self.grid_size * grid_spaces_margin_x[0], self.gridpos_y[0] - self.grid_size * grid_spaces_margin_y[0], ), size=(self.grid_size * x_grid_spaces, self.grid_size * y_grid_spaces), texture=cached_texture(Theme.BOARD_TEXTURE), ) Color(*Theme.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): Line(points=[(self.gridpos_x[0], self.gridpos_y[i]), (self.gridpos_x[-1], self.gridpos_y[i])]) # star points def star_point_coords(size): star_point_pos = 3 if size <= 11 else 4 if size < 7: 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 * Theme.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, Theme.LINE_COLOR) # coordinates if self.draw_coords_enabled: Color(0.25, 0.25, 0.25) coord_offset = self.grid_size * 1.5 / 2 for i in range(board_size_x): draw_text( pos=(self.gridpos_x[i], self.gridpos_y[0] - coord_offset), text=Move.GTP_COORD[i], font_size=self.grid_size / 1.5, font_name="Roboto", ) for i in range(board_size_y): draw_text( pos=(self.gridpos_x[0] - coord_offset, self.gridpos_y[i]), text=str(i + 1), font_size=self.grid_size / 1.5, font_name="Roboto", )
def draw_move_tree(self, current_node): if not self.scroll_view_widget: return spacing = 5 moves_vert = 3 self.move_size = (self.scroll_view_widget.height - (moves_vert + 1) * spacing) / moves_vert root = current_node.root 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! while stack: move = stack.pop() x = move.depth 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]) 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) def draw_stone(pos, player): draw_circle(pos, self.move_size / 2 - 0.5, STONE_COLORS[player]) Color(*STONE_TEXT_COLORS[player]) Line(circle=(*pos, self.move_size / 2), width=1) 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 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 self.move_pos.items()} with self.canvas: self.canvas.clear() Color(*YELLOW) Rectangle( pos=[c - self.move_size / 2 - spacing / 2 for c in self.move_xy_pos[current_node]], size=(self.move_size + spacing, self.move_size + spacing), ) Color(*LIGHTGREY) for node, (x, y) in self.move_xy_pos.items(): for ci, c in enumerate(children[node]): cx, cy = self.move_xy_pos[c] Line(points=[x, y, x, cy, cx, cy], width=1) for node, pos in self.move_xy_pos.items(): draw_stone(pos, node.player) text = str(node.depth) 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.scroll_view_widget.scroll_to_pixel(*self.move_xy_pos[current_node])
def draw_hover_contents(self, *_args): 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 board_size_x, board_size_y = katrain.game.board_size if len(self.gridpos_x) < board_size_x or len( self.gridpos_y) < board_size_y: return # race condition with self.canvas.after: self.canvas.after.clear() self.active_pv_moves = [] # children of current moves in undo / review alpha = GHOST_ALPHA if katrain.analysis_controls.show_children.active: for child_node in current_node.children: points_lost = child_node.points_lost move = child_node.move if move and move.coords is not None: if points_lost is None: evalcol = None else: evalcol = copy.copy(self.eval_color(points_lost)) evalcol[3] = alpha if child_node.analysis_ready: self.active_pv_moves.append( (move.coords, [move.gtp()] + child_node.candidate_moves[0]["pv"], current_node)) scale = CHILD_SCALE self.draw_stone( move.coords[0], move.coords[1], move.player, alpha=alpha, evalcol=evalcol, evalscale=scale, scale=scale, ) # hints or PV if katrain.analysis_controls.hints.active and not game_ended: hint_moves = current_node.candidate_moves pass_value = None for move_dict in hint_moves: if move_dict["move"] == "pass": pass_value = move_dict["pointsLost"] break for i, move_dict in enumerate(hint_moves): move = Move.from_gtp(move_dict["move"]) if move.coords is not None: alpha, scale = GHOST_ALPHA, 1.0 if 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)) else: katrain.log( f"PV missing for move_dict {move_dict}", OUTPUT_DEBUG) position = (self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]]) draw_circle( position, col=[ *self.eval_color(move_dict["pointsLost"])[:3], alpha ], r=self.stone_size * scale, ) if pass_value: Color(0, 0, 0, 1) value = pass_value - move_dict["pointsLost"] draw_text(pos=position, text=f'{value:.1f}', font_size=self.grid_size / 2, font_name="Roboto") if i == 0: Color(*TOP_MOVE_BORDER_COLOR) Line( circle=( self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]], self.stone_size * scale - 1.2, ), width=dp(1.2), ) # hover next move ghost stone if self.ghost_stone: self.draw_stone(*self.ghost_stone, next_player, alpha=ghost_alpha) animating_pv = self.animating_pv if animating_pv: pv, node, start_time, _ = animating_pv delay = self.katrain.config("general/anim_pv_time", 0.5) up_to_move = (time.time() - start_time) / delay self.draw_pv(pv, node, up_to_move)
def draw_hover_contents(self, *_args): ghost_alpha = POLICY_ALPHA katrain = self.katrain game_ended = katrain.game.end_result current_node = katrain.game.current_node player, next_player = current_node.player, current_node.next_player board_size_x, board_size_y = katrain.game.board_size if len(self.gridpos_x) < board_size_x or len(self.gridpos_y) < board_size_y: return # race condition with self.canvas.after: self.canvas.after.clear() self.active_pv_moves = [] # hints or PV hint_moves = [] if ( katrain.analysis_controls.hints.active and not katrain.analysis_controls.policy.active and not game_ended ): hint_moves = current_node.candidate_moves elif katrain.controls.status_state[1] == STATUS_TEACHING: # show score hint for teaching undo hint_moves = [ m for m in current_node.candidate_moves for c in current_node.children if c.move and c.auto_undo and c.move.gtp() == m["move"] ] top_move_coords = None if hint_moves: low_visits_threshold = katrain.config("trainer/low_visits", 25) for move_dict in hint_moves: move = Move.from_gtp(move_dict["move"]) if move.coords is not None: engine_best_move = move_dict.get("order", 99) == 0 scale = HINT_SCALE text_on = True alpha = HINTS_ALPHA if move_dict["visits"] < low_visits_threshold and not engine_best_move: scale = UNCERTAIN_HINT_SCALE text_on = False alpha = HINTS_MIN_ALPHA + (HINTS_ALPHA - HINTS_MIN_ALPHA) * ( move_dict["visits"] / low_visits_threshold ) if "pv" in move_dict: self.active_pv_moves.append((move.coords, move_dict["pv"], current_node)) else: katrain.log(f"PV missing for move_dict {move_dict}", OUTPUT_DEBUG) evalsize = self.stone_size * scale evalcol = self.eval_color(move_dict["pointsLost"]) Color(*evalcol[:3], alpha) Rectangle( pos=(self.gridpos_x[move.coords[0]] - evalsize, self.gridpos_y[move.coords[1]] - evalsize), size=(2 * evalsize, 2 * evalsize), source="img/topmove.png", ) if self.trainer_config["text_point_loss"] and text_on: if move_dict["pointsLost"] < 0.05: ptloss_text = "0.0" else: ptloss_text = f"{-move_dict['pointsLost']:+.1f}" sizefac = 1 Color(*BLACK) draw_text( pos=(self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]]), text=ptloss_text, font_size=self.grid_size * sizefac / 2.5, font_name="Roboto", ) if engine_best_move: top_move_coords = move.coords Color(*TOP_MOVE_BORDER_COLOR) Line( circle=( self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]], self.stone_size - dp(1.2), ), width=dp(1.2), ) # children of current moves in undo / review if katrain.analysis_controls.show_children.active: for child_node in current_node.children: move = child_node.move if move and move.coords is not None: if child_node.analysis_ready: self.active_pv_moves.append( (move.coords, [move.gtp()] + child_node.candidate_moves[0]["pv"], current_node) ) if move.coords != top_move_coords: # for contrast dashed_width = 18 Color(*STONE_CONTRAST_COLORS[child_node.player]) Line( circle=( self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]], self.stone_size - dp(1.2), ), width=dp(1.2), ) else: dashed_width = 10 Color(*STONE_COLORS[child_node.player]) for s in range(0, 360, 30): Line( circle=( self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]], self.stone_size - dp(1.2), s, s + dashed_width, ), width=dp(1.2), ) # hover next move ghost stone if self.ghost_stone: self.draw_stone(*self.ghost_stone, next_player, alpha=ghost_alpha) animating_pv = self.animating_pv if animating_pv: pv, node, start_time, _ = animating_pv delay = self.katrain.config("general/anim_pv_time", 0.5) up_to_move = (time.time() - start_time) / delay self.draw_pv(pv, node, up_to_move)
def draw_board_contents(self, *_args): if not (self.katrain and self.katrain.game): return katrain = self.katrain board_size_x, board_size_y = katrain.game.board_size if len(self.gridpos_x) < board_size_x or len(self.gridpos_y) < board_size_y: return # race condition show_n_eval = self.trainer_config["eval_off_show_last"] with self.canvas: self.canvas.clear() # stones current_node = katrain.game.current_node game_ended = katrain.game.end_result full_eval_on = katrain.analysis_controls.eval.active has_stone = {} drawn_stone = {} for m in katrain.game.stones: has_stone[m.coords] = m.player show_dots_for = { p: self.trainer_config["eval_show_ai"] or katrain.players_info[p].human for p in Move.PLAYERS } show_dots_for_class = self.trainer_config["show_dots"] nodes = katrain.game.current_node.nodes_from_root realized_points_lost = None for i, node in enumerate(nodes[::-1]): # reverse order! points_lost = node.points_lost evalscale = 1 if points_lost and realized_points_lost: if points_lost <= 0.5 and realized_points_lost <= 1.5: evalscale = 0 else: evalscale = min(1, max(0, realized_points_lost / points_lost)) placements = node.placements for m in node.moves + placements: if has_stone.get(m.coords) and not drawn_stone.get(m.coords): # skip captures, last only for move_eval_on = show_dots_for.get(m.player) and (i < show_n_eval or full_eval_on) if move_eval_on and points_lost is not None: evalcol = self.eval_color(points_lost, show_dots_for_class) else: evalcol = None inner = STONE_COLORS[m.opponent] if i == 0 and not m in placements else None drawn_stone[m.coords] = m.player self.draw_stone( x=m.coords[0], y=m.coords[1], player=m.player, innercol=inner, evalcol=evalcol, evalscale=evalscale, ) realized_points_lost = node.parent_realized_points_lost if katrain.game.current_node.is_root and katrain.debug_level >= 3: # secret ;) for y in range(0, board_size_y): evalcol = self.eval_color(16 * y / board_size_y) self.draw_stone(0, y, "B", evalcol=evalcol, evalscale=y / (board_size_y - 1)) self.draw_stone(1, y, "B", innercol=STONE_COLORS["W"], evalcol=evalcol) self.draw_stone(2, y, "W", evalcol=evalcol, evalscale=y / (board_size_y - 1)) self.draw_stone(3, y, "W", innercol=STONE_COLORS["B"], evalcol=evalcol) # ownership - allow one move out of date for smooth animation ownership = current_node.ownership or (current_node.parent and current_node.parent.ownership) if katrain.analysis_controls.ownership.active and ownership: rsz = self.grid_size * 0.2 if ( current_node.children and katrain.controls.status_state[1] == STATUS_TEACHING and current_node.children[-1].auto_undo and current_node.children[-1].ownership ): # loss loss_grid = var_to_grid( [a - b for a, b in zip(current_node.children[-1].ownership, ownership)], (board_size_x, board_size_y), ) for y in range(board_size_y - 1, -1, -1): for x in range(board_size_x): loss = max(0, (-1 if current_node.children[-1].move.player == "B" else 1) * loss_grid[y][x]) if loss > 0: Color(*EVAL_COLORS[self.trainer_config["theme"]][1][:3], loss) Rectangle( pos=(self.gridpos_x[x] - rsz / 2, self.gridpos_y[y] - rsz / 2), size=(rsz, rsz) ) else: ownership_grid = var_to_grid(ownership, (board_size_x, board_size_y)) for y in range(board_size_y - 1, -1, -1): for x in range(board_size_x): ix_owner = "B" if ownership_grid[y][x] > 0 else "W" if ix_owner != (has_stone.get((x, y), -1)): Color(*STONE_COLORS[ix_owner][:3], abs(ownership_grid[y][x])) 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.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 pass_btn.canvas.after.clear() if katrain.analysis_controls.policy.active and policy: policy_grid = var_to_grid(policy, (board_size_x, board_size_y)) best_move_policy = max(*policy) for y in range(board_size_y - 1, -1, -1): for x in range(board_size_x): if policy_grid[y][x] > 0: polsize = 1.1 * math.sqrt(policy_grid[y][x]) policy_circle_color = ( *POLICY_COLOR, POLICY_ALPHA + TOP_POLICY_ALPHA * (policy_grid[y][x] == best_move_policy), ) draw_circle( (self.gridpos_x[x], self.gridpos_y[y]), polsize * self.stone_size, policy_circle_color ) 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, POLICY_COLOR, ) # pass circle passed = len(nodes) > 1 and current_node.is_pass if passed or game_ended: if game_ended: text = game_ended katrain.controls.timer.paused = True else: text = i18n._("board-pass") Color(0.45, 0.05, 0.45, 0.7) center = (self.gridpos_x[int(board_size_x / 2)], self.gridpos_y[int(board_size_y / 2)]) size = min(self.width, self.height) * 0.227 Ellipse(pos=(center[0] - size / 2, center[1] - size / 2), size=(size, size)) Color(0.85, 0.85, 0.85) draw_text( pos=center, text=text, font_size=size * 0.25, halign="center", outline_color=[0.95, 0.95, 0.95] ) self.draw_hover_contents()