Spaces:
Sleeping
Sleeping
File size: 8,281 Bytes
57e0211 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | """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
|