""" Unit tests for the geometry module (Pinships orbital mechanics). Tests cover: - Basic utility functions (distance, angle, etc.) - Bresenham circle/line algorithms - OrbitalBody recursive positioning - Pathfinding helpers """ import sys import math # Import the geometry module sys.path.insert(0, '/home/john/Development/McRogueFace/src/scripts') from geometry import ( # Utilities distance, distance_squared, angle_between, normalize_angle, angle_difference, lerp, clamp, point_on_circle, rotate_point, # Grid algorithms bresenham_circle, bresenham_line, filled_circle, sort_circle_cells, # Orbital system OrbitalBody, OrbitingShip, # Pathfinding nearest_orbit_entry, optimal_exit_heading, is_viable_waypoint, project_body_positions, line_of_sight_blocked, # Convenience create_solar_system, create_planet, create_moon ) EPSILON = 0.0001 # Float comparison tolerance def approx_equal(a, b, eps=EPSILON): """Check if two floats are approximately equal.""" return abs(a - b) < eps def test_distance(): """Test distance calculations.""" assert approx_equal(distance((0, 0), (3, 4)), 5.0) assert approx_equal(distance((0, 0), (0, 0)), 0.0) assert approx_equal(distance((1, 1), (4, 5)), 5.0) assert approx_equal(distance((-3, -4), (0, 0)), 5.0) print(" distance: PASS") def test_distance_squared(): """Test squared distance (no sqrt).""" assert distance_squared((0, 0), (3, 4)) == 25 assert distance_squared((0, 0), (0, 0)) == 0 print(" distance_squared: PASS") def test_angle_between(): """Test angle calculations.""" # East = 0 degrees assert approx_equal(angle_between((0, 0), (1, 0)), 0.0) # North = 90 degrees (in screen coordinates, +y is down, but atan2 treats +y as up) assert approx_equal(angle_between((0, 0), (0, 1)), 90.0) # West = 180 degrees assert approx_equal(angle_between((0, 0), (-1, 0)), 180.0) # South = 270 degrees assert approx_equal(angle_between((0, 0), (0, -1)), 270.0) # Diagonal assert approx_equal(angle_between((0, 0), (1, 1)), 45.0) print(" angle_between: PASS") def test_normalize_angle(): """Test angle normalization to 0-360.""" assert approx_equal(normalize_angle(0), 0.0) assert approx_equal(normalize_angle(360), 0.0) assert approx_equal(normalize_angle(720), 0.0) assert approx_equal(normalize_angle(-90), 270.0) assert approx_equal(normalize_angle(-360), 0.0) assert approx_equal(normalize_angle(450), 90.0) print(" normalize_angle: PASS") def test_angle_difference(): """Test shortest angular distance.""" assert approx_equal(angle_difference(0, 90), 90.0) assert approx_equal(angle_difference(90, 0), -90.0) assert approx_equal(angle_difference(350, 10), 20.0) # Wrap around assert approx_equal(angle_difference(10, 350), -20.0) assert approx_equal(angle_difference(0, 180), 180.0) print(" angle_difference: PASS") def test_lerp(): """Test linear interpolation.""" assert approx_equal(lerp(0, 10, 0.0), 0.0) assert approx_equal(lerp(0, 10, 1.0), 10.0) assert approx_equal(lerp(0, 10, 0.5), 5.0) assert approx_equal(lerp(-5, 5, 0.5), 0.0) print(" lerp: PASS") def test_clamp(): """Test value clamping.""" assert clamp(5, 0, 10) == 5 assert clamp(-5, 0, 10) == 0 assert clamp(15, 0, 10) == 10 assert clamp(0, 0, 10) == 0 assert clamp(10, 0, 10) == 10 print(" clamp: PASS") def test_point_on_circle(): """Test point calculation on circle.""" center = (100, 100) radius = 50 # East (0 degrees) p = point_on_circle(center, radius, 0) assert approx_equal(p[0], 150.0) assert approx_equal(p[1], 100.0) # North (90 degrees) p = point_on_circle(center, radius, 90) assert approx_equal(p[0], 100.0) assert approx_equal(p[1], 150.0) # West (180 degrees) p = point_on_circle(center, radius, 180) assert approx_equal(p[0], 50.0) assert approx_equal(p[1], 100.0) print(" point_on_circle: PASS") def test_rotate_point(): """Test point rotation around center.""" center = (0, 0) point = (1, 0) # Rotate 90 degrees p = rotate_point(point, center, 90) assert approx_equal(p[0], 0.0) assert approx_equal(p[1], 1.0) # Rotate 180 degrees p = rotate_point(point, center, 180) assert approx_equal(p[0], -1.0) assert approx_equal(p[1], 0.0) print(" rotate_point: PASS") def test_bresenham_circle(): """Test Bresenham circle generation.""" # Radius 0 = just the center cells = bresenham_circle((5, 5), 0) assert cells == [(5, 5)] # Radius 3 should give a circle-ish shape cells = bresenham_circle((10, 10), 3) assert len(cells) > 0 # All cells should be roughly radius distance from center for x, y in cells: dist = math.sqrt((x - 10) ** 2 + (y - 10) ** 2) assert 2.5 <= dist <= 3.5, f"Cell ({x},{y}) has distance {dist}" # Should be symmetric cells_set = set(cells) for x, y in cells: # Check all 4 quadrant reflections exist dx, dy = x - 10, y - 10 assert (10 + dx, 10 + dy) in cells_set assert (10 - dx, 10 + dy) in cells_set assert (10 + dx, 10 - dy) in cells_set assert (10 - dx, 10 - dy) in cells_set print(" bresenham_circle: PASS") def test_bresenham_line(): """Test Bresenham line generation.""" # Horizontal line cells = bresenham_line((0, 0), (5, 0)) assert cells == [(0, 0), (1, 0), (2, 0), (3, 0), (4, 0), (5, 0)] # Vertical line cells = bresenham_line((0, 0), (0, 3)) assert cells == [(0, 0), (0, 1), (0, 2), (0, 3)] # Diagonal line cells = bresenham_line((0, 0), (3, 3)) assert (0, 0) in cells assert (3, 3) in cells assert len(cells) == 4 # Should hit 4 cells for 45-degree line # Start and end should be included cells = bresenham_line((10, 20), (15, 22)) assert (10, 20) in cells assert (15, 22) in cells print(" bresenham_line: PASS") def test_filled_circle(): """Test filled circle generation.""" cells = filled_circle((5, 5), 2) # Center should be included assert (5, 5) in cells # Edges should be included assert (5, 3) in cells # top assert (5, 7) in cells # bottom assert (3, 5) in cells # left assert (7, 5) in cells # right # Corners (at distance sqrt(8) ≈ 2.83) should NOT be included for radius 2 assert (3, 3) not in cells print(" filled_circle: PASS") def test_orbital_body_stationary(): """Test stationary body (star) positioning.""" star = OrbitalBody( name="Star", surface_radius=10, orbit_ring_radius=15, parent=None, base_position=(500, 500) ) # Position should never change assert star.grid_position_at_time(0) == (500, 500) assert star.grid_position_at_time(100) == (500, 500) assert star.grid_position_at_time(9999) == (500, 500) # Continuous position should match assert star.center_at_time(0) == (500.0, 500.0) print(" orbital_body_stationary: PASS") def test_orbital_body_simple_orbit(): """Test planet orbiting a star.""" star = OrbitalBody( name="Star", surface_radius=10, orbit_ring_radius=15, parent=None, base_position=(500, 500) ) planet = OrbitalBody( name="Planet", surface_radius=5, orbit_ring_radius=10, parent=star, orbital_radius=100, # 100 units from star angular_velocity=90, # 90 degrees per turn (quarter orbit) initial_angle=0 # Start to the east ) # t=0: Planet should be east of star pos0 = planet.center_at_time(0) assert approx_equal(pos0[0], 600.0) # 500 + 100 assert approx_equal(pos0[1], 500.0) # t=1: Planet should be north of star (rotated 90 degrees) pos1 = planet.center_at_time(1) assert approx_equal(pos1[0], 500.0) assert approx_equal(pos1[1], 600.0) # 500 + 100 # t=2: Planet should be west of star pos2 = planet.center_at_time(2) assert approx_equal(pos2[0], 400.0) # 500 - 100 assert approx_equal(pos2[1], 500.0) # t=4: Back to start (full orbit) pos4 = planet.center_at_time(4) assert approx_equal(pos4[0], 600.0) assert approx_equal(pos4[1], 500.0) print(" orbital_body_simple_orbit: PASS") def test_orbital_body_nested_orbit(): """Test moon orbiting a planet orbiting a star.""" star = OrbitalBody( name="Star", surface_radius=10, orbit_ring_radius=15, parent=None, base_position=(500, 500) ) planet = OrbitalBody( name="Planet", surface_radius=5, orbit_ring_radius=10, parent=star, orbital_radius=100, angular_velocity=90, # Quarter orbit per turn initial_angle=0 ) moon = OrbitalBody( name="Moon", surface_radius=2, orbit_ring_radius=5, parent=planet, orbital_radius=20, # 20 units from planet angular_velocity=180, # Half orbit per turn (faster than planet) initial_angle=0 ) # t=0: Moon should be east of planet, which is east of star moon_pos0 = moon.center_at_time(0) # Planet at (600, 500), moon 20 units east = (620, 500) assert approx_equal(moon_pos0[0], 620.0) assert approx_equal(moon_pos0[1], 500.0) # t=1: Planet moved north (500, 600), moon rotated 180 degrees (west of planet) moon_pos1 = moon.center_at_time(1) # Planet at (500, 600), moon 20 units west = (480, 600) assert approx_equal(moon_pos1[0], 480.0) assert approx_equal(moon_pos1[1], 600.0) print(" orbital_body_nested_orbit: PASS") def test_orbiting_ship(): """Test ship orbiting a body.""" star = OrbitalBody( name="Star", surface_radius=10, orbit_ring_radius=50, parent=None, base_position=(500, 500) ) ship = OrbitingShip(body=star, orbital_angle=0) # Ship at angle 0 should be east of star pos = ship.grid_position_at_time(0) assert pos == (550, 500) # 500 + 50 # Move ship along orbit ship.move_along_orbit(90) pos = ship.grid_position_at_time(0) assert pos == (500, 550) # North of star # Set specific angle ship.set_orbit_angle(180) pos = ship.grid_position_at_time(0) assert pos == (450, 500) # West of star print(" orbiting_ship: PASS") def test_orbit_ring_cells(): """Test orbit ring cell generation.""" body = OrbitalBody( name="Planet", surface_radius=5, orbit_ring_radius=10, parent=None, base_position=(100, 100) ) cells = body.orbit_ring_cells(0) # Should have cells on the ring assert len(cells) > 0 # All cells should be approximately orbit_ring_radius from center for x, y in cells: dist = math.sqrt((x - 100) ** 2 + (y - 100) ** 2) assert 9.0 <= dist <= 11.0, f"Cell ({x},{y}) has distance {dist}" print(" orbit_ring_cells: PASS") def test_surface_cells(): """Test surface cell generation.""" body = OrbitalBody( name="Planet", surface_radius=3, orbit_ring_radius=10, parent=None, base_position=(50, 50) ) cells = body.surface_cells(0) # Center should be included assert (50, 50) in cells # All cells should be within surface_radius for x, y in cells: dist = math.sqrt((x - 50) ** 2 + (y - 50) ** 2) assert dist <= 3.5, f"Cell ({x},{y}) has distance {dist}" print(" surface_cells: PASS") def test_nearest_orbit_entry(): """Test finding nearest orbit entry point.""" body = OrbitalBody( name="Planet", surface_radius=5, orbit_ring_radius=20, parent=None, base_position=(100, 100) ) # Ship approaching from east ship_pos = (150, 100) entry_pos, angle = nearest_orbit_entry(ship_pos, body, 0) # Entry should be on the east side of orbit ring assert approx_equal(angle, 0.0) assert entry_pos == (120, 100) # 100 + 20 # Ship approaching from north-east ship_pos = (150, 150) entry_pos, angle = nearest_orbit_entry(ship_pos, body, 0) assert approx_equal(angle, 45.0) print(" nearest_orbit_entry: PASS") def test_optimal_exit_heading(): """Test finding optimal orbit exit toward target.""" body = OrbitalBody( name="Planet", surface_radius=5, orbit_ring_radius=20, parent=None, base_position=(100, 100) ) # Target to the west target = (0, 100) exit_angle, exit_pos = optimal_exit_heading(body, target, 0) assert approx_equal(exit_angle, 180.0) assert exit_pos == (80, 100) # 100 - 20 print(" optimal_exit_heading: PASS") def test_is_viable_waypoint(): """Test waypoint viability check.""" body = OrbitalBody( name="Planet", surface_radius=5, orbit_ring_radius=10, parent=None, base_position=(100, 100) ) ship_pos = (50, 100) # West of body target_east = (200, 100) # Far east target_west = (0, 100) # Far west # Body is between ship and eastern target - viable assert is_viable_waypoint(ship_pos, body, target_east, 0, angle_threshold=90) # Body is NOT between ship and western target - not viable assert not is_viable_waypoint(ship_pos, body, target_west, 0, angle_threshold=45) print(" is_viable_waypoint: PASS") def test_line_of_sight_blocked(): """Test line of sight blocking by bodies.""" blocker = OrbitalBody( name="Planet", surface_radius=10, orbit_ring_radius=20, parent=None, base_position=(100, 100) ) # LOS through the planet should be blocked p1 = (50, 100) p2 = (150, 100) result = line_of_sight_blocked(p1, p2, [blocker], 0) assert result == blocker # LOS around the planet should be clear p1 = (50, 50) p2 = (150, 50) result = line_of_sight_blocked(p1, p2, [blocker], 0) assert result is None print(" line_of_sight_blocked: PASS") def test_convenience_functions(): """Test solar system creation helpers.""" star = create_solar_system(1000, 1000, star_radius=15, star_orbit_radius=25) assert star.name == "Star" assert star.base_position == (500, 500) assert star.surface_radius == 15 assert star.orbit_ring_radius == 25 assert star.parent is None planet = create_planet( name="Terra", star=star, orbital_radius=200, surface_radius=10, orbit_ring_radius=20, angular_velocity=10, initial_angle=45 ) assert planet.name == "Terra" assert planet.parent == star assert planet.orbital_radius == 200 moon = create_moon( name="Luna", planet=planet, orbital_radius=30, surface_radius=3, orbit_ring_radius=8, angular_velocity=30 ) assert moon.name == "Luna" assert moon.parent == planet print(" convenience_functions: PASS") def test_discrete_movement(): """Test that grid positions change at discrete thresholds.""" star = OrbitalBody( name="Star", surface_radius=10, orbit_ring_radius=15, parent=None, base_position=(500, 500) ) # Planet with moderate angular velocity planet = OrbitalBody( name="Planet", surface_radius=5, orbit_ring_radius=10, parent=star, orbital_radius=100, angular_velocity=1.0, # 1 degree per turn initial_angle=0 ) # Positions should be deterministic pos0 = planet.grid_position_at_time(0) pos10 = planet.grid_position_at_time(10) pos10_again = planet.grid_position_at_time(10) # Same time = same position (deterministic) assert pos10 == pos10_again # Position should change over time assert pos0 != pos10 # Full orbit (360 degrees / 1 deg per turn = 360 turns) should return to start pos360 = planet.grid_position_at_time(360) assert pos0 == pos360 # Check the turns_until_position_changes function turns = planet.turns_until_position_changes(0) assert turns >= 1 # Should eventually change # Verify it actually changes at that turn pos_before = planet.grid_position_at_time(0) pos_after = planet.grid_position_at_time(turns) assert pos_before != pos_after print(" discrete_movement: PASS") def run_all_tests(): """Run all geometry tests.""" print("Running geometry module tests...\n") print("Utility functions:") test_distance() test_distance_squared() test_angle_between() test_normalize_angle() test_angle_difference() test_lerp() test_clamp() test_point_on_circle() test_rotate_point() print("\nGrid algorithms:") test_bresenham_circle() test_bresenham_line() test_filled_circle() print("\nOrbital body system:") test_orbital_body_stationary() test_orbital_body_simple_orbit() test_orbital_body_nested_orbit() test_orbiting_ship() test_orbit_ring_cells() test_surface_cells() test_discrete_movement() print("\nPathfinding helpers:") test_nearest_orbit_entry() test_optimal_exit_heading() test_is_viable_waypoint() test_line_of_sight_blocked() print("\nConvenience functions:") test_convenience_functions() print("\n" + "=" * 50) print("All geometry tests PASSED!") print("=" * 50) if __name__ == "__main__": run_all_tests()