File size: 8,432 Bytes
6c3d778
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
from amongus_env.engine import AmongUsEngine
from amongus_env.models import (
    CallMeeting,
    ClaimKind,
    CompleteTask,
    Kill,
    Move,
    PassMeeting,
    Phase,
    PlayerRole,
    ReportBody,
    Speak,
    Vote,
    Winner,
)


def test_reset_is_deterministic_and_spawns_in_cafeteria() -> None:
    first = AmongUsEngine(seed=7, impostor_ids=["blue"]).reset()
    second = AmongUsEngine(seed=7, impostor_ids=["blue"]).reset()

    assert first == second
    assert first.role is PlayerRole.CREWMATE
    assert first.location == "Cafeteria"
    assert first.phase is Phase.TASKS
    assert "Match reset" in first.message_log[-1]


def test_invalid_movement_is_penalized_without_changing_location() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"])
    engine.reset()

    observation = engine.step(Move(room="Navigation"))

    assert observation.location == "Cafeteria"
    assert observation.reward == -0.1
    assert "Invalid move" in observation.message_log[-1]


def test_location_history_tracks_reset_and_valid_moves_only() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"])
    engine.reset()

    engine.step(Move(room="Electrical"))
    engine.step(Move(room="Navigation"))

    assert engine.location_history["red"] == ["Cafeteria", "Electrical"]
    assert engine.location_history["blue"] == ["Cafeteria"]


def test_visibility_only_includes_living_players_in_same_room() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"])
    engine.reset()
    engine.step(Move(room="Electrical"))

    engine.players["blue"].location = "Electrical"
    engine.players["green"].location = "MedBay"
    engine.players["yellow"].location = "Electrical"
    engine.players["yellow"].alive = False

    observation = engine.observe()

    assert observation.visible_players == ["blue"]


def test_crewmate_completes_current_room_task_for_dense_reward() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"])
    engine.reset()
    engine.step(Move(room="Electrical"))

    observation = engine.step(CompleteTask())

    assert observation.reward == 0.2
    assert observation.task_list[0].completed is True
    assert "Completed task" in observation.message_log[-1]


def test_impostor_kill_marks_body_and_rewards_controlled_impostor() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["red"])
    engine.reset()
    engine.players["blue"].location = "Cafeteria"

    observation = engine.step(Kill(target_id="blue"))

    assert observation.reward == 0.5
    assert engine.players["blue"].alive is False
    assert "blue" in engine.dead_bodies
    assert "Killed blue" in observation.message_log[-1]


def test_report_body_enters_meeting_phase() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["red"])
    engine.reset()
    engine.step(Kill(target_id="blue"))

    observation = engine.step(ReportBody())

    assert observation.phase is Phase.MEETING
    assert observation.voting_open is False
    assert observation.meeting_turns_remaining == 1
    assert "Reported body" in observation.message_log[-1]


def test_vote_before_discussion_turn_is_illegal() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"])
    engine.reset()
    engine.step(CallMeeting())

    observation = engine.step(Vote(target_id="blue"))

    assert observation.reward == -0.1
    assert observation.phase is Phase.MEETING
    assert observation.voting_open is False
    assert observation.meeting_turns_remaining == 1
    assert "Cannot vote before discussion" in observation.message_log[-1]


def test_pass_opens_voting_without_claim_reward() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"])
    engine.reset()
    engine.step(CallMeeting())

    observation = engine.step(PassMeeting())

    assert observation.reward == 0.0
    assert observation.phase is Phase.MEETING
    assert observation.voting_open is True
    assert observation.meeting_turns_remaining == 0
    assert observation.discussion_log[-1] == "red: pass"


def test_meeting_speech_parses_self_location_claim() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"])
    engine.reset()
    engine.step(Move(room="Electrical"))
    engine.step(CallMeeting())

    observation = engine.step(Speak(message="I was in Electrical"))

    assert observation.discussion_log[-1] == "red: I was in Electrical"
    assert observation.claims[-1].kind is ClaimKind.SELF_LOCATION
    assert observation.claims[-1].speaker_id == "red"
    assert observation.claims[-1].room == "Electrical"
    assert observation.claims[-1].truth_value is True
    assert observation.voting_open is True
    assert observation.meeting_turns_remaining == 0


def test_meeting_speech_parses_saw_player_claim() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"])
    engine.reset()
    engine.players["blue"].location = "MedBay"
    engine.location_history["blue"].append("MedBay")
    engine.step(CallMeeting())

    observation = engine.step(Speak(message="I saw blue in MedBay"))

    assert observation.claims[-1].kind is ClaimKind.SAW_PLAYER
    assert observation.claims[-1].target_id == "blue"
    assert observation.claims[-1].room == "MedBay"
    assert observation.claims[-1].truth_value is True


def test_speaking_outside_meeting_is_illegal() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"])
    engine.reset()

    observation = engine.step(Speak(message="I was in Cafeteria"))

    assert observation.reward == -0.1
    assert "Cannot speak outside meeting" in observation.message_log[-1]


def test_pass_outside_meeting_is_illegal() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"])
    engine.reset()

    observation = engine.step(PassMeeting())

    assert observation.reward == -0.1
    assert observation.phase is Phase.TASKS
    assert observation.voting_open is False
    assert observation.meeting_turns_remaining == 0
    assert "Cannot pass outside meeting" in observation.message_log[-1]


def test_speak_after_voting_opens_is_illegal() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"])
    engine.reset()
    engine.step(CallMeeting())
    engine.step(PassMeeting())

    observation = engine.step(Speak(message="I was in Cafeteria"))

    assert observation.reward == -0.1
    assert observation.voting_open is True
    assert "Voting is already open" in observation.message_log[-1]


def test_false_self_location_claim_gets_hallucination_penalty() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"])
    engine.reset()
    engine.step(CallMeeting())

    observation = engine.step(Speak(message="I was in Electrical"))

    assert observation.reward == -1.0
    assert observation.claims[-1].truth_value is False


def test_single_vote_without_bot_support_ejects_nobody() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"])
    engine.reset()
    engine.step(CallMeeting())
    engine.step(PassMeeting())

    observation = engine.step(Vote(target_id="blue"))

    assert observation.reward == 0.0
    assert engine.players["blue"].ejected is False
    assert observation.done is False
    assert "No majority" in observation.message_log[-1]


def test_bot_votes_eject_speaker_caught_in_false_alibi() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"])
    engine.reset()
    engine.step(CallMeeting())
    engine.step(Speak(message="I was in Electrical"))

    observation = engine.step(Vote(target_id="blue"))

    assert engine.players["red"].ejected is True
    assert observation.reward == -0.5
    assert "Ejected red" in observation.message_log[-1]


def test_majority_voting_tie_ejects_nobody() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["blue"], player_ids=["red", "blue", "green"])
    engine.reset()
    engine.step(CallMeeting())
    engine.step(PassMeeting())

    observation = engine.step(Vote(target_id="blue"))

    assert engine.players["blue"].ejected is False
    assert observation.phase is Phase.TASKS
    assert "No majority" in observation.message_log[-1]


def test_impostors_win_when_they_reach_parity() -> None:
    engine = AmongUsEngine(seed=1, impostor_ids=["red"], player_ids=["red", "blue", "green"])
    engine.reset()

    observation = engine.step(Kill(target_id="blue"))

    assert observation.done is True
    assert observation.winner is Winner.IMPOSTORS
    assert observation.reward == 1.5