정책 인터페이스로 네이밍 정리
This commit is contained in:
@@ -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,
|
||||
|
||||
+3
-3
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user