"""Tests for standing analysis — trailing mode detection. 来源:STRATEGY_IMPROVEMENT_REPORT_20260714.md §5.7 """ from __future__ import annotations import pytest from werewolftown.config import StrategyConfig from werewolftown.features.standing import is_trailing, projected_gap_to_leader from werewolftown.state.game_state import GameState, PendingLedger, PlayerView @pytest.fixture def make_state(): def _make( player_id: str = "me", round: int = 2, cash: int = 50000, plots: frozenset[int] | None = None, shops: tuple[str, ...] | None = None, built_on: dict[int, str] | None = None, other_players: dict[str, PlayerView] | None = None, ) -> GameState: return GameState( player_id=player_id, round=round, cash=cash, initial_cash=50000, plots=plots or frozenset(), shops=shops or (), built_on=built_on or {}, other_players=other_players or {}, pending=PendingLedger(), ) return _make @pytest.fixture def cfg() -> StrategyConfig: return StrategyConfig({ "standing": { "trailing_gap_threshold": 50000, "trailing_rank_threshold": 2, } }) class TestProjectedGapToLeader: def test_leading_negative_gap(self, make_state): """When leading, gap should be negative.""" state = make_state( cash=300000, other_players={ "opp": PlayerView(name="opp", cash=100000, plots=frozenset(), shops=()), }, ) gap = projected_gap_to_leader(state) assert gap < 0 def test_trailing_positive_gap(self, make_state): """When trailing, gap should be positive.""" state = make_state( cash=100000, other_players={ "opp": PlayerView(name="opp", cash=300000, plots=frozenset(), shops=()), }, ) gap = projected_gap_to_leader(state) assert gap > 0 assert gap == 200000 def test_no_opponents_zero_gap(self, make_state): """No opponents → gap is negative (we're the only player).""" state = make_state(cash=100000) gap = projected_gap_to_leader(state) assert gap < 0 # No opponents = effectively leading class TestIsTrailing: def test_trailing_when_far_behind(self, make_state, cfg): """Trailing when gap > threshold and rank >= 2.""" state = make_state( cash=100000, other_players={ "opp1": PlayerView(name="opp1", cash=300000, plots=frozenset(), shops=()), "opp2": PlayerView(name="opp2", cash=200000, plots=frozenset(), shops=()), }, ) assert is_trailing(state, cfg) is True def test_not_trailing_when_leading(self, make_state, cfg): """Not trailing when leading.""" state = make_state( cash=500000, other_players={ "opp": PlayerView(name="opp", cash=100000, plots=frozenset(), shops=()), }, ) assert is_trailing(state, cfg) is False def test_not_trailing_when_gap_small(self, make_state, cfg): """Not trailing when gap < threshold.""" state = make_state( cash=190000, other_players={ "opp": PlayerView(name="opp", cash=200000, plots=frozenset(), shops=()), }, ) # Gap = 10k < 50k threshold assert is_trailing(state, cfg) is False def test_not_trailing_no_opponents(self, make_state, cfg): """Not trailing with no opponents.""" state = make_state(cash=100000) assert is_trailing(state, cfg) is False def test_trailing_r4_uses_projection(self, make_state, cfg): """R4 uses projected ranking.""" state = make_state( round=4, cash=100000, plots=frozenset({1}), shops=("猫天天",), built_on={}, other_players={ "opp": PlayerView( name="opp", cash=400000, plots=frozenset({2}), shops=(), ), }, ) # In R4, opponent has 300k more cash → definitely trailing assert is_trailing(state, cfg) is True