맥락: - 새 레포의 첫 범위를 RL 없는 Lost Cities classic 게임 구현으로 잡았다. - 기존 tier0-3 실험 축은 제거하고 classic 5-expedition 룰을 기본값으로 둔다. 변경: - games/classic 아래에 Cython 게임 엔진, env, bots, backend 경계, Rust core와 proto schema를 이식했다. - setuptools/Cython 빌드 설정과 package data, README, classic port notes를 추가했다. - 룰, 점수, 마스크, env, canonical state, bot, Rust parity 테스트를 새 경로로 가져왔다. 확인: - uv run pytest tests/games/classic - uv run lost-cities-classic
59 lines
2.0 KiB
Python
59 lines
2.0 KiB
Python
from coolrl_lost_cities.games.classic.bots import RandomBot
|
|
from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig
|
|
|
|
|
|
def test_legal_mask_has_action_in_nonterminal_phases() -> None:
|
|
state = GameState.new_game(LostCitiesConfig(seed=3))
|
|
while not state.terminal:
|
|
assert any(state.legal_mask())
|
|
action = RandomBot(11).act(state)
|
|
state.apply_action(action)
|
|
|
|
|
|
def test_empty_hand_slots_are_masked() -> None:
|
|
state = GameState.empty(LostCitiesConfig())
|
|
state.hands[0] = [Card(0, 1)]
|
|
mask = state.legal_card_mask()
|
|
assert mask[0] is True
|
|
assert mask[1] is True
|
|
assert all(value is False for value in mask[2:])
|
|
|
|
|
|
def test_empty_discard_pile_draw_is_illegal() -> None:
|
|
state = GameState.empty(LostCitiesConfig())
|
|
state.phase = "draw"
|
|
state.deck = [Card(0, 1)]
|
|
mask = state.legal_draw_mask()
|
|
assert mask[0] is True
|
|
assert all(mask[1 + color] is False for color in range(state.config.n_colors))
|
|
|
|
|
|
def test_unified_legal_mask_has_fixed_shape_across_phases() -> None:
|
|
config = LostCitiesConfig()
|
|
state = GameState.new_game(config, seed=1)
|
|
assert len(state.unified_legal_mask()) == config.action_size
|
|
|
|
action = next(index for index, legal in enumerate(state.legal_mask()) if legal)
|
|
state.apply_action(action)
|
|
|
|
mask = state.unified_legal_mask()
|
|
assert state.phase == "draw"
|
|
assert len(mask) == config.action_size
|
|
assert all(value is False for value in mask[:config.card_action_size])
|
|
assert any(mask[config.card_action_size:])
|
|
|
|
|
|
def test_random_fuzz_invariants() -> None:
|
|
config = LostCitiesConfig(n_colors=3, n_ranks=5, n_handshakes=1, hand_size=5)
|
|
bot = RandomBot(99)
|
|
for seed in range(1000):
|
|
state = GameState.new_game(config, seed=seed)
|
|
steps = 0
|
|
while not state.terminal:
|
|
mask = state.legal_mask()
|
|
assert any(mask)
|
|
action = bot.act(state)
|
|
state.apply_action(action)
|
|
steps += 1
|
|
assert steps < 1000
|