Example #1
0
 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
     ])
Example #2
0
    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)
Example #3
0
 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])
Example #4
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
Example #5
0
    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
Example #6
0
    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()
Example #7
0
    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()
Example #8
0
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
Example #9
0
File: ai.py Project: yibit/katrain
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
Example #10
0
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
Example #11
0
    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
Example #12
0
    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()