diff --git a/tests/geometry_demo/__init__.py b/tests/geometry_demo/__init__.py new file mode 100644 index 0000000..a44e7d8 --- /dev/null +++ b/tests/geometry_demo/__init__.py @@ -0,0 +1 @@ +"""Geometry module demo system for Pinships orbital mechanics.""" diff --git a/tests/geometry_demo/geometry_main.py b/tests/geometry_demo/geometry_main.py new file mode 100644 index 0000000..e97d073 --- /dev/null +++ b/tests/geometry_demo/geometry_main.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +""" +Geometry Module Demo System + +Demonstrates the geometry module for Pinships orbital mechanics: +- Bresenham algorithms for grid-aligned circles and lines +- Angle calculations for pathfinding +- Static pathfinding through planetary orbits +- Animated solar system with discrete time steps +- Ship navigation anticipating planetary motion + +Usage: + Headless (screenshots): ./mcrogueface --headless --exec tests/geometry_demo/geometry_main.py + Interactive: ./mcrogueface tests/geometry_demo/geometry_main.py +""" +import mcrfpy +from mcrfpy import automation +import sys +import os + +# Add paths for imports +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'src', 'scripts')) + +# Import screen modules +from geometry_demo.screens.bresenham_demo import BresenhamDemo +from geometry_demo.screens.angle_lines_demo import AngleLinesDemo +from geometry_demo.screens.pathfinding_static_demo import PathfindingStaticDemo +from geometry_demo.screens.solar_system_demo import SolarSystemDemo +from geometry_demo.screens.pathfinding_animated_demo import PathfindingAnimatedDemo + +# All demo screens in order +DEMO_SCREENS = [ + BresenhamDemo, + AngleLinesDemo, + PathfindingStaticDemo, + SolarSystemDemo, + PathfindingAnimatedDemo, +] + + +class GeometryDemoRunner: + """Manages the geometry demo system.""" + + def __init__(self): + self.screens = [] + self.current_index = 0 + self.headless = self._detect_headless() + self.screenshot_dir = os.path.join(os.path.dirname(__file__), "screenshots") + + def _detect_headless(self): + """Detect if running in headless mode.""" + try: + win = mcrfpy.Window.get() + return str(win).find("headless") >= 0 + except: + return True + + def setup_all_screens(self): + """Initialize all demo screens.""" + for i, ScreenClass in enumerate(DEMO_SCREENS): + scene_name = f"geo_{i:02d}_{ScreenClass.name.lower().replace(' ', '_')}" + screen = ScreenClass(scene_name) + screen.setup() + self.screens.append(screen) + + def create_menu(self): + """Create the main menu screen.""" + mcrfpy.createScene("geo_menu") + ui = mcrfpy.sceneUI("geo_menu") + + # Background + bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600)) + bg.fill_color = mcrfpy.Color(15, 15, 25) + ui.append(bg) + + # Title + title = mcrfpy.Caption(text="Geometry Module Demo", pos=(400, 30)) + title.fill_color = mcrfpy.Color(255, 255, 255) + title.outline = 2 + title.outline_color = mcrfpy.Color(0, 0, 0) + ui.append(title) + + subtitle = mcrfpy.Caption(text="Pinships Orbital Mechanics", pos=(400, 70)) + subtitle.fill_color = mcrfpy.Color(180, 180, 180) + ui.append(subtitle) + + # Menu items + for i, screen in enumerate(self.screens): + y = 130 + i * 60 + + # Button frame + btn = mcrfpy.Frame(pos=(200, y), size=(400, 50)) + btn.fill_color = mcrfpy.Color(30, 40, 60) + btn.outline = 2 + btn.outline_color = mcrfpy.Color(80, 100, 150) + ui.append(btn) + + # Button text + label = mcrfpy.Caption(text=f"{i+1}. {screen.name}", pos=(20, 12)) + label.fill_color = mcrfpy.Color(200, 200, 255) + btn.children.append(label) + + # Description + desc = mcrfpy.Caption(text=screen.description, pos=(20, 32)) + desc.fill_color = mcrfpy.Color(120, 120, 150) + btn.children.append(desc) + + # Instructions + instr1 = mcrfpy.Caption(text="Press 1-5 to view demos", pos=(300, 480)) + instr1.fill_color = mcrfpy.Color(150, 150, 150) + ui.append(instr1) + + instr2 = mcrfpy.Caption(text="ESC = return to menu | Q = quit", pos=(270, 510)) + instr2.fill_color = mcrfpy.Color(100, 100, 100) + ui.append(instr2) + + # Credits + credits = mcrfpy.Caption(text="Geometry module: src/scripts/geometry.py", pos=(250, 560)) + credits.fill_color = mcrfpy.Color(80, 80, 100) + ui.append(credits) + + def run_headless(self): + """Run in headless mode - generate all screenshots.""" + print(f"Generating {len(self.screens)} geometry demo screenshots...") + + os.makedirs(self.screenshot_dir, exist_ok=True) + + self.current_index = 0 + self.render_wait = 0 + + def screenshot_cycle(runtime): + if self.render_wait == 0: + if self.current_index >= len(self.screens): + print("Done!") + sys.exit(0) + return + screen = self.screens[self.current_index] + mcrfpy.setScene(screen.scene_name) + self.render_wait = 1 + elif self.render_wait < 3: + # Wait for animated demos to show initial state + self.render_wait += 1 + else: + screen = self.screens[self.current_index] + filename = os.path.join(self.screenshot_dir, screen.get_screenshot_name()) + automation.screenshot(filename) + print(f" [{self.current_index+1}/{len(self.screens)}] {filename}") + + # Clean up timers for animated demos + screen.cleanup() + + self.current_index += 1 + self.render_wait = 0 + if self.current_index >= len(self.screens): + print("Done!") + sys.exit(0) + + mcrfpy.setTimer("screenshot", screenshot_cycle, 100) + + def run_interactive(self): + """Run in interactive mode with menu.""" + self.create_menu() + + def handle_key(key, state): + if state != "start": + return + + # Number keys 1-9 for direct screen access + if key in [f"Num{n}" for n in "123456789"]: + idx = int(key[-1]) - 1 + if idx < len(self.screens): + # Clean up previous screen's timers + for screen in self.screens: + screen.cleanup() + mcrfpy.setScene(self.screens[idx].scene_name) + # Re-setup the screen to restart animations + # (timers were cleaned up, need to restart) + + # ESC returns to menu + elif key == "Escape": + for screen in self.screens: + screen.cleanup() + mcrfpy.setScene("geo_menu") + + # Q quits + elif key == "Q": + sys.exit(0) + + # Register keyboard handler on all scenes + mcrfpy.setScene("geo_menu") + mcrfpy.keypressScene(handle_key) + + for screen in self.screens: + mcrfpy.setScene(screen.scene_name) + mcrfpy.keypressScene(handle_key) + + mcrfpy.setScene("geo_menu") + + +def main(): + """Main entry point.""" + runner = GeometryDemoRunner() + runner.setup_all_screens() + + if runner.headless: + runner.run_headless() + else: + runner.run_interactive() + + +# Run when executed +main() diff --git a/tests/geometry_demo/screens/__init__.py b/tests/geometry_demo/screens/__init__.py new file mode 100644 index 0000000..088c8b4 --- /dev/null +++ b/tests/geometry_demo/screens/__init__.py @@ -0,0 +1,6 @@ +"""Geometry demo screens.""" +from .bresenham_demo import BresenhamDemo +from .angle_lines_demo import AngleLinesDemo +from .pathfinding_static_demo import PathfindingStaticDemo +from .solar_system_demo import SolarSystemDemo +from .pathfinding_animated_demo import PathfindingAnimatedDemo diff --git a/tests/geometry_demo/screens/angle_lines_demo.py b/tests/geometry_demo/screens/angle_lines_demo.py new file mode 100644 index 0000000..45e0e30 --- /dev/null +++ b/tests/geometry_demo/screens/angle_lines_demo.py @@ -0,0 +1,319 @@ +"""Angle calculation demonstration with Line elements.""" +import mcrfpy +import math +from .base import (GeometryDemoScreen, angle_between, angle_difference, + normalize_angle, point_on_circle, distance) + + +class AngleLinesDemo(GeometryDemoScreen): + """Demonstrate angle calculations between points using Line elements.""" + + name = "Angle Calculations" + description = "Visualizing angles between grid positions" + + def setup(self): + self.add_title("Angle Calculations & Line Elements") + self.add_description("Computing headings, deviations, and opposite angles for pathfinding") + + # Demo 1: Basic angle between two points + self._demo_basic_angle() + + # Demo 2: Angle between three points (deviation) + self._demo_angle_deviation() + + # Demo 3: Waypoint viability visualization + self._demo_waypoint_viability() + + # Demo 4: Orbit exit heading + self._demo_orbit_exit() + + def _demo_basic_angle(self): + """Show angle from point A to point B.""" + bg = mcrfpy.Frame(pos=(30, 80), size=(350, 200)) + bg.fill_color = mcrfpy.Color(15, 15, 25) + bg.outline = 1 + bg.outline_color = mcrfpy.Color(60, 60, 100) + self.ui.append(bg) + + self.add_label("Basic Angle Calculation", 50, 85, (255, 200, 100)) + + # Point A (origin) + ax, ay = 100, 180 + # Point B (target) + bx, by = 300, 120 + + angle = angle_between((ax, ay), (bx, by)) + dist = distance((ax, ay), (bx, by)) + + # Draw the line A to B (green) + line_ab = mcrfpy.Line( + start=(ax, ay), end=(bx, by), + color=mcrfpy.Color(100, 255, 100), + thickness=3 + ) + self.ui.append(line_ab) + + # Draw reference line (east from A) in gray + line_ref = mcrfpy.Line( + start=(ax, ay), end=(ax + 150, ay), + color=mcrfpy.Color(100, 100, 100), + thickness=1 + ) + self.ui.append(line_ref) + + # Draw arc showing the angle + arc = mcrfpy.Arc( + center=(ax, ay), radius=40, + start_angle=0, end_angle=-angle, # Negative because screen Y is inverted + color=mcrfpy.Color(255, 255, 100), + thickness=2 + ) + self.ui.append(arc) + + # Points + point_a = mcrfpy.Circle(center=(ax, ay), radius=8, + fill_color=mcrfpy.Color(255, 100, 100)) + point_b = mcrfpy.Circle(center=(bx, by), radius=8, + fill_color=mcrfpy.Color(100, 255, 100)) + self.ui.append(point_a) + self.ui.append(point_b) + + # Labels + self.add_label("A", ax - 20, ay - 5, (255, 100, 100)) + self.add_label("B", bx + 10, by - 5, (100, 255, 100)) + self.add_label(f"Angle: {angle:.1f}°", 50, 250, (255, 255, 100)) + self.add_label(f"Distance: {dist:.1f}", 180, 250, (150, 150, 150)) + + def _demo_angle_deviation(self): + """Show angle deviation when considering a waypoint.""" + bg = mcrfpy.Frame(pos=(400, 80), size=(380, 200)) + bg.fill_color = mcrfpy.Color(15, 15, 25) + bg.outline = 1 + bg.outline_color = mcrfpy.Color(60, 60, 100) + self.ui.append(bg) + + self.add_label("Waypoint Deviation", 420, 85, (255, 200, 100)) + self.add_label("Is planet C a useful waypoint from A to B?", 420, 105, (150, 150, 150)) + + # Ship at A, target at B, potential waypoint C + ax, ay = 450, 230 + bx, by = 720, 180 + cx, cy = 550, 150 + + # Calculate angles + angle_to_target = angle_between((ax, ay), (bx, by)) + angle_to_waypoint = angle_between((ax, ay), (cx, cy)) + deviation = abs(angle_difference(angle_to_target, angle_to_waypoint)) + + # Draw line A to B (direct path - green) + line_ab = mcrfpy.Line( + start=(ax, ay), end=(bx, by), + color=mcrfpy.Color(100, 255, 100), + thickness=2 + ) + self.ui.append(line_ab) + + # Draw line A to C (waypoint path - yellow if viable, red if not) + viable = deviation <= 45 + waypoint_color = mcrfpy.Color(255, 255, 100) if viable else mcrfpy.Color(255, 100, 100) + line_ac = mcrfpy.Line( + start=(ax, ay), end=(cx, cy), + color=waypoint_color, + thickness=2 + ) + self.ui.append(line_ac) + + # Draw deviation arc + arc = mcrfpy.Arc( + center=(ax, ay), radius=50, + start_angle=-angle_to_target, end_angle=-angle_to_waypoint, + color=waypoint_color, + thickness=2 + ) + self.ui.append(arc) + + # Points + point_a = mcrfpy.Circle(center=(ax, ay), radius=8, + fill_color=mcrfpy.Color(255, 100, 100)) + point_b = mcrfpy.Circle(center=(bx, by), radius=8, + fill_color=mcrfpy.Color(100, 255, 100)) + point_c = mcrfpy.Circle(center=(cx, cy), radius=12, + fill_color=mcrfpy.Color(100, 100, 200), + outline_color=mcrfpy.Color(150, 150, 255), + outline=2) + self.ui.append(point_a) + self.ui.append(point_b) + self.ui.append(point_c) + + # Labels + self.add_label("A (ship)", ax - 10, ay + 10, (255, 100, 100)) + self.add_label("B (target)", bx - 20, by + 15, (100, 255, 100)) + self.add_label("C (planet)", cx + 15, cy - 5, (150, 150, 255)) + label_color = (255, 255, 100) if viable else (255, 100, 100) + self.add_label(f"Deviation: {deviation:.1f}°", 550, 250, label_color) + status = "VIABLE (<45°)" if viable else "NOT VIABLE (>45°)" + self.add_label(status, 680, 250, label_color) + + def _demo_waypoint_viability(self): + """Show multiple potential waypoints with viability indicators.""" + bg = mcrfpy.Frame(pos=(30, 300), size=(350, 280)) + bg.fill_color = mcrfpy.Color(15, 15, 25) + bg.outline = 1 + bg.outline_color = mcrfpy.Color(60, 60, 100) + self.ui.append(bg) + + self.add_label("Multiple Waypoint Analysis", 50, 305, (255, 200, 100)) + + # Ship and target + ax, ay = 80, 450 + bx, by = 320, 380 + + angle_to_target = angle_between((ax, ay), (bx, by)) + + # Draw direct path + line_ab = mcrfpy.Line( + start=(ax, ay), end=(bx, by), + color=mcrfpy.Color(100, 255, 100, 128), + thickness=2 + ) + self.ui.append(line_ab) + + # Potential waypoints at various angles + waypoints = [ + (150, 360, "W1"), # Ahead and left - viable + (200, 500, "W2"), # Below path - marginal + (100, 540, "W3"), # Behind - not viable + (250, 340, "W4"), # Almost on path - very viable + ] + + threshold = 45 + for wx, wy, label in waypoints: + angle_to_wp = angle_between((ax, ay), (wx, wy)) + deviation = abs(angle_difference(angle_to_target, angle_to_wp)) + viable = deviation <= threshold + + # Line to waypoint + color_tuple = (100, 255, 100) if viable else (255, 100, 100) + color = mcrfpy.Color(*color_tuple) + line = mcrfpy.Line( + start=(ax, ay), end=(wx, wy), + color=color, + thickness=1 + ) + self.ui.append(line) + + # Waypoint circle + wp_circle = mcrfpy.Circle( + center=(wx, wy), radius=15, + fill_color=mcrfpy.Color(80, 80, 120), + outline_color=color, + outline=2 + ) + self.ui.append(wp_circle) + + self.add_label(f"{label}:{deviation:.0f}°", wx + 18, wy - 8, color_tuple) + + # Ship and target markers + ship = mcrfpy.Circle(center=(ax, ay), radius=8, + fill_color=mcrfpy.Color(255, 200, 100)) + target = mcrfpy.Circle(center=(bx, by), radius=8, + fill_color=mcrfpy.Color(100, 255, 100)) + self.ui.append(ship) + self.ui.append(target) + + self.add_label("Ship", ax - 5, ay + 12, (255, 200, 100)) + self.add_label("Target", bx - 15, by + 12, (100, 255, 100)) + self.add_label(f"Threshold: {threshold}°", 50, 555, (150, 150, 150)) + + def _demo_orbit_exit(self): + """Show optimal orbit exit heading toward target.""" + bg = mcrfpy.Frame(pos=(400, 300), size=(380, 280)) + bg.fill_color = mcrfpy.Color(15, 15, 25) + bg.outline = 1 + bg.outline_color = mcrfpy.Color(60, 60, 100) + self.ui.append(bg) + + self.add_label("Orbit Exit Heading", 420, 305, (255, 200, 100)) + self.add_label("Ship in orbit chooses optimal exit point", 420, 325, (150, 150, 150)) + + # Planet center and orbit + px, py = 520, 450 + orbit_radius = 60 + surface_radius = 25 + + # Target position + tx, ty = 720, 380 + + # Calculate optimal exit angle + exit_angle = angle_between((px, py), (tx, ty)) + exit_x = px + orbit_radius * math.cos(math.radians(exit_angle)) + exit_y = py - orbit_radius * math.sin(math.radians(exit_angle)) # Flip for screen coords + + # Draw planet surface + planet = mcrfpy.Circle( + center=(px, py), radius=surface_radius, + fill_color=mcrfpy.Color(80, 120, 180), + outline_color=mcrfpy.Color(100, 150, 220), + outline=2 + ) + self.ui.append(planet) + + # Draw orbit ring + orbit = mcrfpy.Circle( + center=(px, py), radius=orbit_radius, + fill_color=mcrfpy.Color(0, 0, 0, 0), + outline_color=mcrfpy.Color(50, 150, 50), + outline=2 + ) + self.ui.append(orbit) + + # Draw ship positions around orbit (current position) + ship_angle = 200 # Current position + ship_x = px + orbit_radius * math.cos(math.radians(ship_angle)) + ship_y = py - orbit_radius * math.sin(math.radians(ship_angle)) + + ship = mcrfpy.Circle( + center=(ship_x, ship_y), radius=8, + fill_color=mcrfpy.Color(255, 200, 100) + ) + self.ui.append(ship) + + # Draw path: ship moves along orbit (free) to exit point + # Arc from ship position to exit position + orbit_arc = mcrfpy.Arc( + center=(px, py), radius=orbit_radius, + start_angle=-ship_angle, end_angle=-exit_angle, + color=mcrfpy.Color(255, 255, 100), + thickness=3 + ) + self.ui.append(orbit_arc) + + # Draw exit point + exit_point = mcrfpy.Circle( + center=(exit_x, exit_y), radius=6, + fill_color=mcrfpy.Color(100, 255, 100) + ) + self.ui.append(exit_point) + + # Draw line from exit to target + exit_line = mcrfpy.Line( + start=(exit_x, exit_y), end=(tx, ty), + color=mcrfpy.Color(100, 255, 100), + thickness=2 + ) + self.ui.append(exit_line) + + # Target + target = mcrfpy.Circle( + center=(tx, ty), radius=10, + fill_color=mcrfpy.Color(255, 100, 100) + ) + self.ui.append(target) + + # Labels + self.add_label("Planet", px - 20, py + surface_radius + 5, (100, 150, 220)) + self.add_label("Ship", ship_x - 25, ship_y - 15, (255, 200, 100)) + self.add_label("Exit", exit_x + 10, exit_y - 10, (100, 255, 100)) + self.add_label("Target", tx - 15, ty + 15, (255, 100, 100)) + self.add_label(f"Exit angle: {exit_angle:.1f}°", 420, 555, (150, 150, 150)) + self.add_label("Yellow arc = free orbital movement", 550, 555, (255, 255, 100)) diff --git a/tests/geometry_demo/screens/base.py b/tests/geometry_demo/screens/base.py new file mode 100644 index 0000000..3420054 --- /dev/null +++ b/tests/geometry_demo/screens/base.py @@ -0,0 +1,84 @@ +"""Base class for geometry demo screens.""" +import mcrfpy +import sys +import os + +# Add scripts path for geometry module +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..', 'src', 'scripts')) +from geometry import * + + +class GeometryDemoScreen: + """Base class for geometry demo screens.""" + + name = "Base Screen" + description = "Override this description" + + def __init__(self, scene_name): + self.scene_name = scene_name + mcrfpy.createScene(scene_name) + self.ui = mcrfpy.sceneUI(scene_name) + self.timers = [] # Track timer names for cleanup + + def setup(self): + """Override to set up the screen content.""" + pass + + def cleanup(self): + """Clean up timers when leaving screen.""" + for timer_name in self.timers: + try: + mcrfpy.delTimer(timer_name) + except: + pass + + def get_screenshot_name(self): + """Return the screenshot filename for this screen.""" + return f"{self.scene_name}.png" + + def add_title(self, text, y=10): + """Add a title caption.""" + title = mcrfpy.Caption(text=text, pos=(400, y)) + title.fill_color = mcrfpy.Color(255, 255, 255) + title.outline = 2 + title.outline_color = mcrfpy.Color(0, 0, 0) + self.ui.append(title) + return title + + def add_description(self, text, y=50): + """Add a description caption.""" + desc = mcrfpy.Caption(text=text, pos=(50, y)) + desc.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(desc) + return desc + + def add_label(self, text, x, y, color=(200, 200, 200)): + """Add a label caption.""" + label = mcrfpy.Caption(text=text, pos=(x, y)) + label.fill_color = mcrfpy.Color(*color) + self.ui.append(label) + return label + + def create_grid(self, grid_size, pos, size, cell_size=16): + """Create a grid for visualization.""" + grid = mcrfpy.Grid( + grid_size=grid_size, + pos=pos, + size=size + ) + grid.fill_color = mcrfpy.Color(20, 20, 30) + self.ui.append(grid) + return grid + + def color_grid_cell(self, grid, x, y, color): + """Color a specific grid cell.""" + try: + point = grid.at(x, y) + point.color = mcrfpy.Color(*color) if isinstance(color, tuple) else color + except: + pass # Out of bounds + + def add_timer(self, name, callback, interval): + """Add a timer and track it for cleanup.""" + mcrfpy.setTimer(name, callback, interval) + self.timers.append(name) diff --git a/tests/geometry_demo/screens/bresenham_demo.py b/tests/geometry_demo/screens/bresenham_demo.py new file mode 100644 index 0000000..02c417b --- /dev/null +++ b/tests/geometry_demo/screens/bresenham_demo.py @@ -0,0 +1,170 @@ +"""Bresenham circle algorithm demonstration on a grid.""" +import mcrfpy +from .base import GeometryDemoScreen, bresenham_circle, bresenham_line, filled_circle + + +class BresenhamDemo(GeometryDemoScreen): + """Demonstrate Bresenham circle and line algorithms on a grid.""" + + name = "Bresenham Algorithms" + description = "Grid-aligned circle and line rasterization" + + def setup(self): + self.add_title("Bresenham Circle & Line Algorithms") + self.add_description("Grid-aligned geometric primitives for orbit rings and LOS calculations") + + # Create a grid for circle demo + grid_w, grid_h = 25, 18 + cell_size = 16 + + # We need a texture for the grid - create a simple one + # Actually, let's use Grid's built-in cell coloring via GridPoint + + # Create display area with Frame background + bg1 = mcrfpy.Frame(pos=(30, 80), size=(420, 310)) + bg1.fill_color = mcrfpy.Color(15, 15, 25) + bg1.outline = 1 + bg1.outline_color = mcrfpy.Color(60, 60, 100) + self.ui.append(bg1) + + self.add_label("Bresenham Circle (radius=8)", 50, 85, (255, 200, 100)) + self.add_label("Center: (12, 9)", 50, 105, (150, 150, 150)) + + # Draw circle using UICircle primitives to show the cells + center = (12, 9) + radius = 8 + circle_cells = bresenham_circle(center, radius) + + # Draw each cell as a small rectangle + for x, y in circle_cells: + px = 40 + x * cell_size + py = 120 + y * cell_size + cell_rect = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) + cell_rect.fill_color = mcrfpy.Color(100, 200, 255) + cell_rect.outline = 0 + self.ui.append(cell_rect) + + # Draw center point + cx_px = 40 + center[0] * cell_size + cy_px = 120 + center[1] * cell_size + center_rect = mcrfpy.Frame(pos=(cx_px, cy_px), size=(cell_size - 1, cell_size - 1)) + center_rect.fill_color = mcrfpy.Color(255, 100, 100) + self.ui.append(center_rect) + + # Draw the actual circle outline for comparison + actual_circle = mcrfpy.Circle( + center=(40 + center[0] * cell_size + cell_size // 2, + 120 + center[1] * cell_size + cell_size // 2), + radius=radius * cell_size, + fill_color=mcrfpy.Color(0, 0, 0, 0), + outline_color=mcrfpy.Color(255, 255, 100, 128), + outline=2 + ) + self.ui.append(actual_circle) + + # Second demo: Bresenham line + bg2 = mcrfpy.Frame(pos=(470, 80), size=(310, 310)) + bg2.fill_color = mcrfpy.Color(15, 15, 25) + bg2.outline = 1 + bg2.outline_color = mcrfpy.Color(60, 60, 100) + self.ui.append(bg2) + + self.add_label("Bresenham Lines", 490, 85, (255, 200, 100)) + + # Draw multiple lines at different angles + lines_data = [ + ((2, 2), (17, 5), (255, 100, 100)), # Shallow + ((2, 7), (17, 14), (100, 255, 100)), # Diagonal-ish + ((2, 12), (10, 17), (100, 100, 255)), # Steep + ] + + for start, end, color in lines_data: + line_cells = bresenham_line(start, end) + for x, y in line_cells: + px = 480 + x * cell_size + py = 110 + y * cell_size + cell_rect = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) + cell_rect.fill_color = mcrfpy.Color(*color) + self.ui.append(cell_rect) + + # Draw the actual line for comparison + line = mcrfpy.Line( + start=(480 + start[0] * cell_size + cell_size // 2, + 110 + start[1] * cell_size + cell_size // 2), + end=(480 + end[0] * cell_size + cell_size // 2, + 110 + end[1] * cell_size + cell_size // 2), + color=mcrfpy.Color(255, 255, 255, 128), + thickness=1 + ) + self.ui.append(line) + + # Third demo: Filled circle (planet surface) + bg3 = mcrfpy.Frame(pos=(30, 410), size=(200, 170)) + bg3.fill_color = mcrfpy.Color(15, 15, 25) + bg3.outline = 1 + bg3.outline_color = mcrfpy.Color(60, 60, 100) + self.ui.append(bg3) + + self.add_label("Filled Circle (radius=4)", 50, 415, (255, 200, 100)) + self.add_label("Planet surface representation", 50, 435, (150, 150, 150)) + + fill_center = (6, 5) + fill_radius = 4 + filled_cells = filled_circle(fill_center, fill_radius) + + for x, y in filled_cells: + px = 40 + x * cell_size + py = 460 + y * cell_size + cell_rect = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) + # Gradient based on distance from center + dist = ((x - fill_center[0])**2 + (y - fill_center[1])**2) ** 0.5 + intensity = int(255 * (1 - dist / (fill_radius + 1))) + cell_rect.fill_color = mcrfpy.Color(intensity, intensity // 2, 50) + self.ui.append(cell_rect) + + # Fourth demo: Combined - planet with orbit ring + bg4 = mcrfpy.Frame(pos=(250, 410), size=(530, 170)) + bg4.fill_color = mcrfpy.Color(15, 15, 25) + bg4.outline = 1 + bg4.outline_color = mcrfpy.Color(60, 60, 100) + self.ui.append(bg4) + + self.add_label("Planet + Orbit Ring", 270, 415, (255, 200, 100)) + self.add_label("Surface (r=3) + Orbit (r=7)", 270, 435, (150, 150, 150)) + + planet_center = (16, 5) + surface_radius = 3 + orbit_radius = 7 + + # Draw orbit ring (behind planet) + orbit_cells = bresenham_circle(planet_center, orbit_radius) + for x, y in orbit_cells: + px = 260 + x * cell_size + py = 460 + y * cell_size + cell_rect = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) + cell_rect.fill_color = mcrfpy.Color(50, 150, 50, 180) + self.ui.append(cell_rect) + + # Draw planet surface (on top) + surface_cells = filled_circle(planet_center, surface_radius) + for x, y in surface_cells: + px = 260 + x * cell_size + py = 460 + y * cell_size + cell_rect = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) + dist = ((x - planet_center[0])**2 + (y - planet_center[1])**2) ** 0.5 + intensity = int(200 * (1 - dist / (surface_radius + 1))) + cell_rect.fill_color = mcrfpy.Color(50 + intensity, 100 + intensity // 2, 200) + self.ui.append(cell_rect) + + # Legend + self.add_label("Legend:", 600, 455, (200, 200, 200)) + + leg1 = mcrfpy.Frame(pos=(600, 475), size=(12, 12)) + leg1.fill_color = mcrfpy.Color(100, 150, 200) + self.ui.append(leg1) + self.add_label("Planet surface", 620, 473, (150, 150, 150)) + + leg2 = mcrfpy.Frame(pos=(600, 495), size=(12, 12)) + leg2.fill_color = mcrfpy.Color(50, 150, 50) + self.ui.append(leg2) + self.add_label("Orbit ring (ship positions)", 620, 493, (150, 150, 150)) diff --git a/tests/geometry_demo/screens/pathfinding_animated_demo.py b/tests/geometry_demo/screens/pathfinding_animated_demo.py new file mode 100644 index 0000000..2d8c2d2 --- /dev/null +++ b/tests/geometry_demo/screens/pathfinding_animated_demo.py @@ -0,0 +1,359 @@ +"""Animated pathfinding through a moving solar system.""" +import mcrfpy +import math +from .base import (GeometryDemoScreen, OrbitalBody, create_solar_system, + create_planet, point_on_circle, distance, angle_between, + normalize_angle, is_viable_waypoint, nearest_orbit_entry, + optimal_exit_heading) + + +class PathfindingAnimatedDemo(GeometryDemoScreen): + """Demonstrate ship navigation through moving orbital bodies.""" + + name = "Animated Pathfinding" + description = "Ship navigates through moving planets" + + def setup(self): + self.add_title("Pathfinding Through Moving Planets") + self.add_description("Ship anticipates planetary motion to use orbital slingshots") + + # Screen layout + self.center_x = 400 + self.center_y = 320 + self.scale = 2.0 + + # Background + bg = mcrfpy.Frame(pos=(50, 80), size=(700, 460)) + bg.fill_color = mcrfpy.Color(5, 5, 15) + bg.outline = 1 + bg.outline_color = mcrfpy.Color(40, 40, 80) + self.ui.append(bg) + + # Create solar system + self.star = create_solar_system( + grid_width=200, grid_height=200, + star_radius=10, star_orbit_radius=18 + ) + + # Create a planet that the ship will use as waypoint + self.planet = create_planet( + name="Waypoint", + star=self.star, + orbital_radius=80, + surface_radius=8, + orbit_ring_radius=15, + angular_velocity=5, # Moves 5 degrees per turn + initial_angle=180 # Starts on left side + ) + + # Ship state + self.ship_speed = 10 # Grid units per turn + self.ship_pos = [30, 100] # Start position (grid coords, relative to star) + self.ship_target = [100, -80] # Target position + self.ship_state = "approach" # approach, orbiting, exiting, traveling + self.ship_orbit_angle = 0 + self.current_time = 0 + + # Plan the path + self.path_plan = [] + self.current_path_index = 0 + + # Store UI elements + self.planet_circle = None + self.planet_orbit_ring = None + self.ship_circle = None + self.path_lines = [] + self.status_label = None + + # Draw static elements + self._draw_static() + + # Draw initial state + self._draw_dynamic() + + # Info panel + self._draw_info_panel() + + # Start animation + self.add_timer("pathfind_tick", self._tick, 1000) + + def _to_screen(self, grid_pos): + """Convert grid position (relative to star) to screen coordinates.""" + gx, gy = grid_pos + return (self.center_x + gx * self.scale, self.center_y - gy * self.scale) + + def _draw_static(self): + """Draw static elements.""" + star_screen = self._to_screen((0, 0)) + + # Star + star = mcrfpy.Circle( + center=star_screen, + radius=self.star.surface_radius * self.scale, + fill_color=mcrfpy.Color(255, 220, 100), + outline_color=mcrfpy.Color(255, 180, 50), + outline=2 + ) + self.ui.append(star) + + # Planet orbital path + orbit_path = mcrfpy.Circle( + center=star_screen, + radius=self.planet.orbital_radius * self.scale, + fill_color=mcrfpy.Color(0, 0, 0, 0), + outline_color=mcrfpy.Color(40, 40, 60), + outline=1 + ) + self.ui.append(orbit_path) + + # Target marker + target_screen = self._to_screen(self.ship_target) + target = mcrfpy.Circle( + center=target_screen, + radius=12, + fill_color=mcrfpy.Color(255, 100, 100), + outline_color=mcrfpy.Color(255, 200, 200), + outline=2 + ) + self.ui.append(target) + self.add_label("TARGET", target_screen[0] - 25, target_screen[1] + 15, (255, 100, 100)) + + # Start marker + start_screen = self._to_screen(self.ship_pos) + self.add_label("START", start_screen[0] - 20, start_screen[1] + 15, (100, 255, 100)) + + def _draw_dynamic(self): + """Draw/update dynamic elements (planet, ship, path).""" + # Get planet position at current time + planet_grid = self._get_planet_pos(self.current_time) + planet_screen = self._to_screen(planet_grid) + + # Planet + if self.planet_circle: + self.planet_circle.center = planet_screen + else: + self.planet_circle = mcrfpy.Circle( + center=planet_screen, + radius=self.planet.surface_radius * self.scale, + fill_color=mcrfpy.Color(100, 150, 255), + outline_color=mcrfpy.Color(150, 200, 255), + outline=2 + ) + self.ui.append(self.planet_circle) + + # Planet orbit ring + if self.planet_orbit_ring: + self.planet_orbit_ring.center = planet_screen + else: + self.planet_orbit_ring = mcrfpy.Circle( + center=planet_screen, + radius=self.planet.orbit_ring_radius * self.scale, + fill_color=mcrfpy.Color(0, 0, 0, 0), + outline_color=mcrfpy.Color(50, 150, 50), + outline=2 + ) + self.ui.append(self.planet_orbit_ring) + + # Ship + ship_screen = self._to_screen(self.ship_pos) + if self.ship_circle: + self.ship_circle.center = ship_screen + else: + self.ship_circle = mcrfpy.Circle( + center=ship_screen, + radius=8, + fill_color=mcrfpy.Color(255, 200, 100), + outline_color=mcrfpy.Color(255, 255, 200), + outline=2 + ) + self.ui.append(self.ship_circle) + + # Draw predicted path + self._draw_predicted_path() + + def _get_planet_pos(self, t): + """Get planet position in grid coords relative to star.""" + angle = self.planet.initial_angle + self.planet.angular_velocity * t + x = self.planet.orbital_radius * math.cos(math.radians(angle)) + y = self.planet.orbital_radius * math.sin(math.radians(angle)) + return (x, y) + + def _draw_predicted_path(self): + """Draw the predicted ship path.""" + # Clear old path lines + # (In a real implementation, we'd remove old lines from UI) + # For now, we'll just draw new ones each time + + ship_pos = tuple(self.ship_pos) + target = tuple(self.ship_target) + + # Simple path prediction: + # 1. Calculate when ship can intercept planet's orbit + # 2. Show line to intercept point + # 3. Show arc on orbit + # 4. Show line to target + + if self.ship_state == "approach": + # Find intercept time + intercept_time, intercept_pos = self._find_intercept() + if intercept_time: + # Line from ship to intercept + ship_screen = self._to_screen(ship_pos) + intercept_screen = self._to_screen(intercept_pos) + + # Draw approach line + approach_line = mcrfpy.Line( + start=ship_screen, end=intercept_screen, + color=mcrfpy.Color(100, 200, 255, 150), + thickness=2 + ) + self.ui.append(approach_line) + + def _find_intercept(self): + """Find when ship can intercept planet's orbit.""" + # Simplified: check next 20 turns + for dt in range(1, 20): + future_t = self.current_time + dt + planet_pos = self._get_planet_pos(future_t) + + # Distance ship could travel + max_dist = self.ship_speed * dt + + # Distance from ship to planet's orbit ring + dist_to_planet = distance(self.ship_pos, planet_pos) + dist_to_orbit = abs(dist_to_planet - self.planet.orbit_ring_radius) + + if dist_to_orbit <= max_dist: + # Calculate entry point + angle_to_planet = angle_between(self.ship_pos, planet_pos) + entry_x = planet_pos[0] + self.planet.orbit_ring_radius * math.cos(math.radians(angle_to_planet + 180)) + entry_y = planet_pos[1] + self.planet.orbit_ring_radius * math.sin(math.radians(angle_to_planet + 180)) + return (future_t, (entry_x, entry_y)) + + return (None, None) + + def _draw_info_panel(self): + """Draw information panel.""" + panel = mcrfpy.Frame(pos=(50, 545), size=(700, 45)) + panel.fill_color = mcrfpy.Color(20, 20, 35) + panel.outline = 1 + panel.outline_color = mcrfpy.Color(60, 60, 100) + self.ui.append(panel) + + # Time display + self.time_label = mcrfpy.Caption(text="Turn: 0", pos=(60, 555)) + self.time_label.fill_color = mcrfpy.Color(255, 255, 255) + self.ui.append(self.time_label) + + # Status display + self.status_label = mcrfpy.Caption(text="Status: Approaching planet", pos=(180, 555)) + self.status_label.fill_color = mcrfpy.Color(100, 200, 255) + self.ui.append(self.status_label) + + # Distance display + self.dist_label = mcrfpy.Caption(text="Distance to target: ---", pos=(450, 555)) + self.dist_label.fill_color = mcrfpy.Color(150, 150, 150) + self.ui.append(self.dist_label) + + def _tick(self, runtime): + """Advance one turn.""" + self.current_time += 1 + self.time_label.text = f"Turn: {self.current_time}" + + # Update ship based on state + if self.ship_state == "approach": + self._update_approach() + elif self.ship_state == "orbiting": + self._update_orbiting() + elif self.ship_state == "exiting": + self._update_exiting() + elif self.ship_state == "traveling": + self._update_traveling() + elif self.ship_state == "arrived": + pass # Done! + + # Update distance display + dist = distance(self.ship_pos, self.ship_target) + self.dist_label.text = f"Distance to target: {dist:.1f}" + + # Update visuals + self._draw_dynamic() + + def _update_approach(self): + """Move ship toward planet's predicted position.""" + # Find where planet will be when we can intercept + intercept_time, intercept_pos = self._find_intercept() + + if intercept_pos: + # Move toward intercept point + dx = intercept_pos[0] - self.ship_pos[0] + dy = intercept_pos[1] - self.ship_pos[1] + dist = math.sqrt(dx*dx + dy*dy) + + if dist <= self.ship_speed: + # Arrived at orbit - enter orbit + self.ship_pos = list(intercept_pos) + planet_pos = self._get_planet_pos(self.current_time) + self.ship_orbit_angle = angle_between(planet_pos, self.ship_pos) + self.ship_state = "orbiting" + self.status_label.text = "Status: In orbit (repositioning FREE)" + self.status_label.fill_color = mcrfpy.Color(255, 255, 100) + else: + # Move toward intercept + self.ship_pos[0] += (dx / dist) * self.ship_speed + self.ship_pos[1] += (dy / dist) * self.ship_speed + self.status_label.text = f"Status: Approaching intercept (T+{intercept_time - self.current_time})" + else: + # Can't find intercept, go direct + self.ship_state = "traveling" + + def _update_orbiting(self): + """Reposition on orbit toward optimal exit.""" + planet_pos = self._get_planet_pos(self.current_time) + + # Calculate optimal exit angle (toward target) + exit_angle = angle_between(planet_pos, self.ship_target) + + # Move along orbit toward exit angle (this is FREE movement) + angle_diff = exit_angle - self.ship_orbit_angle + if angle_diff > 180: + angle_diff -= 360 + elif angle_diff < -180: + angle_diff += 360 + + # Move up to 45 degrees per turn along orbit (arbitrary limit for demo) + move_angle = max(-45, min(45, angle_diff)) + self.ship_orbit_angle = normalize_angle(self.ship_orbit_angle + move_angle) + + # Update ship position to new orbital position + self.ship_pos[0] = planet_pos[0] + self.planet.orbit_ring_radius * math.cos(math.radians(self.ship_orbit_angle)) + self.ship_pos[1] = planet_pos[1] + self.planet.orbit_ring_radius * math.sin(math.radians(self.ship_orbit_angle)) + + # Check if we're at optimal exit + if abs(angle_diff) < 10: + self.ship_state = "exiting" + self.status_label.text = "Status: Exiting orbit toward target" + self.status_label.fill_color = mcrfpy.Color(100, 255, 100) + + def _update_exiting(self): + """Exit orbit and head toward target.""" + # Just transition to traveling + self.ship_state = "traveling" + + def _update_traveling(self): + """Travel directly toward target.""" + dx = self.ship_target[0] - self.ship_pos[0] + dy = self.ship_target[1] - self.ship_pos[1] + dist = math.sqrt(dx*dx + dy*dy) + + if dist <= self.ship_speed: + # Arrived! + self.ship_pos = list(self.ship_target) + self.ship_state = "arrived" + self.status_label.text = "Status: ARRIVED!" + self.status_label.fill_color = mcrfpy.Color(100, 255, 100) + else: + # Move toward target + self.ship_pos[0] += (dx / dist) * self.ship_speed + self.ship_pos[1] += (dy / dist) * self.ship_speed + self.status_label.text = f"Status: Traveling to target ({dist:.0f} units)" diff --git a/tests/geometry_demo/screens/pathfinding_static_demo.py b/tests/geometry_demo/screens/pathfinding_static_demo.py new file mode 100644 index 0000000..ca7c66f --- /dev/null +++ b/tests/geometry_demo/screens/pathfinding_static_demo.py @@ -0,0 +1,292 @@ +"""Static pathfinding demonstration with planets and orbit rings.""" +import mcrfpy +import math +from .base import (GeometryDemoScreen, OrbitalBody, bresenham_circle, filled_circle, + angle_between, distance, point_on_circle, is_viable_waypoint, + nearest_orbit_entry, optimal_exit_heading) + + +class PathfindingStaticDemo(GeometryDemoScreen): + """Demonstrate optimal path through a static solar system.""" + + name = "Static Pathfinding" + description = "Optimal path using orbital slingshots" + + def setup(self): + self.add_title("Pathfinding Through Orbital Bodies") + self.add_description("Using free orbital movement to optimize travel paths") + + # Create a scenario with multiple planets + # Ship needs to go from bottom-left to top-right + # Optimal path uses planetary orbits as "free repositioning stations" + + self.cell_size = 8 + self.offset_x = 50 + self.offset_y = 100 + + # Background + bg = mcrfpy.Frame(pos=(30, 80), size=(740, 480)) + bg.fill_color = mcrfpy.Color(5, 5, 15) + bg.outline = 1 + bg.outline_color = mcrfpy.Color(40, 40, 80) + self.ui.append(bg) + + # Define planets (center_x, center_y, surface_radius, orbit_radius, name) + self.planets = [ + (20, 45, 8, 14, "Alpha"), + (55, 25, 5, 10, "Beta"), + (70, 50, 6, 12, "Gamma"), + ] + + # Ship start and end + self.ship_start = (5, 55) + self.ship_end = (85, 10) + + # Draw grid reference (faint) + self._draw_grid_reference() + + # Draw planets with surfaces and orbit rings + for px, py, sr, orbit_r, name in self.planets: + self._draw_planet(px, py, sr, orbit_r, name) + + # Calculate and draw optimal path + self._draw_optimal_path() + + # Draw ship and target + self._draw_ship_and_target() + + # Legend + self._draw_legend() + + def _to_screen(self, gx, gy): + """Convert grid coords to screen coords.""" + return (self.offset_x + gx * self.cell_size, + self.offset_y + gy * self.cell_size) + + def _draw_grid_reference(self): + """Draw faint grid lines for reference.""" + for i in range(0, 91, 10): + # Vertical lines + x = self.offset_x + i * self.cell_size + line = mcrfpy.Line( + start=(x, self.offset_y), + end=(x, self.offset_y + 60 * self.cell_size), + color=mcrfpy.Color(30, 30, 50), + thickness=1 + ) + self.ui.append(line) + + for i in range(0, 61, 10): + # Horizontal lines + y = self.offset_y + i * self.cell_size + line = mcrfpy.Line( + start=(self.offset_x, y), + end=(self.offset_x + 90 * self.cell_size, y), + color=mcrfpy.Color(30, 30, 50), + thickness=1 + ) + self.ui.append(line) + + def _draw_planet(self, cx, cy, surface_r, orbit_r, name): + """Draw a planet with surface and orbit ring.""" + sx, sy = self._to_screen(cx, cy) + + # Orbit ring (using mcrfpy.Circle for smooth rendering) + orbit = mcrfpy.Circle( + center=(sx, sy), + radius=orbit_r * self.cell_size, + fill_color=mcrfpy.Color(0, 0, 0, 0), + outline_color=mcrfpy.Color(50, 150, 50, 150), + outline=2 + ) + self.ui.append(orbit) + + # Also draw Bresenham orbit cells for accuracy demo + orbit_cells = bresenham_circle((cx, cy), orbit_r) + for gx, gy in orbit_cells: + px, py = self._to_screen(gx, gy) + cell = mcrfpy.Frame( + pos=(px, py), + size=(self.cell_size - 1, self.cell_size - 1) + ) + cell.fill_color = mcrfpy.Color(40, 100, 40, 100) + self.ui.append(cell) + + # Planet surface (filled circle) + surface_cells = filled_circle((cx, cy), surface_r) + for gx, gy in surface_cells: + px, py = self._to_screen(gx, gy) + dist = math.sqrt((gx - cx)**2 + (gy - cy)**2) + intensity = int(180 * (1 - dist / (surface_r + 1))) + cell = mcrfpy.Frame( + pos=(px, py), + size=(self.cell_size - 1, self.cell_size - 1) + ) + cell.fill_color = mcrfpy.Color(60 + intensity, 80 + intensity//2, 150) + self.ui.append(cell) + + # Planet label + self.add_label(name, sx - 15, sy - surface_r * self.cell_size - 15, (150, 150, 200)) + + def _draw_optimal_path(self): + """Calculate and draw the optimal path using orbital waypoints.""" + # The optimal path: + # 1. Ship starts at (5, 55) + # 2. Direct line to Alpha's orbit entry + # 3. Free arc around Alpha to optimal exit + # 4. Direct line to Gamma's orbit entry + # 5. Free arc around Gamma to optimal exit + # 6. Direct line to target (85, 10) + + path_segments = [] + + # Current position + current = self.ship_start + + # For this demo, manually define the path through Alpha and Gamma + # (In a real implementation, this would be computed by the pathfinder) + + # Planet Alpha (20, 45, orbit_r=14) + alpha_center = (20, 45) + alpha_orbit = 14 + + # Entry to Alpha + entry_angle_alpha = angle_between(alpha_center, current) + entry_alpha = point_on_circle(alpha_center, alpha_orbit, entry_angle_alpha) + + # Draw line: start -> Alpha entry + self._draw_path_line(current, entry_alpha, (100, 200, 255)) + current = entry_alpha + + # Exit from Alpha toward Gamma + gamma_center = (70, 50) + exit_angle_alpha = angle_between(alpha_center, gamma_center) + exit_alpha = point_on_circle(alpha_center, alpha_orbit, exit_angle_alpha) + + # Draw arc on Alpha's orbit + self._draw_orbit_arc(alpha_center, alpha_orbit, entry_angle_alpha, exit_angle_alpha) + current = exit_alpha + + # Planet Gamma (70, 50, orbit_r=12) + gamma_orbit = 12 + + # Entry to Gamma + entry_angle_gamma = angle_between(gamma_center, current) + entry_gamma = point_on_circle(gamma_center, gamma_orbit, entry_angle_gamma) + + # Draw line: Alpha exit -> Gamma entry + self._draw_path_line(current, entry_gamma, (100, 200, 255)) + current = entry_gamma + + # Exit from Gamma toward target + exit_angle_gamma = angle_between(gamma_center, self.ship_end) + exit_gamma = point_on_circle(gamma_center, gamma_orbit, exit_angle_gamma) + + # Draw arc on Gamma's orbit + self._draw_orbit_arc(gamma_center, gamma_orbit, entry_angle_gamma, exit_angle_gamma) + current = exit_gamma + + # Final segment to target + self._draw_path_line(current, self.ship_end, (100, 200, 255)) + + # For comparison, draw direct path (inefficient) + direct_start = self._to_screen(*self.ship_start) + direct_end = self._to_screen(*self.ship_end) + direct_line = mcrfpy.Line( + start=direct_start, end=direct_end, + color=mcrfpy.Color(255, 100, 100, 80), + thickness=1 + ) + self.ui.append(direct_line) + + def _draw_path_line(self, p1, p2, color): + """Draw a path segment line.""" + s1 = self._to_screen(p1[0], p1[1]) + s2 = self._to_screen(p2[0], p2[1]) + line = mcrfpy.Line( + start=s1, end=s2, + color=mcrfpy.Color(*color), + thickness=3 + ) + self.ui.append(line) + + def _draw_orbit_arc(self, center, radius, start_angle, end_angle): + """Draw an arc showing orbital movement (free movement).""" + sx, sy = self._to_screen(center[0], center[1]) + + # Normalize angles for drawing + # Screen coordinates have Y inverted, so negate angles + arc = mcrfpy.Arc( + center=(sx, sy), + radius=radius * self.cell_size, + start_angle=-start_angle, + end_angle=-end_angle, + color=mcrfpy.Color(255, 255, 100), + thickness=4 + ) + self.ui.append(arc) + + def _draw_ship_and_target(self): + """Draw ship start and target end positions.""" + # Ship + ship_x, ship_y = self._to_screen(*self.ship_start) + ship = mcrfpy.Circle( + center=(ship_x + self.cell_size//2, ship_y + self.cell_size//2), + radius=10, + fill_color=mcrfpy.Color(255, 200, 100), + outline_color=mcrfpy.Color(255, 255, 200), + outline=2 + ) + self.ui.append(ship) + self.add_label("SHIP", ship_x - 10, ship_y + 20, (255, 200, 100)) + + # Target + target_x, target_y = self._to_screen(*self.ship_end) + target = mcrfpy.Circle( + center=(target_x + self.cell_size//2, target_y + self.cell_size//2), + radius=10, + fill_color=mcrfpy.Color(255, 100, 100), + outline_color=mcrfpy.Color(255, 200, 200), + outline=2 + ) + self.ui.append(target) + self.add_label("TARGET", target_x - 15, target_y + 20, (255, 100, 100)) + + def _draw_legend(self): + """Draw legend explaining the visualization.""" + leg_x = 50 + leg_y = 520 + + # Blue line = movement cost + line1 = mcrfpy.Line( + start=(leg_x, leg_y + 10), end=(leg_x + 30, leg_y + 10), + color=mcrfpy.Color(100, 200, 255), + thickness=3 + ) + self.ui.append(line1) + self.add_label("Impulse movement (costs energy)", leg_x + 40, leg_y + 3, (150, 150, 150)) + + # Yellow arc = free movement + arc1 = mcrfpy.Arc( + center=(leg_x + 15, leg_y + 45), radius=15, + start_angle=0, end_angle=180, + color=mcrfpy.Color(255, 255, 100), + thickness=3 + ) + self.ui.append(arc1) + self.add_label("Orbital movement (FREE)", leg_x + 40, leg_y + 35, (255, 255, 100)) + + # Red line = direct (inefficient) + line2 = mcrfpy.Line( + start=(leg_x + 300, leg_y + 10), end=(leg_x + 330, leg_y + 10), + color=mcrfpy.Color(255, 100, 100, 80), + thickness=1 + ) + self.ui.append(line2) + self.add_label("Direct path (for comparison)", leg_x + 340, leg_y + 3, (150, 150, 150)) + + # Green cells = orbit ring + cell1 = mcrfpy.Frame(pos=(leg_x + 300, leg_y + 35), size=(15, 15)) + cell1.fill_color = mcrfpy.Color(40, 100, 40) + self.ui.append(cell1) + self.add_label("Orbit ring cells (valid ship positions)", leg_x + 320, leg_y + 35, (150, 150, 150)) diff --git a/tests/geometry_demo/screens/solar_system_demo.py b/tests/geometry_demo/screens/solar_system_demo.py new file mode 100644 index 0000000..8210851 --- /dev/null +++ b/tests/geometry_demo/screens/solar_system_demo.py @@ -0,0 +1,274 @@ +"""Animated solar system demonstration.""" +import mcrfpy +import math +from .base import (GeometryDemoScreen, OrbitalBody, create_solar_system, + create_planet, create_moon, point_on_circle) + + +class SolarSystemDemo(GeometryDemoScreen): + """Demonstrate animated orbital mechanics with timer-based updates.""" + + name = "Solar System Animation" + description = "Planets orbiting with discrete time steps" + + def setup(self): + self.add_title("Animated Solar System") + self.add_description("Planets snap to grid positions as time advances (1 tick = 1 turn)") + + # Screen layout + self.center_x = 400 + self.center_y = 320 + self.scale = 1.5 # Pixels per grid unit + + # Background + bg = mcrfpy.Frame(pos=(50, 80), size=(700, 480)) + bg.fill_color = mcrfpy.Color(5, 5, 15) + bg.outline = 1 + bg.outline_color = mcrfpy.Color(40, 40, 80) + self.ui.append(bg) + + # Create the solar system using geometry module + self.star = create_solar_system( + grid_width=200, grid_height=200, + star_radius=15, star_orbit_radius=25 + ) + + # Create planets with different orbital speeds + self.planet1 = create_planet( + name="Mercury", + star=self.star, + orbital_radius=60, + surface_radius=5, + orbit_ring_radius=12, + angular_velocity=12, # Fast orbit + initial_angle=0 + ) + + self.planet2 = create_planet( + name="Venus", + star=self.star, + orbital_radius=100, + surface_radius=8, + orbit_ring_radius=16, + angular_velocity=7, # Medium orbit + initial_angle=120 + ) + + self.planet3 = create_planet( + name="Earth", + star=self.star, + orbital_radius=150, + surface_radius=10, + orbit_ring_radius=20, + angular_velocity=4, # Slow orbit + initial_angle=240 + ) + + # Moon orbiting Earth + self.moon = create_moon( + name="Luna", + planet=self.planet3, + orbital_radius=30, + surface_radius=3, + orbit_ring_radius=8, + angular_velocity=15, # Faster than Earth + initial_angle=45 + ) + + self.planets = [self.planet1, self.planet2, self.planet3] + self.moons = [self.moon] + + # Current time step + self.current_time = 0 + + # Store UI elements for updating + self.planet_circles = {} + self.orbit_rings = {} + self.moon_circles = {} + + # Draw static elements (star, orbit paths) + self._draw_static_elements() + + # Draw initial planet positions + self._draw_planets() + + # Time display + self.time_label = mcrfpy.Caption(text="Turn: 0", pos=(60, 530)) + self.time_label.fill_color = mcrfpy.Color(255, 255, 255) + self.ui.append(self.time_label) + + # Instructions + self.add_label("Time advances automatically every second", 200, 530, (150, 150, 150)) + + # Start the animation timer + self.add_timer("solar_tick", self._tick, 1000) # 1 second per turn + + def _to_screen(self, grid_pos): + """Convert grid position to screen coordinates.""" + gx, gy = grid_pos + # Center on screen, with star at center + star_pos = self.star.base_position + dx = (gx - star_pos[0]) * self.scale + dy = (gy - star_pos[1]) * self.scale + return (self.center_x + dx, self.center_y + dy) + + def _draw_static_elements(self): + """Draw elements that don't move (star, orbital paths).""" + star_screen = self._to_screen(self.star.base_position) + + # Star + star_circle = mcrfpy.Circle( + center=star_screen, + radius=self.star.surface_radius * self.scale, + fill_color=mcrfpy.Color(255, 220, 100), + outline_color=mcrfpy.Color(255, 180, 50), + outline=3 + ) + self.ui.append(star_circle) + + # Star glow effect + for i in range(3): + glow = mcrfpy.Circle( + center=star_screen, + radius=(self.star.surface_radius + 5 + i * 8) * self.scale, + fill_color=mcrfpy.Color(0, 0, 0, 0), + outline_color=mcrfpy.Color(255, 200, 50, 50 - i * 15), + outline=2 + ) + self.ui.append(glow) + + # Orbital paths (static ellipses showing where planets travel) + for planet in self.planets: + path = mcrfpy.Circle( + center=star_screen, + radius=planet.orbital_radius * self.scale, + fill_color=mcrfpy.Color(0, 0, 0, 0), + outline_color=mcrfpy.Color(40, 40, 60), + outline=1 + ) + self.ui.append(path) + + # Star label + self.add_label("Star", star_screen[0] - 15, star_screen[1] + self.star.surface_radius * self.scale + 5, + (255, 220, 100)) + + def _draw_planets(self): + """Draw planets at their current positions.""" + for planet in self.planets: + self._draw_planet(planet) + + for moon in self.moons: + self._draw_moon(moon) + + def _draw_planet(self, planet): + """Draw a single planet.""" + # Get grid position at current time + grid_pos = planet.grid_position_at_time(self.current_time) + screen_pos = self._to_screen(grid_pos) + + # Color based on planet + colors = { + "Mercury": (180, 180, 180), + "Venus": (255, 200, 150), + "Earth": (100, 150, 255), + } + color = colors.get(planet.name, (150, 150, 150)) + + # Planet surface + planet_circle = mcrfpy.Circle( + center=screen_pos, + radius=planet.surface_radius * self.scale, + fill_color=mcrfpy.Color(*color), + outline_color=mcrfpy.Color(255, 255, 255, 100), + outline=1 + ) + self.ui.append(planet_circle) + self.planet_circles[planet.name] = planet_circle + + # Orbit ring around planet + orbit_ring = mcrfpy.Circle( + center=screen_pos, + radius=planet.orbit_ring_radius * self.scale, + fill_color=mcrfpy.Color(0, 0, 0, 0), + outline_color=mcrfpy.Color(50, 150, 50, 100), + outline=1 + ) + self.ui.append(orbit_ring) + self.orbit_rings[planet.name] = orbit_ring + + # Planet label + label = mcrfpy.Caption( + text=planet.name, + pos=(screen_pos[0] - 20, screen_pos[1] - planet.surface_radius * self.scale - 15) + ) + label.fill_color = mcrfpy.Color(*color) + self.ui.append(label) + # Store label for updating + if not hasattr(self, 'planet_labels'): + self.planet_labels = {} + self.planet_labels[planet.name] = label + + def _draw_moon(self, moon): + """Draw a moon.""" + grid_pos = moon.grid_position_at_time(self.current_time) + screen_pos = self._to_screen(grid_pos) + + moon_circle = mcrfpy.Circle( + center=screen_pos, + radius=moon.surface_radius * self.scale, + fill_color=mcrfpy.Color(200, 200, 200), + outline_color=mcrfpy.Color(255, 255, 255, 100), + outline=1 + ) + self.ui.append(moon_circle) + self.moon_circles[moon.name] = moon_circle + + # Moon's orbit path around Earth + parent_pos = self._to_screen(moon.parent.grid_position_at_time(self.current_time)) + moon_path = mcrfpy.Circle( + center=parent_pos, + radius=moon.orbital_radius * self.scale, + fill_color=mcrfpy.Color(0, 0, 0, 0), + outline_color=mcrfpy.Color(60, 60, 80), + outline=1 + ) + self.ui.append(moon_path) + self.orbit_rings[moon.name + "_path"] = moon_path + + def _tick(self, runtime): + """Advance time by one turn and update planet positions.""" + self.current_time += 1 + + # Update time display + self.time_label.text = f"Turn: {self.current_time}" + + # Update planet positions + for planet in self.planets: + grid_pos = planet.grid_position_at_time(self.current_time) + screen_pos = self._to_screen(grid_pos) + + # Update circle position + if planet.name in self.planet_circles: + self.planet_circles[planet.name].center = screen_pos + self.orbit_rings[planet.name].center = screen_pos + + # Update label position + if hasattr(self, 'planet_labels') and planet.name in self.planet_labels: + self.planet_labels[planet.name].pos = ( + screen_pos[0] - 20, + screen_pos[1] - planet.surface_radius * self.scale - 15 + ) + + # Update moon positions + for moon in self.moons: + grid_pos = moon.grid_position_at_time(self.current_time) + screen_pos = self._to_screen(grid_pos) + + if moon.name in self.moon_circles: + self.moon_circles[moon.name].center = screen_pos + + # Update moon's orbital path center + parent_pos = self._to_screen(moon.parent.grid_position_at_time(self.current_time)) + path_key = moon.name + "_path" + if path_key in self.orbit_rings: + self.orbit_rings[path_key].center = parent_pos diff --git a/tests/geometry_demo/screenshots/geo_00_bresenham_algorithms.png b/tests/geometry_demo/screenshots/geo_00_bresenham_algorithms.png new file mode 100644 index 0000000..64b43e5 Binary files /dev/null and b/tests/geometry_demo/screenshots/geo_00_bresenham_algorithms.png differ diff --git a/tests/geometry_demo/screenshots/geo_01_angle_calculations.png b/tests/geometry_demo/screenshots/geo_01_angle_calculations.png new file mode 100644 index 0000000..5d1550d Binary files /dev/null and b/tests/geometry_demo/screenshots/geo_01_angle_calculations.png differ diff --git a/tests/geometry_demo/screenshots/geo_02_static_pathfinding.png b/tests/geometry_demo/screenshots/geo_02_static_pathfinding.png new file mode 100644 index 0000000..833bef1 Binary files /dev/null and b/tests/geometry_demo/screenshots/geo_02_static_pathfinding.png differ diff --git a/tests/geometry_demo/screenshots/geo_03_solar_system_animation.png b/tests/geometry_demo/screenshots/geo_03_solar_system_animation.png new file mode 100644 index 0000000..fb74ab4 Binary files /dev/null and b/tests/geometry_demo/screenshots/geo_03_solar_system_animation.png differ diff --git a/tests/geometry_demo/screenshots/geo_04_animated_pathfinding.png b/tests/geometry_demo/screenshots/geo_04_animated_pathfinding.png new file mode 100644 index 0000000..7346ebb Binary files /dev/null and b/tests/geometry_demo/screenshots/geo_04_animated_pathfinding.png differ