From 91b8846894972bd7e6e5b44511ed795aef7741c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Wed, 6 May 2026 22:52:04 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=95=EC=B1=85=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=EB=A1=9C=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/classic-port-notes.md | 2 +- .../games/classic/__init__.py | 14 +++---- .../games/classic/bots/__init__.py | 6 +-- .../games/classic/bots/base.py | 5 ++- .../games/classic/bots/heuristic.py | 6 +-- .../games/classic/bots/passive.py | 5 ++- .../games/classic/bots/random.py | 6 +-- .../games/classic/bots/registry.py | 12 +++--- .../games/classic/evaluation.py | 40 +++++++++---------- .../classic/{interfaces.py => policy.py} | 6 +-- .../games/classic/pygame_pvp.py | 4 +- tests/games/classic/test_bots.py | 8 ++-- tests/games/classic/test_evaluation.py | 6 +-- tests/games/classic/test_public_api.py | 2 +- 14 files changed, 62 insertions(+), 60 deletions(-) rename src/coolrl_lost_cities/games/classic/{interfaces.py => policy.py} (64%) diff --git a/docs/classic-port-notes.md b/docs/classic-port-notes.md index 0acfb89..ecd1585 100644 --- a/docs/classic-port-notes.md +++ b/docs/classic-port-notes.md @@ -24,7 +24,7 @@ src/coolrl_lost_cities/ snapshots.py evaluation.py env.py - interfaces.py + policy.py bots/ pygame_pvp.py fixtures/ diff --git a/src/coolrl_lost_cities/games/classic/__init__.py b/src/coolrl_lost_cities/games/classic/__init__.py index 3dab106..760c788 100644 --- a/src/coolrl_lost_cities/games/classic/__init__.py +++ b/src/coolrl_lost_cities/games/classic/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations from .bots import ( - LostCitiesBot, + LostCitiesPolicy, available_bot_names, build_bot, ) @@ -11,8 +11,8 @@ from .evaluation import ( MatchEvalRecord, MatchResult, TimingResult, - evaluate_bot, - make_bot_factory, + evaluate_policy, + make_policy_factory, play_game_for_evaluation, play_match, ) @@ -22,13 +22,13 @@ from .game import ( LostCitiesConfig, classic_config, ) -from .interfaces import Snapshot +from .snapshots import Snapshot __all__ = [ "GameState", "GameResult", "IllegalMoveError", - "LostCitiesBot", + "LostCitiesPolicy", "LostCitiesConfig", "LostCitiesEnv", "MatchEvalRecord", @@ -38,8 +38,8 @@ __all__ = [ "available_bot_names", "build_bot", "classic_config", - "evaluate_bot", - "make_bot_factory", + "evaluate_policy", + "make_policy_factory", "play_game_for_evaluation", "play_match", ] diff --git a/src/coolrl_lost_cities/games/classic/bots/__init__.py b/src/coolrl_lost_cities/games/classic/bots/__init__.py index c497f92..2c38d1a 100644 --- a/src/coolrl_lost_cities/games/classic/bots/__init__.py +++ b/src/coolrl_lost_cities/games/classic/bots/__init__.py @@ -1,15 +1,15 @@ from __future__ import annotations -from ..interfaces import BotInput, LostCitiesBot +from ..policy import LostCitiesPolicy, PolicyInput from .heuristic import SafeHeuristicBot from .passive import PassiveDiscardBot from .random import RandomBot from .registry import DEFAULT_BOT, available_bot_names, build_bot __all__ = [ - "BotInput", + "PolicyInput", "DEFAULT_BOT", - "LostCitiesBot", + "LostCitiesPolicy", "PassiveDiscardBot", "RandomBot", "SafeHeuristicBot", diff --git a/src/coolrl_lost_cities/games/classic/bots/base.py b/src/coolrl_lost_cities/games/classic/bots/base.py index 8b36c43..e7c9a2b 100644 --- a/src/coolrl_lost_cities/games/classic/bots/base.py +++ b/src/coolrl_lost_cities/games/classic/bots/base.py @@ -1,7 +1,8 @@ from __future__ import annotations from ..game import GameState -from ..interfaces import BotInput, Snapshot +from ..policy import PolicyInput +from ..snapshots import Snapshot try: import numpy as np @@ -9,7 +10,7 @@ except ImportError as exc: # pragma: no cover raise RuntimeError("numpy is required for Lost Cities bots") from exc -def legal_from_obs(obs_or_state: BotInput) -> np.ndarray: +def legal_from_obs(obs_or_state: PolicyInput) -> np.ndarray: if isinstance(obs_or_state, GameState) or hasattr(obs_or_state, "legal_mask"): return np.asarray(obs_or_state.legal_mask(), dtype=bool) if isinstance(obs_or_state, Snapshot): diff --git a/src/coolrl_lost_cities/games/classic/bots/heuristic.py b/src/coolrl_lost_cities/games/classic/bots/heuristic.py index f1a41af..28737f2 100644 --- a/src/coolrl_lost_cities/games/classic/bots/heuristic.py +++ b/src/coolrl_lost_cities/games/classic/bots/heuristic.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from functools import lru_cache from ..game import Card, GameState, LostCitiesConfig -from ..interfaces import BotInput, LostCitiesBot +from ..policy import LostCitiesPolicy, PolicyInput from .base import first_legal, legal_from_obs try: @@ -146,11 +146,11 @@ def derive_heuristic_config( ) -class SafeHeuristicBot(LostCitiesBot): +class SafeHeuristicBot(LostCitiesPolicy): def __init__(self, params: SafeHeuristicParams | None = None): self.params = params or SafeHeuristicParams() - def act(self, obs_or_state: BotInput) -> int: + def act(self, obs_or_state: PolicyInput) -> int: if not isinstance(obs_or_state, GameState) and not hasattr(obs_or_state, "legal_mask"): LOGGER.debug( "SafeHeuristicBot fallback to first legal: input_type=%s", diff --git a/src/coolrl_lost_cities/games/classic/bots/passive.py b/src/coolrl_lost_cities/games/classic/bots/passive.py index 89dc565..d91d7e2 100644 --- a/src/coolrl_lost_cities/games/classic/bots/passive.py +++ b/src/coolrl_lost_cities/games/classic/bots/passive.py @@ -1,14 +1,15 @@ from __future__ import annotations from ..game import GameState -from ..interfaces import BotInput, Snapshot +from ..policy import PolicyInput +from ..snapshots import Snapshot from .base import first_legal, legal_from_obs class PassiveDiscardBot: """Baseline that avoids opening expeditions whenever discarding is legal.""" - def act(self, obs_or_state: BotInput) -> int: + def act(self, obs_or_state: PolicyInput) -> int: if isinstance(obs_or_state, GameState) or hasattr(obs_or_state, "legal_mask"): return self._act_phase_local( obs_or_state.phase, diff --git a/src/coolrl_lost_cities/games/classic/bots/random.py b/src/coolrl_lost_cities/games/classic/bots/random.py index 887d6ba..9d84663 100644 --- a/src/coolrl_lost_cities/games/classic/bots/random.py +++ b/src/coolrl_lost_cities/games/classic/bots/random.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ..interfaces import BotInput, LostCitiesBot +from ..policy import LostCitiesPolicy, PolicyInput from .base import legal_from_obs try: @@ -9,11 +9,11 @@ except ImportError as exc: # pragma: no cover raise RuntimeError("numpy is required for Lost Cities bots") from exc -class RandomBot(LostCitiesBot): +class RandomBot(LostCitiesPolicy): def __init__(self, seed: int | None = None): self.rng = np.random.default_rng(seed) - def act(self, obs_or_state: BotInput) -> int: + def act(self, obs_or_state: PolicyInput) -> int: legal = legal_from_obs(obs_or_state) legal_indices = np.nonzero(legal)[0] if len(legal_indices) == 0: diff --git a/src/coolrl_lost_cities/games/classic/bots/registry.py b/src/coolrl_lost_cities/games/classic/bots/registry.py index 3976873..9945b18 100644 --- a/src/coolrl_lost_cities/games/classic/bots/registry.py +++ b/src/coolrl_lost_cities/games/classic/bots/registry.py @@ -2,16 +2,16 @@ from __future__ import annotations from collections.abc import Callable -from ..interfaces import LostCitiesBot +from ..policy import LostCitiesPolicy from .heuristic import SafeHeuristicBot from .passive import PassiveDiscardBot from .random import RandomBot BotName = str DEFAULT_BOT: BotName = "random" -BotFactory = Callable[[int | None], LostCitiesBot] +PolicyFactory = Callable[[int | None], LostCitiesPolicy] -BOT_REGISTRY: dict[BotName, BotFactory] = { +BOT_REGISTRY: dict[BotName, PolicyFactory] = { DEFAULT_BOT: RandomBot, "passive-discard": lambda seed: PassiveDiscardBot(), "safe-heuristic": lambda seed: SafeHeuristicBot(), @@ -22,9 +22,9 @@ def available_bot_names() -> list[BotName]: return sorted(BOT_REGISTRY) -def build_bot(name: BotName, *, seed: int | None = None) -> LostCitiesBot: +def build_bot(name: BotName, *, seed: int | None = None) -> LostCitiesPolicy: try: - bot_factory = BOT_REGISTRY[name] + policy_factory = BOT_REGISTRY[name] except KeyError as exc: raise ValueError(f"unknown Lost Cities bot: {name}") from exc - return bot_factory(seed) + return policy_factory(seed) diff --git a/src/coolrl_lost_cities/games/classic/evaluation.py b/src/coolrl_lost_cities/games/classic/evaluation.py index 4a0acc6..ebf89cb 100644 --- a/src/coolrl_lost_cities/games/classic/evaluation.py +++ b/src/coolrl_lost_cities/games/classic/evaluation.py @@ -11,9 +11,9 @@ import numpy as np from .bots import available_bot_names, build_bot from .game import GameState, LostCitiesConfig, classic_config -from .interfaces import LostCitiesBot +from .policy import LostCitiesPolicy -BotFactory = Callable[[int | None], LostCitiesBot] +PolicyFactory = Callable[[int | None], LostCitiesPolicy] MATCH_EVAL_RECORD_TYPE = "lost_cities.classic.eval.match.v1" @@ -134,18 +134,18 @@ class MatchEvalRecord: } -def make_bot_factory(name: str) -> BotFactory: +def make_policy_factory(name: str) -> PolicyFactory: canonical = _canonical_bot_name(name) - def factory(seed: int | None = None) -> LostCitiesBot: + def factory(seed: int | None = None) -> LostCitiesPolicy: return build_bot(canonical, seed=seed) return factory def play_game_for_evaluation( - bot0: LostCitiesBot, - bot1: LostCitiesBot, + policy0: LostCitiesPolicy, + policy1: LostCitiesPolicy, config: LostCitiesConfig, *, seed: int | None = None, @@ -154,12 +154,12 @@ def play_game_for_evaluation( if max_steps <= 0: raise ValueError(f"max_steps must be positive, got {max_steps}") state = GameState.new_game(config, seed=seed) - bots = [bot0, bot1] + policies = [policy0, policy1] steps = 0 for _ in range(max_steps): if state.terminal: break - action = bots[state.current_player].act(state) + action = policies[state.current_player].act(state) state.apply_action(action) steps += 1 timed_out = not state.terminal @@ -180,8 +180,8 @@ def play_game_for_evaluation( def play_match( - bot0_factory: BotFactory, - bot1_factory: BotFactory, + policy0_factory: PolicyFactory, + policy1_factory: PolicyFactory, config: LostCitiesConfig, *, games: int, @@ -203,11 +203,11 @@ def play_match( game_seed = seed + index swap = alternate_seats and index % 2 == 1 if swap: - left = bot1_factory(game_seed * 2) - right = bot0_factory(game_seed * 2 + 1) + left = policy1_factory(game_seed * 2) + right = policy0_factory(game_seed * 2 + 1) else: - left = bot0_factory(game_seed * 2) - right = bot1_factory(game_seed * 2 + 1) + left = policy0_factory(game_seed * 2) + right = policy1_factory(game_seed * 2 + 1) _, result = play_game_for_evaluation( left, @@ -255,9 +255,9 @@ def play_match( ) -def evaluate_bot( - bot_factory: BotFactory, - opponent_factory: BotFactory, +def evaluate_policy( + policy_factory: PolicyFactory, + opponent_factory: PolicyFactory, config: LostCitiesConfig, *, games: int, @@ -265,7 +265,7 @@ def evaluate_bot( max_steps: int = 10_000, ) -> MatchResult: return play_match( - bot_factory, + policy_factory, opponent_factory, config, games=games, @@ -290,8 +290,8 @@ def main(argv: list[str] | None = None) -> None: config = classic_config() alternate_seats = not args.no_alternate_seats result = play_match( - make_bot_factory(args.bot0), - make_bot_factory(args.bot1), + make_policy_factory(args.bot0), + make_policy_factory(args.bot1), config, games=args.games, seed=args.seed, diff --git a/src/coolrl_lost_cities/games/classic/interfaces.py b/src/coolrl_lost_cities/games/classic/policy.py similarity index 64% rename from src/coolrl_lost_cities/games/classic/interfaces.py rename to src/coolrl_lost_cities/games/classic/policy.py index 4d188d3..16134af 100644 --- a/src/coolrl_lost_cities/games/classic/interfaces.py +++ b/src/coolrl_lost_cities/games/classic/policy.py @@ -5,10 +5,10 @@ from typing import Protocol, TypeAlias, runtime_checkable from .game import GameState from .snapshots import Snapshot -BotInput: TypeAlias = dict | GameState | Snapshot +PolicyInput: TypeAlias = dict | GameState | Snapshot @runtime_checkable -class LostCitiesBot(Protocol): - def act(self, obs_or_state: BotInput) -> int: +class LostCitiesPolicy(Protocol): + def act(self, obs_or_state: PolicyInput) -> int: """Choose an action id from the current state or observation.""" diff --git a/src/coolrl_lost_cities/games/classic/pygame_pvp.py b/src/coolrl_lost_cities/games/classic/pygame_pvp.py index d593062..86240e3 100644 --- a/src/coolrl_lost_cities/games/classic/pygame_pvp.py +++ b/src/coolrl_lost_cities/games/classic/pygame_pvp.py @@ -12,7 +12,7 @@ from datetime import datetime from pathlib import Path from typing import Any, Literal -from .bots import DEFAULT_BOT, LostCitiesBot, available_bot_names, build_bot +from .bots import DEFAULT_BOT, LostCitiesPolicy, available_bot_names, build_bot from .game import Card, GameState, LostCitiesConfig, classic_config from .resources import theme_path from .snapshots import Snapshot, snapshot_from_state, snapshot_summary @@ -239,7 +239,7 @@ class LostCitiesGuiApp: def _computer_bot_display_name(self) -> str: return self.computer_bot_label - def _build_computer_bot(self) -> tuple[LostCitiesBot, str]: + def _build_computer_bot(self) -> tuple[LostCitiesPolicy, str]: return build_bot(self.bot_name, seed=self._bot_seed()), self.bot_name def snapshot(self) -> Snapshot: diff --git a/tests/games/classic/test_bots.py b/tests/games/classic/test_bots.py index a4085cf..6d0139a 100644 --- a/tests/games/classic/test_bots.py +++ b/tests/games/classic/test_bots.py @@ -1,7 +1,7 @@ from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig from coolrl_lost_cities.games.classic.bots import ( - LostCitiesBot, + LostCitiesPolicy, RandomBot, SafeHeuristicBot, ) @@ -17,9 +17,9 @@ def _expeditions(config: LostCitiesConfig) -> list[list[list[Card]]]: ] -def test_builtin_bots_implement_lost_cities_bot() -> None: - assert isinstance(RandomBot(1), LostCitiesBot) - assert isinstance(SafeHeuristicBot(), LostCitiesBot) +def test_builtin_bots_implement_lost_cities_policy() -> None: + assert isinstance(RandomBot(1), LostCitiesPolicy) + assert isinstance(SafeHeuristicBot(), LostCitiesPolicy) def test_safe_heuristic_mirror_match_finishes() -> None: diff --git a/tests/games/classic/test_evaluation.py b/tests/games/classic/test_evaluation.py index 0e59556..d56303f 100644 --- a/tests/games/classic/test_evaluation.py +++ b/tests/games/classic/test_evaluation.py @@ -1,7 +1,7 @@ from coolrl_lost_cities.games.classic import ( LostCitiesConfig, build_bot, - make_bot_factory, + make_policy_factory, play_game_for_evaluation, play_match, ) @@ -29,8 +29,8 @@ def test_play_match_alternates_seats_and_reports_rates() -> None: config = LostCitiesConfig(n_colors=3, n_ranks=5, n_handshakes=1, hand_size=5) result = play_match( - make_bot_factory("random"), - make_bot_factory("passive-discard"), + make_policy_factory("random"), + make_policy_factory("passive-discard"), config, games=4, seed=10, diff --git a/tests/games/classic/test_public_api.py b/tests/games/classic/test_public_api.py index bf96f23..846310a 100644 --- a/tests/games/classic/test_public_api.py +++ b/tests/games/classic/test_public_api.py @@ -37,4 +37,4 @@ def test_classic_package_exports_snapshot_alias() -> None: def test_classic_package_exports_bot_registry_helpers() -> None: assert "random" in classic.available_bot_names() - assert isinstance(classic.build_bot("random", seed=1), classic.LostCitiesBot) + assert isinstance(classic.build_bot("random", seed=1), classic.LostCitiesPolicy)