Save work: Update game engine, maps, and add vision/pathing utilities. (Tests failing: vision raycast)
This commit is contained in:
+40
-22
@@ -1,5 +1,6 @@
|
||||
"""
|
||||
Tests for the game engine.
|
||||
Updated for pixel-based Position system.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
@@ -14,40 +15,61 @@ from src.map.graph import GameMap, Room, Edge, Task, Vent
|
||||
|
||||
|
||||
def create_simple_map():
|
||||
"""Create a simple test map."""
|
||||
"""Create a simple test map with pixel coordinates."""
|
||||
game_map = GameMap()
|
||||
game_map.spawn_points["cafeteria"] = Position(x=500, y=200)
|
||||
|
||||
# Cafeteria with task
|
||||
game_map.add_room(Room(
|
||||
id="cafeteria",
|
||||
name="Cafeteria",
|
||||
tasks=[Task(id="wires_cafe", name="Fix Wiring", duration=3.0)]
|
||||
center=Position(x=500, y=200),
|
||||
bounds=(Position(x=400, y=100), Position(x=600, y=300)),
|
||||
tasks=[Task(id="wires_cafe", name="Fix Wiring", duration=3.0, position=Position(x=450, y=150))]
|
||||
))
|
||||
|
||||
# Electrical with vent
|
||||
elec_vent = Vent(id="vent_elec", connects_to=["vent_security"])
|
||||
elec_vent = Vent(id="vent_elec", connects_to=["vent_security"], position=Position(x=200, y=450))
|
||||
game_map.add_room(Room(
|
||||
id="electrical",
|
||||
name="Electrical",
|
||||
center=Position(x=200, y=500),
|
||||
bounds=(Position(x=100, y=400), Position(x=300, y=600)),
|
||||
vent=elec_vent,
|
||||
tasks=[Task(id="wires_elec", name="Fix Wiring", duration=3.0)]
|
||||
tasks=[Task(id="wires_elec", name="Fix Wiring", duration=3.0, position=Position(x=250, y=550))]
|
||||
))
|
||||
|
||||
# Security with vent
|
||||
sec_vent = Vent(id="vent_security", connects_to=["vent_elec"])
|
||||
sec_vent = Vent(id="vent_security", connects_to=["vent_elec"], position=Position(x=400, y=450))
|
||||
game_map.add_room(Room(
|
||||
id="security",
|
||||
name="Security",
|
||||
center=Position(x=400, y=500),
|
||||
bounds=(Position(x=300, y=400), Position(x=500, y=600)),
|
||||
vent=sec_vent
|
||||
))
|
||||
|
||||
# Admin
|
||||
game_map.add_room(Room(id="admin", name="Admin"))
|
||||
game_map.add_room(Room(
|
||||
id="admin",
|
||||
name="Admin",
|
||||
center=Position(x=700, y=200),
|
||||
bounds=(Position(x=600, y=100), Position(x=800, y=300))
|
||||
))
|
||||
|
||||
# Connect rooms
|
||||
game_map.add_edge(Edge(id="cafe_elec", room_a="cafeteria", room_b="electrical", distance=5.0))
|
||||
game_map.add_edge(Edge(id="cafe_admin", room_a="cafeteria", room_b="admin", distance=3.0))
|
||||
game_map.add_edge(Edge(id="elec_sec", room_a="electrical", room_b="security", distance=4.0))
|
||||
# Connect rooms with waypoints
|
||||
game_map.add_edge(Edge(
|
||||
id="cafe_elec", room_a="cafeteria", room_b="electrical",
|
||||
waypoints=[Position(x=500, y=300), Position(x=300, y=400), Position(x=200, y=500)]
|
||||
))
|
||||
game_map.add_edge(Edge(
|
||||
id="cafe_admin", room_a="cafeteria", room_b="admin",
|
||||
waypoints=[Position(x=600, y=200), Position(x=700, y=200)]
|
||||
))
|
||||
game_map.add_edge(Edge(
|
||||
id="elec_sec", room_a="electrical", room_b="security",
|
||||
waypoints=[Position(x=300, y=500), Position(x=400, y=500)]
|
||||
))
|
||||
|
||||
return game_map
|
||||
|
||||
@@ -96,10 +118,6 @@ class TestGameEngineSetup(unittest.TestCase):
|
||||
self.assertIn("p1", self.engine.impostor_ids)
|
||||
self.assertEqual(player.kill_cooldown, self.config.kill_cooldown)
|
||||
|
||||
def test_custom_player_speed(self):
|
||||
player = self.engine.add_player("p1", "Red", "red", speed=3.0)
|
||||
self.assertEqual(player.speed, 3.0)
|
||||
|
||||
def test_impostor_context(self):
|
||||
self.engine.add_player("p1", "Red", "red", Role.IMPOSTOR)
|
||||
self.engine.add_player("p2", "Blue", "blue", Role.IMPOSTOR)
|
||||
@@ -176,7 +194,7 @@ class TestMovement(unittest.TestCase):
|
||||
|
||||
def test_move_no_path(self):
|
||||
# Add isolated room
|
||||
self.game_map.add_room(Room(id="isolated", name="Isolated"))
|
||||
self.game_map.add_room(Room(id="isolated", name="Isolated", center=Position(x=1000, y=1000)))
|
||||
|
||||
self.engine.queue_action("p1", "MOVE", {"destination": "isolated"})
|
||||
results = self.engine.resolve_actions()
|
||||
@@ -222,7 +240,7 @@ class TestKill(unittest.TestCase):
|
||||
|
||||
def test_cannot_kill_different_room(self):
|
||||
crew = self.engine.simulator.get_player("crew")
|
||||
crew.position = Position(room_id="electrical")
|
||||
crew.position = Position(x=200, y=500, room_id="electrical")
|
||||
|
||||
self.engine.queue_action("imp", "KILL", {"target_id": "crew"})
|
||||
results = self.engine.resolve_actions()
|
||||
@@ -262,7 +280,7 @@ class TestVenting(unittest.TestCase):
|
||||
|
||||
# Place impostor in electrical (has vent)
|
||||
imp = self.engine.simulator.get_player("imp")
|
||||
imp.position = Position(room_id="electrical")
|
||||
imp.position = Position(x=200, y=500, room_id="electrical")
|
||||
|
||||
def test_impostor_can_vent(self):
|
||||
self.engine.queue_action("imp", "VENT", {"destination": "security"})
|
||||
@@ -272,7 +290,7 @@ class TestVenting(unittest.TestCase):
|
||||
|
||||
def test_crewmate_cannot_vent(self):
|
||||
crew = self.engine.simulator.get_player("crew")
|
||||
crew.position = Position(room_id="electrical")
|
||||
crew.position = Position(x=200, y=500, room_id="electrical")
|
||||
|
||||
self.engine.queue_action("crew", "VENT", {"destination": "security"})
|
||||
results = self.engine.resolve_actions()
|
||||
@@ -282,7 +300,7 @@ class TestVenting(unittest.TestCase):
|
||||
def test_cannot_vent_unconnected(self):
|
||||
# Cafeteria has no vent
|
||||
imp = self.engine.simulator.get_player("imp")
|
||||
imp.position = Position(room_id="cafeteria")
|
||||
imp.position = Position(x=500, y=200, room_id="cafeteria")
|
||||
|
||||
self.engine.queue_action("imp", "VENT", {"destination": "security"})
|
||||
results = self.engine.resolve_actions()
|
||||
@@ -336,7 +354,7 @@ class TestReporting(unittest.TestCase):
|
||||
id="body1",
|
||||
player_id="dead",
|
||||
player_name="Blue",
|
||||
position=Position(room_id="cafeteria"),
|
||||
position=Position(x=500, y=200, room_id="cafeteria"),
|
||||
time_of_death=0.0
|
||||
)
|
||||
self.engine.simulator.bodies.append(body)
|
||||
@@ -349,7 +367,7 @@ class TestReporting(unittest.TestCase):
|
||||
|
||||
def test_cannot_report_body_in_different_room(self):
|
||||
player = self.engine.simulator.get_player("p1")
|
||||
player.position = Position(room_id="electrical")
|
||||
player.position = Position(x=200, y=500, room_id="electrical")
|
||||
|
||||
self.engine.queue_action("p1", "REPORT", {"body_id": "body1"})
|
||||
results = self.engine.resolve_actions()
|
||||
@@ -383,7 +401,7 @@ class TestEmergency(unittest.TestCase):
|
||||
|
||||
def test_cannot_call_emergency_outside_cafeteria(self):
|
||||
player = self.engine.simulator.get_player("p1")
|
||||
player.position = Position(room_id="electrical")
|
||||
player.position = Position(x=200, y=500, room_id="electrical")
|
||||
|
||||
self.engine.queue_action("p1", "EMERGENCY", {})
|
||||
results = self.engine.resolve_actions()
|
||||
|
||||
+71
-34
@@ -6,11 +6,11 @@ import unittest
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
import json
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from src.map.graph import GameMap, Room, Edge, Task, Vent
|
||||
from src.engine.types import Position
|
||||
|
||||
|
||||
class TestRoom(unittest.TestCase):
|
||||
@@ -34,18 +34,40 @@ class TestRoom(unittest.TestCase):
|
||||
room = Room(id="test", name="Test Room", vent=vent)
|
||||
self.assertIsNotNone(room.vent)
|
||||
self.assertEqual(len(room.vent.connects_to), 2)
|
||||
|
||||
def test_room_with_center(self):
|
||||
room = Room(id="test", name="Test Room", center=Position(x=100, y=200))
|
||||
self.assertEqual(room.center.x, 100)
|
||||
self.assertEqual(room.center.y, 200)
|
||||
|
||||
def test_room_contains_point(self):
|
||||
room = Room(
|
||||
id="test", name="Test Room",
|
||||
center=Position(x=100, y=100),
|
||||
bounds=(Position(x=0, y=0), Position(x=200, y=200))
|
||||
)
|
||||
self.assertTrue(room.contains_point(Position(x=100, y=100)))
|
||||
self.assertTrue(room.contains_point(Position(x=50, y=50)))
|
||||
self.assertFalse(room.contains_point(Position(x=300, y=100)))
|
||||
|
||||
|
||||
class TestEdge(unittest.TestCase):
|
||||
"""Tests for Edge dataclass."""
|
||||
|
||||
def test_edge_creation(self):
|
||||
edge = Edge(id="e1", room_a="a", room_b="b", distance=5.0)
|
||||
edge = Edge(id="e1", room_a="a", room_b="b")
|
||||
self.assertEqual(edge.id, "e1")
|
||||
self.assertEqual(edge.distance, 5.0)
|
||||
self.assertEqual(edge.room_a, "a")
|
||||
|
||||
def test_edge_with_waypoints(self):
|
||||
edge = Edge(
|
||||
id="e1", room_a="a", room_b="b",
|
||||
waypoints=[Position(x=0, y=0), Position(x=100, y=0)]
|
||||
)
|
||||
self.assertAlmostEqual(edge.distance, 100.0)
|
||||
|
||||
def test_edge_other_room(self):
|
||||
edge = Edge(id="e1", room_a="a", room_b="b", distance=5.0)
|
||||
edge = Edge(id="e1", room_a="a", room_b="b")
|
||||
self.assertEqual(edge.other_room("a"), "b")
|
||||
self.assertEqual(edge.other_room("b"), "a")
|
||||
|
||||
@@ -60,14 +82,24 @@ class TestGameMap(unittest.TestCase):
|
||||
# Create rooms: A -- B -- C
|
||||
# |
|
||||
# D
|
||||
self.game_map.add_room(Room(id="a", name="Room A"))
|
||||
self.game_map.add_room(Room(id="b", name="Room B"))
|
||||
self.game_map.add_room(Room(id="c", name="Room C"))
|
||||
self.game_map.add_room(Room(id="d", name="Room D"))
|
||||
self.game_map.add_room(Room(id="a", name="Room A", center=Position(x=0, y=0)))
|
||||
self.game_map.add_room(Room(id="b", name="Room B", center=Position(x=100, y=0)))
|
||||
self.game_map.add_room(Room(id="c", name="Room C", center=Position(x=200, y=0)))
|
||||
self.game_map.add_room(Room(id="d", name="Room D", center=Position(x=100, y=100)))
|
||||
|
||||
self.game_map.add_edge(Edge(id="ab", room_a="a", room_b="b", distance=3.0))
|
||||
self.game_map.add_edge(Edge(id="bc", room_a="b", room_b="c", distance=4.0))
|
||||
self.game_map.add_edge(Edge(id="bd", room_a="b", room_b="d", distance=2.0))
|
||||
# Create edges with waypoints
|
||||
self.game_map.add_edge(Edge(
|
||||
id="ab", room_a="a", room_b="b",
|
||||
waypoints=[Position(x=0, y=0), Position(x=50, y=0), Position(x=100, y=0)]
|
||||
))
|
||||
self.game_map.add_edge(Edge(
|
||||
id="bc", room_a="b", room_b="c",
|
||||
waypoints=[Position(x=100, y=0), Position(x=200, y=0)]
|
||||
))
|
||||
self.game_map.add_edge(Edge(
|
||||
id="bd", room_a="b", room_b="d",
|
||||
waypoints=[Position(x=100, y=0), Position(x=100, y=100)]
|
||||
))
|
||||
|
||||
def test_add_room(self):
|
||||
self.assertEqual(len(self.game_map.rooms), 4)
|
||||
@@ -87,7 +119,7 @@ class TestGameMap(unittest.TestCase):
|
||||
def test_get_edge(self):
|
||||
edge = self.game_map.get_edge("ab")
|
||||
self.assertIsNotNone(edge)
|
||||
self.assertEqual(edge.distance, 3.0)
|
||||
self.assertAlmostEqual(edge.distance, 100.0)
|
||||
|
||||
def test_get_neighbors(self):
|
||||
neighbors = self.game_map.get_neighbors("b")
|
||||
@@ -126,20 +158,6 @@ class TestGameMap(unittest.TestCase):
|
||||
self.game_map.add_room(Room(id="isolated", name="Isolated"))
|
||||
path = self.game_map.find_path("a", "isolated")
|
||||
self.assertIsNone(path)
|
||||
|
||||
def test_path_distance(self):
|
||||
path = self.game_map.find_path("a", "c")
|
||||
distance = self.game_map.path_distance(path)
|
||||
self.assertEqual(distance, 7.0) # 3 + 4
|
||||
|
||||
def test_shortest_path(self):
|
||||
# Add direct edge from a to d (should be longer)
|
||||
self.game_map.add_edge(Edge(id="ad", room_a="a", room_b="d", distance=10.0))
|
||||
|
||||
# Shortest path should still go through b
|
||||
path = self.game_map.find_path("a", "d")
|
||||
distance = self.game_map.path_distance(path)
|
||||
self.assertEqual(distance, 5.0) # 3 + 2 via b
|
||||
|
||||
|
||||
class TestMapSerialization(unittest.TestCase):
|
||||
@@ -147,9 +165,12 @@ class TestMapSerialization(unittest.TestCase):
|
||||
|
||||
def test_to_dict(self):
|
||||
game_map = GameMap()
|
||||
game_map.add_room(Room(id="a", name="A"))
|
||||
game_map.add_room(Room(id="b", name="B"))
|
||||
game_map.add_edge(Edge(id="ab", room_a="a", room_b="b", distance=5.0))
|
||||
game_map.add_room(Room(id="a", name="A", center=Position(x=0, y=0)))
|
||||
game_map.add_room(Room(id="b", name="B", center=Position(x=100, y=0)))
|
||||
game_map.add_edge(Edge(
|
||||
id="ab", room_a="a", room_b="b",
|
||||
waypoints=[Position(x=0, y=0), Position(x=100, y=0)]
|
||||
))
|
||||
|
||||
data = game_map.to_dict()
|
||||
self.assertEqual(len(data["rooms"]), 2)
|
||||
@@ -157,11 +178,19 @@ class TestMapSerialization(unittest.TestCase):
|
||||
|
||||
def test_save_and_load(self):
|
||||
game_map = GameMap()
|
||||
task = Task(id="t1", name="Task", duration=3.0)
|
||||
vent = Vent(id="v1", connects_to=["v2"])
|
||||
game_map.add_room(Room(id="a", name="A", tasks=[task], vent=vent))
|
||||
game_map.add_room(Room(id="b", name="B"))
|
||||
game_map.add_edge(Edge(id="ab", room_a="a", room_b="b", distance=5.0))
|
||||
task = Task(id="t1", name="Task", duration=3.0, position=Position(x=50, y=50))
|
||||
vent = Vent(id="v1", connects_to=["v2"], position=Position(x=60, y=60))
|
||||
game_map.add_room(Room(
|
||||
id="a", name="A",
|
||||
center=Position(x=50, y=50),
|
||||
bounds=(Position(x=0, y=0), Position(x=100, y=100)),
|
||||
tasks=[task], vent=vent
|
||||
))
|
||||
game_map.add_room(Room(id="b", name="B", center=Position(x=200, y=50)))
|
||||
game_map.add_edge(Edge(
|
||||
id="ab", room_a="a", room_b="b",
|
||||
waypoints=[Position(x=100, y=50), Position(x=200, y=50)]
|
||||
))
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
|
||||
game_map.save(f.name)
|
||||
@@ -213,6 +242,14 @@ class TestSkeldMap(unittest.TestCase):
|
||||
medbay = self.skeld.get_room("medbay")
|
||||
self.assertIn("vent_security", medbay.vent.connects_to)
|
||||
self.assertIn("vent_elec", medbay.vent.connects_to)
|
||||
|
||||
def test_skeld_has_walls(self):
|
||||
"""Skeld should have wall geometry for raycasting."""
|
||||
self.assertGreater(len(self.skeld.walls), 0)
|
||||
|
||||
def test_skeld_has_spawn_points(self):
|
||||
"""Skeld should have spawn points."""
|
||||
self.assertIn("cafeteria", self.skeld.spawn_points)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
"""
|
||||
Tests for path utilities — walk interpolation.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from src.engine.types import Position
|
||||
from src.engine.path_utils import WalkPath, WalkState, WalkManager, PathSegment
|
||||
|
||||
|
||||
class TestPathSegment(unittest.TestCase):
|
||||
"""Tests for PathSegment."""
|
||||
|
||||
def test_interpolate_start(self):
|
||||
segment = PathSegment(
|
||||
start=Position(x=0, y=0),
|
||||
end=Position(x=100, y=0),
|
||||
distance=100,
|
||||
cumulative_distance=100
|
||||
)
|
||||
pos = segment.interpolate(0.0)
|
||||
self.assertEqual(pos.x, 0)
|
||||
self.assertEqual(pos.y, 0)
|
||||
|
||||
def test_interpolate_end(self):
|
||||
segment = PathSegment(
|
||||
start=Position(x=0, y=0),
|
||||
end=Position(x=100, y=0),
|
||||
distance=100,
|
||||
cumulative_distance=100
|
||||
)
|
||||
pos = segment.interpolate(1.0)
|
||||
self.assertEqual(pos.x, 100)
|
||||
self.assertEqual(pos.y, 0)
|
||||
|
||||
def test_interpolate_middle(self):
|
||||
segment = PathSegment(
|
||||
start=Position(x=0, y=0),
|
||||
end=Position(x=100, y=0),
|
||||
distance=100,
|
||||
cumulative_distance=100
|
||||
)
|
||||
pos = segment.interpolate(0.5)
|
||||
self.assertAlmostEqual(pos.x, 50)
|
||||
self.assertAlmostEqual(pos.y, 0)
|
||||
|
||||
|
||||
class TestWalkPath(unittest.TestCase):
|
||||
"""Tests for WalkPath."""
|
||||
|
||||
def test_straight_line_distance(self):
|
||||
path = WalkPath(waypoints=[
|
||||
Position(x=0, y=0),
|
||||
Position(x=100, y=0)
|
||||
])
|
||||
self.assertAlmostEqual(path.total_distance, 100.0)
|
||||
|
||||
def test_multi_segment_distance(self):
|
||||
path = WalkPath(waypoints=[
|
||||
Position(x=0, y=0),
|
||||
Position(x=100, y=0),
|
||||
Position(x=100, y=100)
|
||||
])
|
||||
self.assertAlmostEqual(path.total_distance, 200.0)
|
||||
|
||||
def test_position_at_distance_start(self):
|
||||
path = WalkPath(waypoints=[
|
||||
Position(x=0, y=0),
|
||||
Position(x=100, y=0)
|
||||
])
|
||||
pos = path.position_at_distance(0)
|
||||
self.assertEqual(pos.x, 0)
|
||||
|
||||
def test_position_at_distance_end(self):
|
||||
path = WalkPath(waypoints=[
|
||||
Position(x=0, y=0),
|
||||
Position(x=100, y=0)
|
||||
])
|
||||
pos = path.position_at_distance(100)
|
||||
self.assertEqual(pos.x, 100)
|
||||
|
||||
def test_position_at_distance_middle(self):
|
||||
path = WalkPath(waypoints=[
|
||||
Position(x=0, y=0),
|
||||
Position(x=100, y=0)
|
||||
])
|
||||
pos = path.position_at_distance(50)
|
||||
self.assertAlmostEqual(pos.x, 50)
|
||||
|
||||
def test_position_at_distance_multi_segment(self):
|
||||
path = WalkPath(waypoints=[
|
||||
Position(x=0, y=0),
|
||||
Position(x=100, y=0),
|
||||
Position(x=100, y=100)
|
||||
])
|
||||
# At 150 pixels: first 100 to (100,0), then 50 down
|
||||
pos = path.position_at_distance(150)
|
||||
self.assertAlmostEqual(pos.x, 100)
|
||||
self.assertAlmostEqual(pos.y, 50)
|
||||
|
||||
def test_position_at_time(self):
|
||||
path = WalkPath(waypoints=[
|
||||
Position(x=0, y=0),
|
||||
Position(x=100, y=0)
|
||||
])
|
||||
# Speed 50 px/sec, after 1 sec = 50 px traveled
|
||||
pos = path.position_at_time(elapsed=1.0, speed=50.0)
|
||||
self.assertAlmostEqual(pos.x, 50)
|
||||
|
||||
def test_time_to_complete(self):
|
||||
path = WalkPath(waypoints=[
|
||||
Position(x=0, y=0),
|
||||
Position(x=100, y=0)
|
||||
])
|
||||
# 100 pixels at 50 px/sec = 2 seconds
|
||||
self.assertAlmostEqual(path.time_to_complete(50.0), 2.0)
|
||||
|
||||
def test_is_complete(self):
|
||||
path = WalkPath(waypoints=[
|
||||
Position(x=0, y=0),
|
||||
Position(x=100, y=0)
|
||||
])
|
||||
self.assertFalse(path.is_complete(elapsed=1.0, speed=50.0))
|
||||
self.assertTrue(path.is_complete(elapsed=3.0, speed=50.0))
|
||||
|
||||
def test_progress(self):
|
||||
path = WalkPath(waypoints=[
|
||||
Position(x=0, y=0),
|
||||
Position(x=100, y=0)
|
||||
])
|
||||
self.assertAlmostEqual(path.progress(elapsed=1.0, speed=50.0), 0.5)
|
||||
self.assertAlmostEqual(path.progress(elapsed=2.0, speed=50.0), 1.0)
|
||||
|
||||
def test_direct_path(self):
|
||||
path = WalkPath.direct(Position(x=0, y=0), Position(x=100, y=100))
|
||||
self.assertAlmostEqual(path.total_distance, 141.42, places=1)
|
||||
|
||||
|
||||
class TestWalkState(unittest.TestCase):
|
||||
"""Tests for WalkState."""
|
||||
|
||||
def test_current_position(self):
|
||||
path = WalkPath(waypoints=[
|
||||
Position(x=0, y=0),
|
||||
Position(x=100, y=0)
|
||||
])
|
||||
state = WalkState(
|
||||
player_id="p1",
|
||||
path=path,
|
||||
start_time=10.0,
|
||||
speed=50.0
|
||||
)
|
||||
# At time 11.0 (1 sec elapsed), should be at x=50
|
||||
pos = state.current_position(11.0)
|
||||
self.assertAlmostEqual(pos.x, 50)
|
||||
|
||||
def test_is_complete(self):
|
||||
path = WalkPath(waypoints=[
|
||||
Position(x=0, y=0),
|
||||
Position(x=100, y=0)
|
||||
])
|
||||
state = WalkState(
|
||||
player_id="p1",
|
||||
path=path,
|
||||
start_time=10.0,
|
||||
speed=50.0
|
||||
)
|
||||
self.assertFalse(state.is_complete(11.0))
|
||||
self.assertTrue(state.is_complete(13.0))
|
||||
|
||||
def test_arrival_time(self):
|
||||
path = WalkPath(waypoints=[
|
||||
Position(x=0, y=0),
|
||||
Position(x=100, y=0)
|
||||
])
|
||||
state = WalkState(
|
||||
player_id="p1",
|
||||
path=path,
|
||||
start_time=10.0,
|
||||
speed=50.0
|
||||
)
|
||||
self.assertAlmostEqual(state.arrival_time(), 12.0)
|
||||
|
||||
|
||||
class TestWalkManager(unittest.TestCase):
|
||||
"""Tests for WalkManager."""
|
||||
|
||||
def setUp(self):
|
||||
self.manager = WalkManager()
|
||||
self.path = WalkPath(waypoints=[
|
||||
Position(x=0, y=0),
|
||||
Position(x=100, y=0)
|
||||
])
|
||||
|
||||
def test_start_walk(self):
|
||||
state = self.manager.start_walk("p1", self.path, 0.0, 50.0)
|
||||
self.assertEqual(state.player_id, "p1")
|
||||
self.assertIsNotNone(self.manager.get_walk_state("p1"))
|
||||
|
||||
def test_get_position(self):
|
||||
self.manager.start_walk("p1", self.path, 0.0, 50.0)
|
||||
pos = self.manager.get_position("p1", 1.0)
|
||||
self.assertIsNotNone(pos)
|
||||
self.assertAlmostEqual(pos.x, 50)
|
||||
|
||||
def test_get_position_no_walk(self):
|
||||
pos = self.manager.get_position("nonexistent", 1.0)
|
||||
self.assertIsNone(pos)
|
||||
|
||||
def test_is_walking(self):
|
||||
self.manager.start_walk("p1", self.path, 0.0, 50.0)
|
||||
self.assertTrue(self.manager.is_walking("p1", 1.0))
|
||||
self.assertFalse(self.manager.is_walking("p1", 3.0))
|
||||
|
||||
def test_cancel_walk(self):
|
||||
self.manager.start_walk("p1", self.path, 0.0, 50.0)
|
||||
self.manager.cancel_walk("p1")
|
||||
self.assertIsNone(self.manager.get_walk_state("p1"))
|
||||
|
||||
def test_cleanup_completed(self):
|
||||
self.manager.start_walk("p1", self.path, 0.0, 50.0)
|
||||
self.manager.start_walk("p2", self.path, 0.0, 100.0)
|
||||
|
||||
# p2 finishes at t=1, p1 at t=2
|
||||
completed = self.manager.cleanup_completed(1.5)
|
||||
|
||||
self.assertEqual(len(completed), 1)
|
||||
self.assertEqual(completed[0].player_id, "p2")
|
||||
self.assertIsNone(self.manager.get_walk_state("p2"))
|
||||
self.assertIsNotNone(self.manager.get_walk_state("p1"))
|
||||
|
||||
def test_get_all_positions(self):
|
||||
self.manager.start_walk("p1", self.path, 0.0, 50.0)
|
||||
self.manager.start_walk("p2", self.path, 0.0, 100.0)
|
||||
|
||||
positions = self.manager.get_all_positions(1.0)
|
||||
|
||||
self.assertEqual(len(positions), 2)
|
||||
self.assertAlmostEqual(positions["p1"].x, 50)
|
||||
self.assertAlmostEqual(positions["p2"].x, 100)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+12
-12
@@ -39,21 +39,21 @@ class TestPlayer(unittest.TestCase):
|
||||
player = Player(
|
||||
id="p1", name="Red", color="red",
|
||||
role=Role.CREWMATE,
|
||||
position=Position(room_id="cafeteria")
|
||||
position=Position(x=1000, y=400, room_id="cafeteria")
|
||||
)
|
||||
self.assertEqual(player.id, "p1")
|
||||
self.assertEqual(player.role, Role.CREWMATE)
|
||||
self.assertTrue(player.is_alive)
|
||||
|
||||
def test_position_in_room(self):
|
||||
pos = Position(room_id="cafeteria")
|
||||
self.assertTrue(pos.is_in_room())
|
||||
self.assertFalse(pos.is_on_edge())
|
||||
def test_position_distance(self):
|
||||
pos1 = Position(x=0, y=0)
|
||||
pos2 = Position(x=3, y=4)
|
||||
self.assertAlmostEqual(pos1.distance_to(pos2), 5.0)
|
||||
|
||||
def test_position_on_edge(self):
|
||||
pos = Position(edge_id="ab", progress=0.5)
|
||||
self.assertFalse(pos.is_in_room())
|
||||
self.assertTrue(pos.is_on_edge())
|
||||
def test_position_with_room(self):
|
||||
pos = Position(x=100, y=200, room_id="cafeteria")
|
||||
self.assertEqual(pos.room_id, "cafeteria")
|
||||
self.assertEqual(pos.x, 100)
|
||||
|
||||
|
||||
class TestSimulator(unittest.TestCase):
|
||||
@@ -196,9 +196,9 @@ class TestSimulator(unittest.TestCase):
|
||||
self.assertEqual(len(living), 2)
|
||||
|
||||
def test_players_at(self):
|
||||
p1 = Player(id="p1", name="Red", color="red", position=Position(room_id="cafeteria"))
|
||||
p2 = Player(id="p2", name="Blue", color="blue", position=Position(room_id="cafeteria"))
|
||||
p3 = Player(id="p3", name="Green", color="green", position=Position(room_id="admin"))
|
||||
p1 = Player(id="p1", name="Red", color="red", position=Position(x=1000, y=400, room_id="cafeteria"))
|
||||
p2 = Player(id="p2", name="Blue", color="blue", position=Position(x=1000, y=400, room_id="cafeteria"))
|
||||
p3 = Player(id="p3", name="Green", color="green", position=Position(x=1200, y=700, room_id="admin"))
|
||||
|
||||
self.sim.add_player(p1)
|
||||
self.sim.add_player(p2)
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
"""
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user