Python 포맷팅과 pre-commit 설정

맥락:
- Python 코드만 대상으로 Ruff 기반 format/lint와 pre-commit hook을 도입한다.
- Cython game.pyx는 Ruff 대상에서 제외해 포맷터 충돌을 피한다.

변경:
- ruff와 pre-commit dev dependency 및 Ruff 설정을 추가했다.
- pre-commit config와 Python format/check 스크립트를 추가했다.
- Ruff format/check --fix 결과로 Python import 정렬과 포맷을 적용했다.

확인:
- uv sync --extra gui
- uv run pre-commit run --all-files
- scripts/check-python.sh
- uv run lost-cities-classic
This commit is contained in:
2026-05-06 19:59:07 +09:00
parent b43bb5917f
commit 12b59211b2
14 changed files with 282 additions and 123 deletions
+13 -9
View File
@@ -1,10 +1,11 @@
from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig
from coolrl_lost_cities.games.classic.bots import (
LostCitiesBot,
RandomBot,
SafeHeuristicBot,
play_game,
)
from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig
from coolrl_lost_cities.games.classic.bots.heuristic import draw_from_discard_action
@@ -162,14 +163,17 @@ def test_safe_heuristic_avoids_opening_weak_fifth_color() -> None:
state.hands[0] = [weak_open, Card(color=4, rank=7), Card(color=0, rank=6)]
state.sort_hand(0)
assert bot._should_open_expedition(
state=state,
player=0,
color=4,
opening_card=weak_open,
derived=bot._derived(state),
deck_left=config.deck_size,
) is False
assert (
bot._should_open_expedition(
state=state,
player=0,
color=4,
opening_card=weak_open,
derived=bot._derived(state),
deck_left=config.deck_size,
)
is False
)
def test_safe_heuristic_prefers_followup_on_started_expedition() -> None:
+5 -16
View File
@@ -3,10 +3,9 @@ import random
from pathlib import Path
import pytest
import coolrl_lost_cities.games.classic as classic
from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig
import coolrl_lost_cities.games.classic as classic
FIXTURE_DIR = Path(classic.__file__).resolve().parent / "fixtures"
@@ -43,13 +42,9 @@ def test_new_game_from_deck_uses_explicit_internal_deck_order() -> None:
def test_snapshot_roundtrip_preserves_json_state() -> None:
state = GameState.new_game(LostCitiesConfig(seed=5))
first_action = next(
index for index, legal in enumerate(state.unified_legal_mask()) if legal
)
first_action = next(index for index, legal in enumerate(state.unified_legal_mask()) if legal)
state.apply_unified_action(first_action)
second_action = next(
index for index, legal in enumerate(state.unified_legal_mask()) if legal
)
second_action = next(index for index, legal in enumerate(state.unified_legal_mask()) if legal)
state.apply_unified_action(second_action)
payload = json.loads(json.dumps(state.to_snapshot()))
@@ -108,11 +103,7 @@ def test_random_games_preserve_python_core_invariants() -> None:
steps = 0
while not state.terminal:
state.validate_invariants()
legal = [
index
for index, is_legal in enumerate(state.unified_legal_mask())
if is_legal
]
legal = [index for index, is_legal in enumerate(state.unified_legal_mask()) if is_legal]
state.apply_unified_action(rng.choice(legal))
steps += 1
assert steps < 1000
@@ -129,8 +120,6 @@ def test_same_seed_and_action_sequence_are_deterministic() -> None:
if left.terminal:
break
action = next(
index for index, is_legal in enumerate(left.unified_legal_mask()) if is_legal
)
action = next(index for index, is_legal in enumerate(left.unified_legal_mask()) if is_legal)
left.apply_unified_action(action)
right.apply_unified_action(action)
+3 -3
View File
@@ -1,7 +1,7 @@
import numpy as np
from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig
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:
@@ -17,8 +17,8 @@ def test_env_observation_uses_fixed_unified_mask() -> None:
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:])
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:
+4 -3
View File
@@ -1,6 +1,7 @@
from coolrl_lost_cities.games.classic.bots import RandomBot
from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig
from coolrl_lost_cities.games.classic.bots import RandomBot
def test_legal_mask_has_action_in_nonterminal_phases() -> None:
state = GameState.new_game(LostCitiesConfig(seed=3))
@@ -39,8 +40,8 @@ def test_unified_legal_mask_has_fixed_shape_across_phases() -> None:
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:])
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:
+7 -2
View File
@@ -1,6 +1,11 @@
import pytest
from coolrl_lost_cities.games.classic.game import Card, GameState, IllegalMoveError, LostCitiesConfig, build_deck
from coolrl_lost_cities.games.classic.game import (
Card,
GameState,
IllegalMoveError,
LostCitiesConfig,
build_deck,
)
def test_deck_generation_count() -> None:
+5 -11
View File
@@ -1,13 +1,13 @@
import json
from pathlib import Path
import random
import subprocess
from pathlib import Path
from coolrl_lost_cities.games.classic.game import GameState, LostCitiesConfig, build_deck
import coolrl_lost_cities.games.classic as classic
from coolrl_lost_cities.games.classic.game import GameState, LostCitiesConfig, build_deck
from coolrl_lost_cities.games.classic.backends.rust import RUST_CORE_DIR
LOST_CITIES_DIR = Path(classic.__file__).resolve().parent
FIXTURE_DIR = LOST_CITIES_DIR / "fixtures"
@@ -81,11 +81,7 @@ def test_rust_randomized_fixture_traces_match_python_core(tmp_path: Path) -> Non
for _ in range(64):
if state.terminal:
break
legal = [
index
for index, is_legal in enumerate(state.unified_legal_mask())
if is_legal
]
legal = [index for index, is_legal in enumerate(state.unified_legal_mask()) if is_legal]
action = rng.choice(legal)
state.apply_unified_action(action)
steps.append({"action": action})
@@ -154,9 +150,7 @@ def test_rust_grpc_contract_is_checked_from_python() -> None:
def test_rust_core_has_no_native_tests_left() -> None:
rust_files = [
path
for path in (RUST_CORE_DIR / "src").rglob("*.rs")
if "target" not in path.parts
path for path in (RUST_CORE_DIR / "src").rglob("*.rs") if "target" not in path.parts
]
for path in rust_files:
text = path.read_text()