로스트 시티 클래식 코어 이식
맥락: - 새 레포의 첫 범위를 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
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import pytest
|
||||
|
||||
from coolrl_lost_cities.games.classic.game import Card, GameState, IllegalMoveError, LostCitiesConfig, build_deck
|
||||
|
||||
|
||||
def test_deck_generation_count() -> None:
|
||||
config = LostCitiesConfig(n_colors=3, n_ranks=5, n_handshakes=1, hand_size=5)
|
||||
assert len(build_deck(config)) == config.n_colors * (config.n_ranks + config.n_handshakes)
|
||||
|
||||
|
||||
def test_initial_hands_remove_cards_from_deck() -> None:
|
||||
config = LostCitiesConfig(seed=7)
|
||||
state = GameState.new_game(config)
|
||||
assert len(state.hands[0]) == config.hand_size
|
||||
assert len(state.hands[1]) == config.hand_size
|
||||
assert len(state.deck) == config.deck_size - 2 * config.hand_size
|
||||
|
||||
|
||||
def test_play_must_be_ascending() -> None:
|
||||
config = LostCitiesConfig()
|
||||
state = GameState.empty(config)
|
||||
state.hands[0] = [Card(0, 2)]
|
||||
state.expeditions[0][0] = [Card(0, 4)]
|
||||
assert state.legal_card_mask()[0] is False
|
||||
|
||||
|
||||
def test_handshake_after_number_forbidden() -> None:
|
||||
config = LostCitiesConfig()
|
||||
state = GameState.empty(config)
|
||||
state.hands[0] = [Card(1, 0)]
|
||||
state.expeditions[0][1] = [Card(1, 1)]
|
||||
assert state.legal_card_mask()[0] is False
|
||||
|
||||
|
||||
def test_cannot_draw_just_discarded_color() -> None:
|
||||
config = LostCitiesConfig()
|
||||
state = GameState.empty(config)
|
||||
state.hands[0] = [Card(2, 2)]
|
||||
state.deck = [Card(0, 1)]
|
||||
state.apply_action(1)
|
||||
mask = state.legal_draw_mask()
|
||||
assert mask[1 + 2] is False
|
||||
|
||||
|
||||
def test_drawing_just_discarded_color_is_rejected() -> None:
|
||||
config = LostCitiesConfig()
|
||||
state = GameState.empty(config)
|
||||
state.hands[0] = [Card(2, 2)]
|
||||
state.deck = [Card(0, 1)]
|
||||
|
||||
state.apply_action(1)
|
||||
|
||||
with pytest.raises(IllegalMoveError):
|
||||
state.apply_action(1 + 2)
|
||||
|
||||
|
||||
def test_discarded_color_can_be_drawn_after_turn_advances() -> None:
|
||||
config = LostCitiesConfig()
|
||||
state = GameState.empty(config)
|
||||
state.hands[0] = [Card(2, 2)]
|
||||
state.hands[1] = [Card(0, 1)]
|
||||
state.deck = [Card(1, 1), Card(1, 2)]
|
||||
state.apply_action(1)
|
||||
state.apply_action(0)
|
||||
assert state.current_player == 1
|
||||
state.apply_action(1)
|
||||
assert state.phase == "draw"
|
||||
assert state.legal_draw_mask()[1 + 2] is True
|
||||
|
||||
|
||||
def test_discarded_card_is_removed_when_drawn_later() -> None:
|
||||
config = LostCitiesConfig()
|
||||
state = GameState.empty(config)
|
||||
state.hands[0] = [Card(2, 2)]
|
||||
state.hands[1] = [Card(0, 1)]
|
||||
state.deck = [Card(1, 1), Card(1, 2)]
|
||||
|
||||
state.apply_action(1)
|
||||
assert state.discards[2] == [Card(2, 2)]
|
||||
|
||||
state.apply_action(0)
|
||||
assert state.current_player == 1
|
||||
|
||||
state.apply_action(1)
|
||||
state.apply_action(1 + 2)
|
||||
|
||||
assert state.discards[2] == []
|
||||
assert Card(2, 2) in state.hands[1]
|
||||
|
||||
|
||||
def test_deck_exhaustion_ends_after_last_deck_draw() -> None:
|
||||
config = LostCitiesConfig()
|
||||
state = GameState.empty(config)
|
||||
state.hands[0] = [Card(0, 1)]
|
||||
state.deck = [Card(1, 1)]
|
||||
state.apply_action(1)
|
||||
state.apply_action(0)
|
||||
assert state.terminal is True
|
||||
assert len(state.deck) == 0
|
||||
|
||||
|
||||
def test_card_phase_can_end_game_when_no_draw_sources_exist() -> None:
|
||||
config = LostCitiesConfig(n_colors=3, n_ranks=5, n_handshakes=1, hand_size=5)
|
||||
state = GameState.empty(config)
|
||||
state.hands[0] = [Card(0, 1)]
|
||||
state.hands[1] = [Card(1, 1)]
|
||||
state.deck = []
|
||||
state.apply_action(1)
|
||||
assert state.phase == "draw"
|
||||
assert state.terminal is True
|
||||
Reference in New Issue
Block a user