def settledness(d, player_sign, player): ownership_grid = var_to_grid( d["ownership"], (board_size_x, board_size_y)) return sum([ abs(ownership_grid[s.coords[0]][s.coords[1]]) for s in game.stones if s.player == player ])
def manual_score(self): rules = self.rules if (not self.current_node.ownership or str(rules).lower() not in ["jp", "japanese"] or not self.current_node.parent or not self.current_node.parent.ownership): if not self.current_node.score: return None return self.current_node.format_score( round(2 * self.current_node.score) / 2) + "?" board_size_x, board_size_y = self.board_size mean_ownership = [(c + p) / 2 for c, p in zip( self.current_node.ownership, self.current_node.parent.ownership)] ownership_grid = var_to_grid(mean_ownership, (board_size_x, board_size_y)) stones = {m.coords: m.player for m in self.stones} lo_threshold = 0.15 hi_threshold = 0.85 max_unknown = 10 max_dame = 4 * (board_size_x + board_size_y) def japanese_score_square(square, owner): player = stones.get(square, None) if ((player == "B" and owner > hi_threshold) or (player == "W" and owner < -hi_threshold) or abs(owner) < lo_threshold): return 0 # dame or own stones if player is None and abs(owner) >= hi_threshold: return round(owner) # surrounded empty intersection if (player == "B" and owner < -hi_threshold) or (player == "W" and owner > hi_threshold): return 2 * round(owner) # captured stone return math.nan # unknown! scored_squares = [ japanese_score_square((x, y), ownership_grid[y][x]) for y in range(board_size_y) for x in range(board_size_x) ] num_sq = { t: sum([s == t for s in scored_squares]) for t in [-2, -1, 0, 1, 2] } num_unkn = sum(math.isnan(s) for s in scored_squares) prisoners = self.prisoner_count score = sum([t * n for t, n in num_sq.items() ]) + prisoners["W"] - prisoners["B"] - self.komi self.katrain.log( f"Manual Scoring: {num_sq} score by square with {num_unkn} unknown, {prisoners} captures, and {self.komi} komi -> score = {score}", OUTPUT_DEBUG, ) if num_unkn > max_unknown or (num_sq[0] - len(stones)) > max_dame: return None return self.current_node.format_score(score)
def policy_ranking( self ) -> Optional[List[Tuple[ float, Move]]]: # return moves from highest policy value to lowest if self.policy: szx, szy = self.board_size policy_grid = var_to_grid(self.policy, size=(szx, szy)) moves = [(policy_grid[y][x], Move((x, y), player=self.next_player)) for x in range(szx) for y in range(szy)] moves.append((self.policy[-1], Move(None, player=self.next_player))) return sorted(moves, key=lambda mp: -mp[0])
def analyze_extra(self, mode): stones = {s.coords for s in self.stones} cn = self.current_node engine = self.engines[cn.next_player] if mode == "extra": visits = cn.analysis_visits_requested + engine.config["max_visits"] self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits)) cn.analyze(engine, visits=visits, priority=-1_000, time_limit=False) return elif mode == "sweep": board_size_x, board_size_y = self.board_size if cn.analysis_ready: policy_grid = ( var_to_grid(self.current_node.policy, size=(board_size_x, board_size_y)) if self.current_node.policy else None ) analyze_moves = sorted( [ Move(coords=(x, y), player=cn.next_player) for x in range(board_size_x) for y in range(board_size_y) if (policy_grid is None and (x, y) not in stones) or policy_grid[y][x] >= 0 ], key=lambda mv: -policy_grid[mv.coords[1]][mv.coords[0]], ) else: analyze_moves = [ Move(coords=(x, y), player=cn.next_player) for x in range(board_size_x) for y in range(board_size_y) if (x, y) not in stones ] visits = engine.config["fast_visits"] self.katrain.controls.set_status(i18n._("sweep analysis").format(visits=visits)) priority = -1_000_000_000 else: # mode=='equalize': if not cn.analysis_ready: self.katrain.controls.set_status(i18n._("wait-before-equalize"), self.current_node) return analyze_moves = [Move.from_gtp(gtp, player=cn.next_player) for gtp, _ in cn.analysis["moves"].items()] visits = max(d["visits"] for d in cn.analysis["moves"].values()) self.katrain.controls.set_status(i18n._("equalizing analysis").format(visits=visits)) priority = -1_000 for move in analyze_moves: cn.analyze( engine, priority, visits=visits, refine_move=move, time_limit=False ) # explicitly requested so take as long as you need
def analyze_extra(self, mode, **kwargs): stones = {s.coords for s in self.stones} cn = self.current_node if mode == "stop": for e in set(self.engines.values()): e.terminate_queries() self.katrain.idle_analysis = False return engine = self.engines[cn.next_player] Clock.schedule_once(self.katrain.analysis_controls.hints.activate, 0) if mode == "extra": if kwargs.get("continuous", False): visits = min( 1_000_000_000, max(engine.config["max_visits"], math.ceil(cn.analysis_visits_requested * 1.25))) else: visits = cn.analysis_visits_requested + engine.config[ "max_visits"] self.katrain.controls.set_status( i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS) self.katrain.controls.set_status( i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS) cn.analyze(engine, visits=visits, priority=-1_000, region_of_interest=self.region_of_interest, time_limit=False) return if mode == "game": nodes = self.root.nodes_in_tree if "visits" in kwargs: visits = kwargs["visits"] else: min_visits = min(node.analysis_visits_requested for node in nodes) visits = min_visits + engine.config["max_visits"] for node in nodes: node.analyze(engine, visits=visits, priority=-1_000_000, time_limit=False, report_every=None) self.katrain.controls.set_status( i18n._("game re-analysis").format(visits=visits), STATUS_ANALYSIS) return elif mode == "sweep": board_size_x, board_size_y = self.board_size if cn.analysis_exists: policy_grid = (var_to_grid(self.current_node.policy, size=(board_size_x, board_size_y)) if self.current_node.policy else None) analyze_moves = sorted( [ Move(coords=(x, y), player=cn.next_player) for x in range(board_size_x) for y in range(board_size_y) if (policy_grid is None and (x, y) not in stones) or policy_grid[y][x] >= 0 ], key=lambda mv: -policy_grid[mv.coords[1]][mv.coords[0]], ) else: analyze_moves = [ Move(coords=(x, y), player=cn.next_player) for x in range(board_size_x) for y in range(board_size_y) if (x, y) not in stones ] visits = engine.config["fast_visits"] self.katrain.controls.set_status( i18n._("sweep analysis").format(visits=visits), STATUS_ANALYSIS) priority = -1_000_000_000 elif mode in ["equalize", "alternative", "local"]: if not cn.analysis_complete and mode != "local": self.katrain.controls.set_status( i18n._("wait-before-extra-analysis"), STATUS_INFO, self.current_node) return if mode == "alternative": # also do a quick update on current candidates so it doesn't look too weird self.katrain.controls.set_status( i18n._("alternative analysis"), STATUS_ANALYSIS) cn.analyze(engine, priority=-500, time_limit=False, find_alternatives="alternative") visits = engine.config["fast_visits"] else: # equalize visits = max(d["visits"] for d in cn.analysis["moves"].values()) self.katrain.controls.set_status( i18n._("equalizing analysis").format(visits=visits), STATUS_ANALYSIS) priority = -1_000 analyze_moves = [ Move.from_gtp(gtp, player=cn.next_player) for gtp, _ in cn.analysis["moves"].items() ] else: raise ValueError("Invalid analysis mode") for move in analyze_moves: if cn.analysis["moves"].get(move.gtp(), {"visits": 0})["visits"] < visits: cn.analyze( engine, priority=priority, visits=visits, refine_move=move, time_limit=False ) # explicitly requested so take as long as you need
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_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 generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode]: cn = game.current_node while not cn.analysis_ready: time.sleep(0.01) game.engines[cn.next_player].check_alive(exception_if_dead=True) ai_thoughts = "" if (ai_mode in AI_STRATEGIES_POLICY) and cn.policy: # pure policy based move policy_moves = cn.policy_ranking pass_policy = cn.policy[-1] # dont make it jump around for the last few sensible non pass moves top_5_pass = any([polmove[1].is_pass for polmove in policy_moves[:5]]) size = game.board_size policy_grid = var_to_grid(cn.policy, size) # type: List[List[float]] top_policy_move = policy_moves[0][1] ai_thoughts += f"Using policy based strategy, base top 5 moves are {fmt_moves(policy_moves[:5])}. " if (ai_mode == AI_POLICY and cn.depth <= ai_settings["opening_moves"] ) or (ai_mode in [AI_LOCAL, AI_TENUKI] and not (cn.move and cn.move.coords)): ai_mode = AI_WEIGHTED ai_thoughts += f"Strategy override, using policy-weighted strategy instead. " ai_settings = { "pick_override": 0.9, "weaken_fac": 1, "lower_bound": 0.02 } if top_5_pass: aimove = top_policy_move ai_thoughts += "Playing top one because one of them is pass." elif ai_mode == AI_POLICY: aimove = top_policy_move ai_thoughts += f"Playing top policy move {aimove.gtp()}." else: # weighted or pick-based legal_policy_moves = [(pol, mv) for pol, mv in policy_moves if not mv.is_pass and pol > 0] board_squares = size[0] * size[1] if ai_mode == AI_RANK: # calibrated, override from 0.8 at start to ~0.4 at full board override = 0.8 * ( 1 - 0.5 * (board_squares - len(legal_policy_moves)) / board_squares) else: override = ai_settings["pick_override"] if policy_moves[0][0] > override: aimove = top_policy_move ai_thoughts += f"Top policy move has weight > {override:.1%}, so overriding other strategies." elif ai_mode == AI_WEIGHTED: aimove, ai_thoughts = policy_weighted_move( policy_moves, ai_settings["lower_bound"], ai_settings["weaken_fac"]) elif ai_mode in AI_STRATEGIES_PICK: if ai_mode != AI_RANK: n_moves = int(ai_settings["pick_frac"] * len(legal_policy_moves) + ai_settings["pick_n"]) else: n_moves = int( round( board_squares / 361 * 10**(-0.05737 * ai_settings["kyu_rank"] + 1.9482))) if ai_mode in [ AI_INFLUENCE, AI_TERRITORY, AI_LOCAL, AI_TENUKI ]: if cn.depth > ai_settings["endgame"] * board_squares: weighted_coords = [(pol, 1, *mv.coords) for pol, mv in legal_policy_moves] x_ai_thoughts = ( f"Generated equal weights as move number >= {ai_settings['endgame'] * size[0] * size[1]}. " ) n_moves = int( max(n_moves, 0.5 * len(legal_policy_moves))) elif ai_mode in [AI_INFLUENCE, AI_TERRITORY]: weighted_coords, x_ai_thoughts = generate_influence_territory_weights( ai_mode, ai_settings, policy_grid, size) else: # ai_mode in [AI_LOCAL, AI_TENUKI] weighted_coords, x_ai_thoughts = generate_local_tenuki_weights( ai_mode, ai_settings, policy_grid, cn, size) ai_thoughts += x_ai_thoughts else: # ai_mode in [AI_PICK, AI_RANK]: weighted_coords = [(policy_grid[y][x], 1, x, y) for x in range(size[0]) for y in range(size[1]) if policy_grid[y][x] > 0] pick_moves = weighted_selection_without_replacement( weighted_coords, n_moves) ai_thoughts += f"Picked {min(n_moves,len(weighted_coords))} random moves according to weights. " if pick_moves: new_top = [ (p, Move((x, y), player=cn.next_player)) for p, wt, x, y in heapq.nlargest(5, pick_moves) ] aimove = new_top[0][1] ai_thoughts += f"Top 5 among these were {fmt_moves(new_top)} and picked top {aimove.gtp()}. " if new_top[0][0] < pass_policy: ai_thoughts += f"But found pass ({pass_policy:.2%} to be higher rated than {aimove.gtp()} ({new_top[0][0]:.2%}) so will play top policy move instead." aimove = top_policy_move else: aimove = top_policy_move ai_thoughts += f"Pick policy strategy {ai_mode} failed to find legal moves, so is playing top policy move {aimove.gtp()}." else: raise ValueError(f"Unknown Policy-based AI mode {ai_mode}") else: # Engine based move candidate_ai_moves = cn.candidate_moves top_cand = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player) if top_cand.is_pass: # don't play suicidal to balance score - pass when it's best aimove = top_cand ai_thoughts += f"Top move is pass, so passing regardless of strategy." else: if ai_mode == AI_JIGO: sign = cn.player_sign(cn.next_player) jigo_move = min( candidate_ai_moves, key=lambda move: abs(sign * move["scoreLead"] - ai_settings["target_score"])) aimove = Move.from_gtp(jigo_move["move"], player=cn.next_player) ai_thoughts += f"Jigo strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} as closest to 0.5 point win" elif ai_mode == AI_SCORELOSS: c = ai_settings["strength"] moves = [( d["pointsLost"], math.exp(min(200, -c * max(0, d["pointsLost"]))), Move.from_gtp(d["move"], player=cn.next_player), ) for d in candidate_ai_moves] topmove = weighted_selection_without_replacement(moves, 1)[0] aimove = topmove[2] ai_thoughts += f"ScoreLoss strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} (weight {topmove[1]:.3f}, point loss {topmove[0]:.1f}) based on score weights." else: if ai_mode != AI_DEFAULT: game.katrain.log( f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO) ai_thoughts += f"Strategy {ai_mode} not found or unexpected fallback." aimove = top_cand ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move" game.katrain.log(f"AI thoughts: {ai_thoughts}", OUTPUT_DEBUG) played_node = game.play(aimove) played_node.ai_thoughts = ai_thoughts return aimove, played_node
def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode]: cn = game.current_node if ai_mode == AI_HANDICAP: pda = ai_settings["pda"] if ai_settings["automatic"]: n_handicaps = len(game.root.get_list_property("AB", [])) MOVE_VALUE = 14 # could be rules dependent b_stones_advantage = max( n_handicaps - 1, 0) - (cn.komi - MOVE_VALUE / 2) / MOVE_VALUE pda = min(3, max( -3, -b_stones_advantage * (3 / 8))) # max PDA at 8 stone adv, normal 9 stone game is 8.46 handicap_analysis = request_ai_analysis( game, cn, { "playoutDoublingAdvantage": pda, "playoutDoublingAdvantagePla": "BLACK" }) if not handicap_analysis: game.katrain.log(f"Error getting handicap-based move", OUTPUT_ERROR) ai_mode = AI_DEFAULT while not cn.analysis_ready: time.sleep(0.01) game.engines[cn.next_player].check_alive(exception_if_dead=True) ai_thoughts = "" if (ai_mode in AI_STRATEGIES_POLICY) and cn.policy: # pure policy based move policy_moves = cn.policy_ranking pass_policy = cn.policy[-1] # dont make it jump around for the last few sensible non pass moves top_5_pass = any([polmove[1].is_pass for polmove in policy_moves[:5]]) size = game.board_size policy_grid = var_to_grid(cn.policy, size) # type: List[List[float]] top_policy_move = policy_moves[0][1] ai_thoughts += f"Using policy based strategy, base top 5 moves are {fmt_moves(policy_moves[:5])}. " if (ai_mode == AI_POLICY and cn.depth <= ai_settings["opening_moves"] ) or (ai_mode in [AI_LOCAL, AI_TENUKI] and not (cn.move and cn.move.coords)): ai_mode = AI_WEIGHTED ai_thoughts += f"Strategy override, using policy-weighted strategy instead. " ai_settings = { "pick_override": 0.9, "weaken_fac": 1, "lower_bound": 0.02 } if top_5_pass: aimove = top_policy_move ai_thoughts += "Playing top one because one of them is pass." elif ai_mode == AI_POLICY: aimove = top_policy_move ai_thoughts += f"Playing top policy move {aimove.gtp()}." else: # weighted or pick-based legal_policy_moves = [(pol, mv) for pol, mv in policy_moves if not mv.is_pass and pol > 0] board_squares = size[0] * size[1] if ai_mode == AI_RANK: # calibrated, override from 0.8 at start to ~0.4 at full board override = 0.8 * ( 1 - 0.5 * (board_squares - len(legal_policy_moves)) / board_squares) overridetwo = 0.85 + max(0, 0.02 * (ai_settings["kyu_rank"] - 8)) else: override = ai_settings["pick_override"] overridetwo = 1.0 if policy_moves[0][0] > override: aimove = top_policy_move ai_thoughts += f"Top policy move has weight > {override:.1%}, so overriding other strategies." elif policy_moves[0][0] + policy_moves[1][0] > overridetwo: aimove = top_policy_move ai_thoughts += ( f"Top two policy moves have cumulative weight > {overridetwo:.1%}, so overriding other strategies." ) elif ai_mode == AI_WEIGHTED: aimove, ai_thoughts = policy_weighted_move( policy_moves, ai_settings["lower_bound"], ai_settings["weaken_fac"]) elif ai_mode in AI_STRATEGIES_PICK: if ai_mode != AI_RANK: n_moves = max( 1, int(ai_settings["pick_frac"] * len(legal_policy_moves) + ai_settings["pick_n"])) else: orig_calib_avemodrank = 0.063015 + 0.7624 * board_squares / ( 10**(-0.05737 * ai_settings["kyu_rank"] + 1.9482)) norm_leg_moves = len(legal_policy_moves) / board_squares modified_calib_avemodrank = ( 0.3931 + 0.6559 * norm_leg_moves * math.exp( -1 * (3.002 * norm_leg_moves * norm_leg_moves - norm_leg_moves - 0.034889 * ai_settings["kyu_rank"] - 0.5097)**2) - 0.01093 * ai_settings["kyu_rank"]) * orig_calib_avemodrank n_moves = board_squares * norm_leg_moves / ( 1.31165 * (modified_calib_avemodrank + 1) - 0.082653) n_moves = max(1, round(n_moves)) if ai_mode in [ AI_INFLUENCE, AI_TERRITORY, AI_LOCAL, AI_TENUKI ]: if cn.depth > ai_settings["endgame"] * board_squares: weighted_coords = [(pol, 1, *mv.coords) for pol, mv in legal_policy_moves] x_ai_thoughts = ( f"Generated equal weights as move number >= {ai_settings['endgame'] * size[0] * size[1]}. " ) n_moves = int( max(n_moves, len(legal_policy_moves) // 2)) elif ai_mode in [AI_INFLUENCE, AI_TERRITORY]: weighted_coords, x_ai_thoughts = generate_influence_territory_weights( ai_mode, ai_settings, policy_grid, size) else: # ai_mode in [AI_LOCAL, AI_TENUKI] weighted_coords, x_ai_thoughts = generate_local_tenuki_weights( ai_mode, ai_settings, policy_grid, cn, size) ai_thoughts += x_ai_thoughts else: # ai_mode in [AI_PICK, AI_RANK]: weighted_coords = [(policy_grid[y][x], 1, x, y) for x in range(size[0]) for y in range(size[1]) if policy_grid[y][x] > 0] pick_moves = weighted_selection_without_replacement( weighted_coords, n_moves) ai_thoughts += f"Picked {min(n_moves,len(weighted_coords))} random moves according to weights. " if pick_moves: new_top = [ (p, Move((x, y), player=cn.next_player)) for p, wt, x, y in heapq.nlargest(5, pick_moves) ] aimove = new_top[0][1] ai_thoughts += f"Top 5 among these were {fmt_moves(new_top)} and picked top {aimove.gtp()}. " if new_top[0][0] < pass_policy: ai_thoughts += f"But found pass ({pass_policy:.2%} to be higher rated than {aimove.gtp()} ({new_top[0][0]:.2%}) so will play top policy move instead." aimove = top_policy_move else: aimove = top_policy_move ai_thoughts += f"Pick policy strategy {ai_mode} failed to find legal moves, so is playing top policy move {aimove.gtp()}." else: raise ValueError(f"Unknown Policy-based AI mode {ai_mode}") else: # Engine based move candidate_ai_moves = cn.candidate_moves if ai_mode == AI_HANDICAP: candidate_ai_moves = handicap_analysis["moveInfos"] top_cand = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player) if top_cand.is_pass and ai_mode not in [ AI_DEFAULT, AI_HANDICAP ]: # don't play suicidal to balance score aimove = top_cand ai_thoughts += f"Top move is pass, so passing regardless of strategy. " else: if ai_mode == AI_JIGO: sign = cn.player_sign(cn.next_player) jigo_move = min( candidate_ai_moves, key=lambda move: abs(sign * move["scoreLead"] - ai_settings["target_score"])) aimove = Move.from_gtp(jigo_move["move"], player=cn.next_player) ai_thoughts += f"Jigo strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} as closest to 0.5 point win" elif ai_mode == AI_SCORELOSS: c = ai_settings["strength"] moves = [( d["pointsLost"], math.exp(min(200, -c * max(0, d["pointsLost"]))), Move.from_gtp(d["move"], player=cn.next_player), ) for d in candidate_ai_moves] topmove = weighted_selection_without_replacement(moves, 1)[0] aimove = topmove[2] ai_thoughts += f"ScoreLoss strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} (weight {topmove[1]:.3f}, point loss {topmove[0]:.1f}) based on score weights." else: if ai_mode not in [AI_DEFAULT, AI_HANDICAP]: game.katrain.log( f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO) ai_thoughts += f"Strategy {ai_mode} not found or unexpected fallback." aimove = top_cand if ai_mode == AI_HANDICAP: ai_thoughts += f"Handicap strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move. PDA based score {cn.format_score(handicap_analysis['rootInfo']['scoreLead'])} and win rate {cn.format_winrate(handicap_analysis['rootInfo']['winrate'])}" else: ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move" game.katrain.log(f"AI thoughts: {ai_thoughts}", OUTPUT_DEBUG) played_node = game.play(aimove) played_node.ai_thoughts = ai_thoughts return aimove, played_node
def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode]: cn = game.current_node if ai_mode == AI_HANDICAP: pda = ai_settings["pda"] if ai_settings["automatic"]: n_handicaps = len(game.root.get_list_property("AB", [])) MOVE_VALUE = 14 # could be rules dependent b_stones_advantage = max( n_handicaps - 1, 0) - (cn.komi - MOVE_VALUE / 2) / MOVE_VALUE pda = min(3, max( -3, -b_stones_advantage * (3 / 8))) # max PDA at 8 stone adv, normal 9 stone game is 8.46 handicap_analysis = request_ai_analysis( game, cn, { "playoutDoublingAdvantage": pda, "playoutDoublingAdvantagePla": "BLACK" }) if not handicap_analysis: game.katrain.log(f"Error getting handicap-based move", OUTPUT_ERROR) ai_mode = AI_DEFAULT while not cn.analysis_complete: time.sleep(0.01) game.engines[cn.next_player].check_alive(exception_if_dead=True) ai_thoughts = "" if (ai_mode in AI_STRATEGIES_POLICY) and cn.policy: # pure policy based move policy_moves = cn.policy_ranking pass_policy = cn.policy[-1] # dont make it jump around for the last few sensible non pass moves top_5_pass = any([polmove[1].is_pass for polmove in policy_moves[:5]]) size = game.board_size policy_grid = var_to_grid(cn.policy, size) # type: List[List[float]] top_policy_move = policy_moves[0][1] ai_thoughts += f"Using policy based strategy, base top 5 moves are {fmt_moves(policy_moves[:5])}. " if (ai_mode == AI_POLICY and cn.depth <= ai_settings["opening_moves"] ) or (ai_mode in [AI_LOCAL, AI_TENUKI] and not (cn.move and cn.move.coords)): ai_mode = AI_WEIGHTED ai_thoughts += f"Strategy override, using policy-weighted strategy instead. " ai_settings = { "pick_override": 0.9, "weaken_fac": 1, "lower_bound": 0.02 } if top_5_pass: aimove = top_policy_move ai_thoughts += "Playing top one because one of them is pass." elif ai_mode == AI_POLICY: aimove = top_policy_move ai_thoughts += f"Playing top policy move {aimove.gtp()}." else: # weighted or pick-based legal_policy_moves = [(pol, mv) for pol, mv in policy_moves if not mv.is_pass and pol > 0] board_squares = size[0] * size[1] if ai_mode == AI_RANK: # calibrated, override from 0.8 at start to ~0.4 at full board override = 0.8 * ( 1 - 0.5 * (board_squares - len(legal_policy_moves)) / board_squares) overridetwo = 0.85 + max(0, 0.02 * (ai_settings["kyu_rank"] - 8)) else: override = ai_settings["pick_override"] overridetwo = 1.0 if policy_moves[0][0] > override: aimove = top_policy_move ai_thoughts += f"Top policy move has weight > {override:.1%}, so overriding other strategies." elif policy_moves[0][0] + policy_moves[1][0] > overridetwo: aimove = top_policy_move ai_thoughts += ( f"Top two policy moves have cumulative weight > {overridetwo:.1%}, so overriding other strategies." ) elif ai_mode == AI_WEIGHTED: aimove, ai_thoughts = policy_weighted_move( policy_moves, ai_settings["lower_bound"], ai_settings["weaken_fac"]) elif ai_mode in AI_STRATEGIES_PICK: if ai_mode != AI_RANK: n_moves = max( 1, int(ai_settings["pick_frac"] * len(legal_policy_moves) + ai_settings["pick_n"])) else: orig_calib_avemodrank = 0.063015 + 0.7624 * board_squares / ( 10**(-0.05737 * ai_settings["kyu_rank"] + 1.9482)) norm_leg_moves = len(legal_policy_moves) / board_squares modified_calib_avemodrank = ( 0.3931 + 0.6559 * norm_leg_moves * math.exp( -1 * (3.002 * norm_leg_moves * norm_leg_moves - norm_leg_moves - 0.034889 * ai_settings["kyu_rank"] - 0.5097)**2) - 0.01093 * ai_settings["kyu_rank"]) * orig_calib_avemodrank n_moves = board_squares * norm_leg_moves / ( 1.31165 * (modified_calib_avemodrank + 1) - 0.082653) n_moves = max(1, round(n_moves)) if ai_mode in [ AI_INFLUENCE, AI_TERRITORY, AI_LOCAL, AI_TENUKI ]: if cn.depth > ai_settings["endgame"] * board_squares: weighted_coords = [(pol, 1, *mv.coords) for pol, mv in legal_policy_moves] x_ai_thoughts = ( f"Generated equal weights as move number >= {ai_settings['endgame'] * size[0] * size[1]}. " ) n_moves = int( max(n_moves, len(legal_policy_moves) // 2)) elif ai_mode in [AI_INFLUENCE, AI_TERRITORY]: weighted_coords, x_ai_thoughts = generate_influence_territory_weights( ai_mode, ai_settings, policy_grid, size) else: # ai_mode in [AI_LOCAL, AI_TENUKI] weighted_coords, x_ai_thoughts = generate_local_tenuki_weights( ai_mode, ai_settings, policy_grid, cn, size) ai_thoughts += x_ai_thoughts else: # ai_mode in [AI_PICK, AI_RANK]: weighted_coords = [(policy_grid[y][x], 1, x, y) for x in range(size[0]) for y in range(size[1]) if policy_grid[y][x] > 0] pick_moves = weighted_selection_without_replacement( weighted_coords, n_moves) ai_thoughts += f"Picked {min(n_moves,len(weighted_coords))} random moves according to weights. " if pick_moves: new_top = [ (p, Move((x, y), player=cn.next_player)) for p, wt, x, y in heapq.nlargest(5, pick_moves) ] aimove = new_top[0][1] ai_thoughts += f"Top 5 among these were {fmt_moves(new_top)} and picked top {aimove.gtp()}. " if new_top[0][0] < pass_policy: ai_thoughts += f"But found pass ({pass_policy:.2%} to be higher rated than {aimove.gtp()} ({new_top[0][0]:.2%}) so will play top policy move instead." aimove = top_policy_move else: aimove = top_policy_move ai_thoughts += f"Pick policy strategy {ai_mode} failed to find legal moves, so is playing top policy move {aimove.gtp()}." else: raise ValueError(f"Unknown Policy-based AI mode {ai_mode}") else: # Engine based move candidate_ai_moves = cn.candidate_moves if ai_mode == AI_HANDICAP: candidate_ai_moves = handicap_analysis["moveInfos"] top_cand = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player) if top_cand.is_pass and ai_mode not in [ AI_DEFAULT, AI_HANDICAP, ]: # don't play suicidal to balance score aimove = top_cand ai_thoughts += f"Top move is pass, so passing regardless of strategy. " else: if ai_mode == AI_JIGO: sign = cn.player_sign(cn.next_player) jigo_move = min( candidate_ai_moves, key=lambda move: abs(sign * move["scoreLead"] - ai_settings["target_score"])) aimove = Move.from_gtp(jigo_move["move"], player=cn.next_player) ai_thoughts += f"Jigo strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} as closest to 0.5 point win" elif ai_mode == AI_SCORELOSS: c = ai_settings["strength"] moves = [( d["pointsLost"], math.exp(min(200, -c * max(0, d["pointsLost"]))), Move.from_gtp(d["move"], player=cn.next_player), ) for d in candidate_ai_moves] topmove = weighted_selection_without_replacement(moves, 1)[0] aimove = topmove[2] ai_thoughts += f"ScoreLoss strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} (weight {topmove[1]:.3f}, point loss {topmove[0]:.1f}) based on score weights." elif ai_mode in [AI_SIMPLE_OWNERSHIP, AI_SETTLE_STONES]: stones_with_player = {(*s.coords, s.player) for s in game.stones} next_player_sign = cn.player_sign(cn.next_player) if ai_mode == AI_SIMPLE_OWNERSHIP: def settledness(d, player_sign, player): return sum([ abs(o) for o in d["ownership"] if player_sign * o > 0 ]) else: board_size_x, board_size_y = game.board_size def settledness(d, player_sign, player): ownership_grid = var_to_grid( d["ownership"], (board_size_x, board_size_y)) return sum([ abs(ownership_grid[s.coords[0]][s.coords[1]]) for s in game.stones if s.player == player ]) def is_attachment(move): if move.is_pass: return False attach_opponent_stones = sum( (move.coords[0] + dx, move.coords[1] + dy, cn.player) in stones_with_player for dx in [-1, 0, 1] for dy in [-1, 0, 1] if abs(dx) + abs(dy) == 1) nearby_own_stones = sum( (move.coords[0] + dx, move.coords[1] + dy, cn.next_player) in stones_with_player for dx in [-2, 0, 1, 2] for dy in [-2 - 1, 0, 1, 2] if abs(dx) + abs(dy) <= 2 # allows clamps/jumps ) return attach_opponent_stones >= 1 and nearby_own_stones == 0 def is_tenuki(d): return not d.is_pass and not any( not node or not node.move or node.move.is_pass or max( abs(last_c - cand_c) for last_c, cand_c in zip( node.move.coords, d.coords)) < 5 for node in [cn, cn.parent]) moves_with_settledness = sorted( [( move, settledness(d, next_player_sign, cn.next_player), settledness(d, -next_player_sign, cn.player), is_attachment(move), is_tenuki(move), d, ) for d in candidate_ai_moves if d["pointsLost"] < ai_settings["max_points_lost"] and "ownership" in d and ( d["order"] <= 1 or d["visits"] >= ai_settings.get("min_visits", 1)) for move in [Move.from_gtp(d["move"], player=cn.next_player)] if not (move.is_pass and d["pointsLost"] > 0.75)], key=lambda t: t[5]["pointsLost"] + ai_settings[ "attach_penalty"] * t[3] + ai_settings[ "tenuki_penalty"] * t[4] - ai_settings[ "settled_weight"] * (t[1] + ai_settings["opponent_fac"] * t[2]), ) if moves_with_settledness: cands = [ f"{move.gtp()} ({d['pointsLost']:.1f} pt lost, {d['visits']} visits, {settled:.1f} settledness, {oppsettled:.1f} opponent settledness{', attachment' if isattach else ''}{', tenuki' if istenuki else ''})" for move, settled, oppsettled, isattach, istenuki, d in moves_with_settledness[:5] ] ai_thoughts += f"{ai_mode} strategy. Top 5 Candidates {', '.join(cands)} " aimove = moves_with_settledness[0][0] else: raise (Exception( "No moves found - are you using an older KataGo with no per-move ownership info?" )) else: if ai_mode not in [AI_DEFAULT, AI_HANDICAP]: game.katrain.log( f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO) ai_thoughts += f"Strategy {ai_mode} not found or unexpected fallback." aimove = top_cand if ai_mode == AI_HANDICAP: ai_thoughts += f"Handicap strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move. PDA based score {cn.format_score(handicap_analysis['rootInfo']['scoreLead'])} and win rate {cn.format_winrate(handicap_analysis['rootInfo']['winrate'])}" else: ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move" game.katrain.log(f"AI thoughts: {ai_thoughts}", OUTPUT_DEBUG) played_node = game.play(aimove) played_node.ai_thoughts = ai_thoughts return aimove, played_node
def analyze_extra(self, mode, **kwargs): stones = {s.coords for s in self.stones} cn = self.current_node engine = self.engines[cn.next_player] Clock.schedule_once(self.katrain.analysis_controls.hints.activate, 0) if mode == "extra": if kwargs.get("continuous", False): visits = max(engine.config["max_visits"], math.ceil(cn.analysis_visits_requested * 1.25)) else: visits = cn.analysis_visits_requested + engine.config[ "max_visits"] self.katrain.controls.set_status( i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS) cn.analyze(engine, visits=visits, priority=-1_000, time_limit=False) return if mode == "game": nodes = self.root.nodes_in_tree if "visits" in kwargs: visits = kwargs["visits"] else: min_visits = min(node.analysis_visits_requested for node in nodes) visits = min_visits + engine.config["max_visits"] for node in nodes: node.analyze(engine, visits=visits, priority=-1_000_000, time_limit=False) self.katrain.controls.set_status( i18n._("game re-analysis").format(visits=visits), STATUS_ANALYSIS) return elif mode == "sweep": board_size_x, board_size_y = self.board_size if cn.analysis_ready: policy_grid = (var_to_grid(self.current_node.policy, size=(board_size_x, board_size_y)) if self.current_node.policy else None) analyze_moves = sorted( [ Move(coords=(x, y), player=cn.next_player) for x in range(board_size_x) for y in range(board_size_y) if (policy_grid is None and (x, y) not in stones) or policy_grid[y][x] >= 0 ], key=lambda mv: -policy_grid[mv.coords[1]][mv.coords[0]], ) else: analyze_moves = [ Move(coords=(x, y), player=cn.next_player) for x in range(board_size_x) for y in range(board_size_y) if (x, y) not in stones ] visits = engine.config["fast_visits"] self.katrain.controls.set_status( i18n._("sweep analysis").format(visits=visits), STATUS_ANALYSIS) priority = -1_000_000_000 elif mode == "pass": board_size_x, board_size_y = self.board_size analyze_moves = [Move(coords=None, player=cn.next_player)] visits = 4 * engine.config["fast_visits"] self.katrain.controls.set_status( i18n._("pass analysis").format(visits=visits), STATUS_ANALYSIS) priority = -1_000_000_000 elif mode == "equalize": if not cn.analysis_ready: self.katrain.controls.set_status( i18n._("wait-before-equalize"), STATUS_INFO, self.current_node) return analyze_moves = [ Move.from_gtp(gtp, player=cn.next_player) for gtp, _ in cn.analysis["moves"].items() ] visits = max(d["visits"] for d in cn.analysis["moves"].values()) self.katrain.controls.set_status( i18n._("equalizing analysis").format(visits=visits), STATUS_ANALYSIS) priority = -1_000 else: raise ValueError("Invalid analysis mode") for move in analyze_moves: cn.analyze(engine, priority, visits=visits, refine_move=move, time_limit=False ) # explicitly requested so take as long as you need
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()