import math from kivy.graphics.context_instructions import Color from kivy.graphics.vertex_instructions import Ellipse, Line, Rectangle from kivy.properties import ListProperty from kivy.uix.boxlayout import BoxLayout from kivy.uix.widget import Widget from common import OUTPUT_DEBUG from game import Move from gui.kivyutils import draw_circle, draw_text from common import var_to_grid class BadukPanWidget(Widget): def __init__(self, **kwargs): super(BadukPanWidget, self).__init__(**kwargs) self.ui_config = {} self.trainer_config = {} self.ghost_stone = [] self.gridpos_x = [] self.gridpos_y = [] self.grid_size = 0 self.stone_size = 0 self.last_eval = 0 # stone placement functions def _find_closest(self, pos, gridpos): return sorted([(abs(p - pos), i) for i, p in enumerate(gridpos)])[0] def on_touch_down(self, touch): if touch.button != "left": return if not self.gridpos_x: return xd, xp = self._find_closest(touch.x, self.gridpos_x) yd, yp = self._find_closest(touch.y, self.gridpos_y) prev_ghost = self.ghost_stone if max(yd, xd) < self.grid_size / 2 and (xp, yp) not in [m.coords for m in self.katrain.game.stones]: self.ghost_stone = (xp, yp) else: self.ghost_stone = None if prev_ghost != self.ghost_stone: self.draw_board_contents() def on_touch_move(self, touch): # on_motion on_touch_move return self.on_touch_down(touch) def on_touch_up(self, touch): if touch.button != "left": return if not self.gridpos_x: return katrain = self.katrain if self.ghost_stone: katrain("play", self.ghost_stone) else: xd, xp = self._find_closest(touch.x, self.gridpos_x) yd, yp = self._find_closest(touch.y, self.gridpos_y) nodes_here = [node for node in katrain.game.current_node.nodes_from_root if node.single_move and node.single_move.coords == (xp, yp)] if nodes_here and max(yd, xd) < self.grid_size / 2: # load old comment katrain.log(f"\nAnalysis:\n{nodes_here[-1].analysis}", OUTPUT_DEBUG) katrain.log(f"\nParent Analysis:\n{nodes_here[-1].parent.analysis}", OUTPUT_DEBUG) katrain.controls.info.text = nodes_here[-1].comment(sgf=True) self.ghost_stone = None self.draw_board_contents() # remove ghost # drawing functions def on_size(self, *args): self.draw_board() self.draw_board_contents() def draw_stone(self, x, y, col, outline_col=None, innercol=None, evalcol=None, evalscale=1.0, scale=1.0): stone_size = self.stone_size * scale draw_circle((self.gridpos_x[x], self.gridpos_y[y]), stone_size, col) if outline_col: Color(*outline_col) Line(circle=(self.gridpos_x[x], self.gridpos_y[y], stone_size), width=0.05 * stone_size) if evalcol: evalsize = self.stone_size * evalscale * self.ui_config["eval_dot_max_size"] draw_circle((self.gridpos_x[x], self.gridpos_y[y]), evalsize, evalcol) if innercol: Color(*innercol) 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"] thresholds = self.trainer_config["eval_thresholds"] i = 0 while i < len(thresholds) and points_lost < thresholds[i]: i += 1 return colors[min(i, len(colors) - 1)] def draw_board(self, *args): if not self.ui_config: 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.125, 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 * self.ui_config["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)] line_color = self.ui_config["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): 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 * self.ui_config["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) 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) def draw_board_contents(self, *args): if not self.ui_config: return stone_color = self.ui_config["stones"] outline_color = self.ui_config["outline"] ghost_alpha = self.ui_config["ghost_alpha"] katrain = self.katrain board_size_x, board_size_y = katrain.game.board_size self.canvas.clear() with self.canvas: # stones current_node = katrain.game.current_node next_player = katrain.game.next_player game_ended = katrain.game.game_ended full_eval_on = katrain.controls.eval.active has_stone = {} drawn_stone = {} for m in katrain.game.stones: has_stone[m.coords] = m.player show_n_eval = self.trainer_config["eval_off_show_last"] nodes = katrain.game.current_node.nodes_from_root for i, node in enumerate(nodes[::-1]): # reverse order! points_lost = node.points_lost evalsize = 1 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 = full_eval_on or i < show_n_eval evalcol = self.eval_color(points_lost) if move_eval_on and points_lost is not None else 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) if katrain.game.current_node.is_root and katrain.config("debug/level") >= 3: # secret ;) for s in range(0, 19): c = s evalcol = self.eval_color(s) evalsize = 1 self.draw_stone(5, c, stone_color["B"], outline_color["B"], None, evalcol, evalsize) self.draw_stone(6, c, stone_color["B"], outline_color["B"], stone_color["W"], evalcol, evalsize) self.draw_stone(7, c, stone_color["W"], outline_color["W"], None, evalcol, evalsize) self.draw_stone(8, c, stone_color["W"], outline_color["W"], stone_color["B"], evalcol, evalsize) self.draw_stone(9, c, [*evalcol, 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.controls.ownership.active and ownership: rsz = self.grid_size * 0.2 ix = 0 for y in range(board_size_y - 1, -1, -1): for x in range(board_size_x): ix_owner = "B" if ownership[ix] > 0 else "W" if ix_owner != (has_stone.get((x, y), -1)): Color(*stone_color[ix_owner], abs(ownership[ix])) Rectangle(pos=(self.gridpos_x[x] - rsz / 2, self.gridpos_y[y] - rsz / 2), size=(rsz, rsz)) ix = ix + 1 policy = current_node.policy if not policy and current_node.parent and current_node.parent.policy and "ai" in katrain.controls.player_mode("B") and "ai" in katrain.controls.player_mode("W"): 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.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 = 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), ) 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"]) # children of current moves in undo / review undo_coords = set() if katrain.controls.show_children.active: alpha = self.ui_config["ghost_alpha"] for child_node in current_node.children: points_lost = child_node.points_lost m = child_node.single_move if m and m.coords is not None: undo_coords.add(m.coords) evalcol = (*self.eval_color(points_lost), alpha) if points_lost is not None else None scale = self.ui_config.get("_child_scale", 0.95) self.draw_stone(m.coords[0], m.coords[1], (*stone_color[m.player][:3], alpha), None, None, evalcol, evalscale=scale, scale=scale) # hints if katrain.controls.hints.active and not game_ended: hint_moves = current_node.candidate_moves for i, d in enumerate(hint_moves): move = Move.from_gtp(d["move"]) if move.coords is not None and move.coords not in undo_coords: alpha, scale = self.ui_config["ghost_alpha"], 1.0 if i == 0: alpha += self.ui_config["top_move_x_alpha"] elif d["visits"] < 0.1 * hint_moves[0]["visits"]: # TODO: config? scale = 0.8 self.draw_stone(move.coords[0], move.coords[1], [*self.eval_color(d["pointsLost"]), alpha], scale=scale) # hover next move ghost stone if self.ghost_stone: self.draw_stone(*self.ghost_stone, (*stone_color[next_player], ghost_alpha)) # pass circle passed = len(nodes) > 1 and current_node.is_pass if passed: if game_ended: text = "game\nend" else: text = "pass" Color(0.45, 0.05, 0.45, 0.5) center = (self.pos[0] + self.width / 2, self.pos[1] + self.height / 2) size = min(self.width, self.height) * 0.22 Ellipse(pos=(center[0] - size / 2, center[1] - size / 2), size=(size, size)) Color(0.15, 0.15, 0.15) draw_text(pos=center, text=text, font_size=size * 0.25, halign="center", outline_color=[0.95, 0.95, 0.95]) class BadukPanControls(BoxLayout): pass