turnabout-bench / tests /test_engine.py
王致渊
Initial commit
57e0211
Raw
History Blame Contribute Delete
8.28 kB
"""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