""" Tests for ray-traced vision system. """ import unittest import sys import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from src.engine.types import Position, Wall, Player, Role from src.engine.vision_raycast import RaycastVision, VisibilityResult class TestPosition(unittest.TestCase): """Tests for pixel-based Position class.""" def test_distance_to(self): """Test distance calculation.""" p1 = Position(x=0, y=0) p2 = Position(x=3, y=4) self.assertAlmostEqual(p1.distance_to(p2), 5.0) def test_direction_to(self): """Test direction vector.""" p1 = Position(x=0, y=0) p2 = Position(x=10, y=0) dx, dy = p1.direction_to(p2) self.assertAlmostEqual(dx, 1.0) self.assertAlmostEqual(dy, 0.0) def test_move_toward(self): """Test moving toward a target.""" p1 = Position(x=0, y=0) p2 = Position(x=10, y=0) p3 = p1.move_toward(p2, 5.0) self.assertAlmostEqual(p3.x, 5.0) self.assertAlmostEqual(p3.y, 0.0) def test_to_tuple(self): """Test tuple conversion.""" p = Position(x=100, y=200) self.assertEqual(p.to_tuple(), (100, 200)) def test_from_tuple(self): """Test tuple construction.""" p = Position.from_tuple((100, 200)) self.assertEqual(p.x, 100) self.assertEqual(p.y, 200) class TestWall(unittest.TestCase): """Tests for Wall class.""" def test_to_dict(self): """Test serialization.""" wall = Wall(p1=(0, 0), p2=(100, 0)) d = wall.to_dict() self.assertEqual(d["p1"], [0, 0]) self.assertEqual(d["p2"], [100, 0]) def test_from_dict(self): """Test deserialization.""" wall = Wall.from_dict({"p1": [0, 0], "p2": [100, 0]}) self.assertEqual(wall.p1, (0, 0)) self.assertEqual(wall.p2, (100, 0)) class TestRaycastVision(unittest.TestCase): """Tests for ray-traced vision system.""" def test_no_walls_visible(self): """With no walls, everything in range is visible.""" vision = RaycastVision(walls=[], base_vision_radius=300) observer = Position(x=0, y=0) target = Position(x=100, y=0) result = vision.check_visibility(observer, target) self.assertTrue(result.visible) self.assertAlmostEqual(result.distance, 100.0) def test_beyond_radius_not_visible(self): """Targets beyond vision radius are not visible.""" vision = RaycastVision(walls=[], base_vision_radius=100) observer = Position(x=0, y=0) target = Position(x=200, y=0) result = vision.check_visibility(observer, target) self.assertFalse(result.visible) def test_wall_blocks_vision(self): """Wall between observer and target blocks vision.""" # Wall at x=50, from y=-100 to y=100 wall = Wall(p1=(50, -100), p2=(50, 100)) vision = RaycastVision(walls=[wall], base_vision_radius=300) observer = Position(x=0, y=0) target = Position(x=100, y=0) result = vision.check_visibility(observer, target) self.assertFalse(result.visible) self.assertTrue(result.blocked_by_wall) def test_wall_not_in_path(self): """Wall not in line of sight doesn't block.""" # Wall off to the side wall = Wall(p1=(100, 100), p2=(100, 200)) vision = RaycastVision(walls=[wall], base_vision_radius=300) observer = Position(x=0, y=0) target = Position(x=100, y=0) result = vision.check_visibility(observer, target) self.assertTrue(result.visible) def test_get_visible_players(self): """Test getting list of visible players.""" vision = RaycastVision(walls=[], base_vision_radius=300) observer = Player(id="red", name="Red", color="red", position=Position(x=0, y=0)) close_player = Player(id="blue", name="Blue", color="blue", position=Position(x=100, y=0)) far_player = Player(id="green", name="Green", color="green", position=Position(x=500, y=0)) all_players = [observer, close_player, far_player] visible = vision.get_visible_players(observer, all_players) # Should see blue but not green self.assertEqual(len(visible), 1) self.assertEqual(visible[0][0].id, "blue") def test_dead_players_not_visible_by_default(self): """Dead players are not visible by default.""" vision = RaycastVision(walls=[], base_vision_radius=300) observer = Player(id="red", name="Red", color="red", position=Position(x=0, y=0)) dead_player = Player(id="blue", name="Blue", color="blue", position=Position(x=100, y=0), is_alive=False) visible = vision.get_visible_players(observer, [observer, dead_player]) self.assertEqual(len(visible), 0) def test_dead_players_visible_when_requested(self): """Dead players visible when include_dead=True.""" vision = RaycastVision(walls=[], base_vision_radius=300) observer = Player(id="red", name="Red", color="red", position=Position(x=0, y=0)) dead_player = Player(id="blue", name="Blue", color="blue", position=Position(x=100, y=0), is_alive=False) visible = vision.get_visible_players(observer, [observer, dead_player], include_dead=True) self.assertEqual(len(visible), 1) class TestLineIntersection(unittest.TestCase): """Tests for line segment intersection math.""" def setUp(self): self.vision = RaycastVision(walls=[], base_vision_radius=300) def test_crossing_lines_intersect(self): """Two crossing lines have an intersection.""" result = self.vision.line_segment_intersection( (0, 0), (10, 10), # Diagonal line (0, 10), (10, 0) # Crossing diagonal ) self.assertIsNotNone(result) self.assertAlmostEqual(result[0], 5.0) self.assertAlmostEqual(result[1], 5.0) def test_parallel_lines_no_intersect(self): """Parallel lines don't intersect.""" result = self.vision.line_segment_intersection( (0, 0), (10, 0), # Horizontal line (0, 5), (10, 5) # Parallel horizontal ) self.assertIsNone(result) def test_non_overlapping_segments(self): """Non-overlapping segments don't intersect.""" result = self.vision.line_segment_intersection( (0, 0), (5, 0), # Short segment (10, -5), (10, 5) # Far away vertical ) self.assertIsNone(result) class TestVisionPolygon(unittest.TestCase): """Tests for vision polygon casting.""" def test_no_walls_circular(self): """Without walls, vision polygon is circular.""" vision = RaycastVision(walls=[], base_vision_radius=100) observer = Position(x=500, y=500) points = vision.cast_rays_for_polygon(observer, num_rays=36) # All points should be at radius distance for px, py in points: dist = ((px - observer.x)**2 + (py - observer.y)**2)**0.5 self.assertAlmostEqual(dist, 100.0, places=1) def test_wall_creates_shadow(self): """Wall creates a shadow in vision polygon.""" # Wall directly in front wall = Wall(p1=(550, 400), p2=(550, 600)) vision = RaycastVision(walls=[wall], base_vision_radius=200) observer = Position(x=400, y=500) points = vision.cast_rays_for_polygon(observer, num_rays=360) # Some points should be closer than radius due to wall min_dist = min(((px - observer.x)**2 + (py - observer.y)**2)**0.5 for px, py in points) self.assertLess(min_dist, 200) class TestImpostorVision(unittest.TestCase): """Tests for role-based vision differences.""" def test_impostor_has_larger_vision(self): """Impostors can see further than crewmates.""" vision = RaycastVision( walls=[], base_vision_radius=300, crewmate_vision=1.0, impostor_vision=1.5 ) crewmate = Player(id="crew", name="Crew", color="blue", position=Position(x=0, y=0), role=Role.CREWMATE) impostor = Player(id="imp", name="Imp", color="red", position=Position(x=0, y=0), role=Role.IMPOSTOR) # Check vision radii crew_radius = vision.get_vision_radius_for_player(crewmate) imp_radius = vision.get_vision_radius_for_player(impostor) self.assertAlmostEqual(crew_radius, 300.0) self.assertAlmostEqual(imp_radius, 450.0) # 300 * 1.5 def test_lights_sabotage_affects_crewmates(self): """Lights sabotage reduces crewmate vision.""" vision = RaycastVision( walls=[], base_vision_radius=300, crewmate_vision=1.0, lights_multiplier=0.25 ) crewmate = Player(id="crew", name="Crew", color="blue", position=Position(x=0, y=0), role=Role.CREWMATE) # Before lights sabotage normal_radius = vision.get_vision_radius_for_player(crewmate) self.assertAlmostEqual(normal_radius, 300.0) # After lights sabotage vision.set_lights_sabotaged(True) sabotaged_radius = vision.get_vision_radius_for_player(crewmate) self.assertAlmostEqual(sabotaged_radius, 75.0) # 300 * 0.25 def test_lights_sabotage_does_not_affect_impostors(self): """Impostors are immune to lights sabotage.""" vision = RaycastVision( walls=[], base_vision_radius=300, impostor_vision=1.5, lights_multiplier=0.25 ) impostor = Player(id="imp", name="Imp", color="red", position=Position(x=0, y=0), role=Role.IMPOSTOR) # Before lights sabotage normal_radius = vision.get_vision_radius_for_player(impostor) # After lights sabotage vision.set_lights_sabotaged(True) sabotaged_radius = vision.get_vision_radius_for_player(impostor) # Impostor vision should be unchanged self.assertAlmostEqual(normal_radius, sabotaged_radius) self.assertAlmostEqual(sabotaged_radius, 450.0) def test_impostor_sees_further_during_lights_sabotage(self): """During lights sabotage, impostors can see targets crewmates cannot.""" vision = RaycastVision( walls=[], base_vision_radius=300, crewmate_vision=1.0, impostor_vision=1.5, lights_multiplier=0.25 ) vision.set_lights_sabotaged(True) crewmate = Player(id="crew", name="Crew", color="blue", position=Position(x=0, y=0), role=Role.CREWMATE) impostor = Player(id="imp", name="Imp", color="red", position=Position(x=0, y=0), role=Role.IMPOSTOR) # Target at 100 pixels away target = Player(id="target", name="Target", color="green", position=Position(x=100, y=0), role=Role.CREWMATE) all_players = [crewmate, impostor, target] # Crewmate sees 75px during sabotage, target is at 100px crew_visible = vision.get_visible_players(crewmate, all_players) self.assertEqual(len(crew_visible), 0) # Can't see target # Impostor sees 450px, target is at 100px imp_visible = vision.get_visible_players(impostor, all_players) self.assertEqual(len(imp_visible), 2) # Can see both if __name__ == "__main__": unittest.main()