로스트 시티 클래식 코어 이식

맥락:
- 새 레포의 첫 범위를 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:
2026-05-06 19:07:46 +09:00
commit 7df6e42904
46 changed files with 7531 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
import numpy as np
from coolrl_lost_cities.games.classic.env import LostCitiesEnv
from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig
def test_env_observation_uses_fixed_unified_mask() -> None:
config = LostCitiesConfig()
env = LostCitiesEnv(config)
obs = env.reset()
assert obs["legal_mask"].shape == (config.action_size,)
assert env.phase == "card"
card_action = int(np.nonzero(obs["legal_mask"])[0][0])
obs, _, _, _ = env.step(card_action)
assert env.phase == "draw"
assert obs["legal_mask"].shape == (config.action_size,)
assert np.all(obs["legal_mask"][:config.card_action_size] == 0)
assert np.any(obs["legal_mask"][config.card_action_size:])
def test_env_step_accepts_legacy_draw_action_ids() -> None:
config = LostCitiesConfig()
env = LostCitiesEnv(config)
env.state = GameState.empty(config)
env.state.hands[0] = [Card(0, 1)]
env.state.hands[1] = [Card(1, 1)]
env.state.deck = [Card(2, 1), Card(2, 2)]
env.state.phase = "draw"
obs, reward, done, _ = env.step(0)
assert obs["legal_mask"].shape == (config.action_size,)
assert reward == 0.0
assert done is False
assert env.current_player == 1
assert env.phase == "card"
def test_terminal_reward_is_relative_to_actor() -> None:
config = LostCitiesConfig(
n_colors=2,
n_ranks=1,
min_rank=1,
n_handshakes=0,
hand_size=1,
expedition_penalty=0,
bonus_threshold=99,
)
env = LostCitiesEnv(config)
env.state = GameState.empty(config)
env.state.current_player = 1
env.state.phase = "draw"
env.state.deck = [Card(1, 1)]
env.state.expeditions[1][0] = [Card(0, 1)]
_, reward, done, _ = env.step(config.card_action_size)
assert done is True
assert reward == 1.0