"""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)"