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