정책 인터페이스로 네이밍 정리

This commit is contained in:
2026-05-06 22:52:04 +09:00
parent 8763430475
commit 91b8846894
14 changed files with 62 additions and 60 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ src/coolrl_lost_cities/
snapshots.py
evaluation.py
env.py
interfaces.py
policy.py
bots/
pygame_pvp.py
fixtures/
@@ -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",
]
@@ -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",
@@ -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):
@@ -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",
@@ -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,
@@ -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:
@@ -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)
@@ -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,
@@ -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."""
@@ -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:
+4 -4
View File
@@ -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:
+3 -3
View File
@@ -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,
+1 -1
View File
@@ -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)