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