Spaces:
Sleeping
Sleeping
| """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" | |
| 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 | |