"""Tests for the core game engine.""" import json from pathlib import Path import pytest from turnabout.core.actions import Action, ActionType from turnabout.core.engine import TurnaboutEngine from turnabout.core.schema import CaseData from turnabout.core.scoring import MetricsComputer from turnabout.core.state import GamePhase CASES_DIR = Path(__file__).parent.parent / "turnabout" / "cases" @pytest.fixture def case(): path = CASES_DIR / "stolen_prototype.json" if not path.exists(): pytest.skip("stolen_prototype.json not yet created") with open(path) as f: return CaseData(**json.load(f)) class TestEasyMode: def test_starts_in_court(self, case): engine = TurnaboutEngine(case, difficulty="easy") state, obs = engine.reset() assert state.phase == GamePhase.COURT_TESTIMONY assert "COURT" in obs def test_all_evidence_available(self, case): engine = TurnaboutEngine(case, difficulty="easy") state, _ = engine.reset() assert len(state.inventory) == len(case.evidence) def test_advance_to_cross_exam(self, case): engine = TurnaboutEngine(case, difficulty="easy") state, _ = engine.reset() assert state.phase == GamePhase.COURT_TESTIMONY state, obs, _, _, _ = engine.step(Action(ActionType.NEXT_STATEMENT)) assert state.phase == GamePhase.COURT_CROSS_EXAM assert "CROSS-EXAMINATION" in obs def test_press_statement(self, case): engine = TurnaboutEngine(case, difficulty="easy") state, _ = engine.reset() engine.step(Action(ActionType.NEXT_STATEMENT)) state, obs, _, _, _ = engine.step(Action(ActionType.PRESS, target_id="patrol_1")) assert "HOLD IT" in obs assert "patrol_1" in state.statements_pressed def test_correct_contradiction_advances(self, case): engine = TurnaboutEngine(case, difficulty="easy") engine.reset() engine.step(Action(ActionType.NEXT_STATEMENT)) state, obs, reward, done, info = engine.step( Action(ActionType.PRESENT_EVIDENCE, target_id="patrol_schedule") ) assert "OBJECTION" in obs assert reward > 0 assert "patrol_1" in state.contradictions_found def test_wrong_evidence_gives_penalty(self, case): engine = TurnaboutEngine(case, difficulty="easy") engine.reset() engine.step(Action(ActionType.NEXT_STATEMENT)) state, obs, reward, done, _ = engine.step( Action(ActionType.PRESENT_EVIDENCE, target_id="incident_report") ) assert state.penalties == 1 assert reward < 0 assert "Penalty" in obs def test_too_many_penalties_loses(self, case): engine = TurnaboutEngine(case, difficulty="easy") engine.reset() engine.step(Action(ActionType.NEXT_STATEMENT)) for _ in range(5): state, obs, _, done, _ = engine.step( Action(ActionType.PRESENT_EVIDENCE, target_id="incident_report") ) if done: break assert state.phase == GamePhase.VERDICT_LOSE assert done assert "GUILTY" in obs def test_full_win_path(self, case): engine = TurnaboutEngine(case, difficulty="easy") engine.reset() # Testimony -> cross-exam engine.step(Action(ActionType.NEXT_STATEMENT)) # Find first contradiction: patrol_schedule vs patrol_1 engine.step(Action(ActionType.PRESENT_EVIDENCE, target_id="patrol_schedule")) # New testimony starts, advance to cross-exam engine.step(Action(ActionType.NEXT_STATEMENT)) # Press revised_2 to reveal revised_2b engine.step(Action(ActionType.NEXT_STATEMENT)) # move to stmt index 1 (revised_2) state, obs, _, _, _ = engine.step(Action(ActionType.PRESS, target_id="revised_2")) assert "revised_2b" in state.revealed_statements or "amend" in obs.lower() # Navigate to revised_2b and present cloning_device engine.step(Action(ActionType.NEXT_STATEMENT)) # to revised_2b state, obs, _, done, _ = engine.step( Action(ActionType.PRESENT_EVIDENCE, target_id="cloning_device") ) assert "OBJECTION" in obs assert "revised_2b" in state.contradictions_found # Navigate to revised_3 and present alex_alibi engine.step(Action(ActionType.NEXT_STATEMENT)) # to revised_3 state, obs, reward, done, info = engine.step( Action(ActionType.PRESENT_EVIDENCE, target_id="alex_alibi") ) assert "OBJECTION" in obs assert done assert state.phase == GamePhase.VERDICT_WIN assert "NOT GUILTY" in obs class TestHardMode: def test_starts_in_investigation(self, case): engine = TurnaboutEngine(case, difficulty="hard") state, obs = engine.reset() assert state.phase == GamePhase.INVESTIGATION assert "INVESTIGATION" in obs def test_pre_given_evidence_only(self, case): engine = TurnaboutEngine(case, difficulty="hard") state, _ = engine.reset() pre_given = {e.id for e in case.get_pre_given_evidence()} assert state.inventory == pre_given def test_cannot_go_to_court_without_evidence(self, case): engine = TurnaboutEngine(case, difficulty="hard") engine.reset() valid = engine.get_valid_actions() court_action = Action(ActionType.GO_TO_COURT) assert court_action not in valid def test_move_between_locations(self, case): engine = TurnaboutEngine(case, difficulty="hard") state, _ = engine.reset() start = state.current_location_id # Move to a connected location valid = engine.get_valid_actions() move_actions = [a for a in valid if a.action_type == ActionType.MOVE] assert len(move_actions) > 0 target = move_actions[0].target_id state, obs, _, _, _ = engine.step(move_actions[0]) assert state.current_location_id == target def test_examine_finds_evidence(self, case): engine = TurnaboutEngine(case, difficulty="hard") engine.reset() # Navigate to security_office and examine filing_cabinet engine.step(Action(ActionType.MOVE, target_id="lab")) engine.step(Action(ActionType.MOVE, target_id="security_office")) state, obs, reward, _, _ = engine.step( Action(ActionType.EXAMINE, target_id="filing_cabinet") ) assert "patrol_schedule" in state.inventory assert reward > 0 def test_talk_grants_evidence(self, case): engine = TurnaboutEngine(case, difficulty="hard") state, _ = engine.reset() if state.current_location_id != "detention_center": engine.step(Action(ActionType.MOVE, target_id="detention_center")) state, obs, _, _, _ = engine.step(Action(ActionType.TALK, target_id="alex_park")) assert "alex_alibi" in state.inventory class TestMetrics: def test_compute_on_win(self, case): engine = TurnaboutEngine(case, difficulty="easy") mc = MetricsComputer(engine) state, _ = engine.reset() engine.step(Action(ActionType.NEXT_STATEMENT)) _, _, r, _, _ = engine.step(Action(ActionType.PRESENT_EVIDENCE, target_id="patrol_schedule")) mc.accumulate_reward(r) engine.step(Action(ActionType.NEXT_STATEMENT)) engine.step(Action(ActionType.NEXT_STATEMENT)) _, _, r, _, _ = engine.step(Action(ActionType.PRESS, target_id="revised_2")) mc.accumulate_reward(r) engine.step(Action(ActionType.NEXT_STATEMENT)) _, _, r, _, _ = engine.step( Action(ActionType.PRESENT_EVIDENCE, target_id="cloning_device") ) mc.accumulate_reward(r) engine.step(Action(ActionType.NEXT_STATEMENT)) _, _, r, done, _ = engine.step( Action(ActionType.PRESENT_EVIDENCE, target_id="alex_alibi") ) mc.accumulate_reward(r) metrics = mc.compute(engine.state) assert metrics.won assert metrics.contradiction_accuracy == 1.0 assert metrics.wrong_presentations == 0 assert metrics.contradictions_found == 3