Source code for simulation_framework.src.actions.movement

from __future__ import annotations

import math
from typing import TYPE_CHECKING, List, Optional, Tuple

from ..systems.pathfinding import Pathfinder
from .base import Action, ActionResult, Event, ResourceCost

if TYPE_CHECKING:
    from ..core.world import World
    from ..entities.base import Entity


[docs] class MoveAction(Action): def __init__(self, actor_id: int, destination: Tuple[int, int]): super().__init__(actor_id) self.destination = destination
[docs] def can_execute(self, actor: Entity, world: World) -> bool: if not world.is_valid_position(self.destination[0], self.destination[1]): return False if not world.is_passable(self.destination[0], self.destination[1]): return False current_x, current_y = actor.position dest_x, dest_y = self.destination distance = math.sqrt((dest_x - current_x) ** 2 + (dest_y - current_y) ** 2) if distance > 1.5: return False return self.get_cost().can_afford(actor)
[docs] def execute(self, actor: Entity, world: World) -> ActionResult: if not self.can_execute(actor, world): return ActionResult.failure("Cannot move to destination") cost = self.get_cost() if not cost.consume(actor): return ActionResult.failure("Not enough resources to move") success = world.move_entity(actor, self.destination[0], self.destination[1]) if success: event = Event( event_type="move", actor_id=actor.id, data={"from": actor.position, "to": self.destination}, ) return ActionResult.success(f"Moved to {self.destination}", [event]) else: return ActionResult.failure("Failed to move")
[docs] def get_duration(self) -> int: return 1
[docs] def get_cost(self) -> ResourceCost: return ResourceCost(stamina=1)
def __repr__(self) -> str: return f"MoveAction(to={self.destination})"
[docs] class PathfindAction(Action): def __init__( self, actor_id: int, destination: Tuple[int, int], pathfinder: Optional[Pathfinder] = None, use_fog_of_war: bool = False, ): super().__init__(actor_id) self.destination = destination self.pathfinder = pathfinder or Pathfinder() self.use_fog_of_war = use_fog_of_war self.path: List[Tuple[int, int]] = [] self.current_step = 0
[docs] def can_execute(self, actor: Entity, world: World) -> bool: if not world.is_valid_position(self.destination[0], self.destination[1]): return False if not world.is_passable(self.destination[0], self.destination[1]): return False known_tiles = None if self.use_fog_of_war and hasattr(actor, "known_map"): known_tiles = actor.known_map.get_explored_tiles() self.path = self.pathfinder.find_path( actor.position, self.destination, world, known_tiles ) if not self.path or len(self.path) < 2: return False return True
[docs] def execute(self, actor: Entity, world: World) -> ActionResult: if not self.path and not self.can_execute(actor, world): return ActionResult.failure("No path to destination") if self.current_step >= len(self.path) - 1: return ActionResult.success("Destination reached") next_position = self.path[self.current_step + 1] move_action = MoveAction(self.actor_id, next_position) result = move_action.execute(actor, world) if result.success: self.current_step += 1 if self.current_step >= len(self.path) - 1: return ActionResult.success("Pathfinding complete", result.events) else: return ActionResult( success=True, message=f"Step {self.current_step}/{len(self.path)-1}", events=result.events, ) else: known_tiles = None if self.use_fog_of_war and hasattr(actor, "known_map"): known_tiles = actor.known_map.get_explored_tiles() new_path = self.pathfinder.find_path( actor.position, self.destination, world, known_tiles ) if new_path and len(new_path) > 1: self.path = new_path self.current_step = 0 return ActionResult( success=True, message="Recalculating path", events=[] ) else: return ActionResult.failure("Path blocked and no alternative found")
[docs] def get_duration(self) -> int: return len(self.path) if self.path else 1
[docs] def get_cost(self) -> ResourceCost: path_length = len(self.path) if self.path else 1 return ResourceCost(stamina=path_length)
[docs] def get_remaining_steps(self) -> int: if not self.path: return 0 return len(self.path) - 1 - self.current_step
[docs] def get_current_target(self) -> Optional[Tuple[int, int]]: if not self.path or self.current_step >= len(self.path) - 1: return None return self.path[self.current_step + 1]
def __repr__(self) -> str: return f"PathfindAction(to={self.destination}, step={self.current_step}/{len(self.path) if self.path else 0})"
[docs] class WanderAction(Action): def __init__( self, actor_id: int, center: Optional[Tuple[int, int]] = None, max_distance: int = 5, ): super().__init__(actor_id) self.center = center self.max_distance = max_distance self.target_position: Optional[Tuple[int, int]] = None self.pathfind_action: Optional[PathfindAction] = None
[docs] def can_execute(self, actor: Entity, world: World) -> bool: center = self.center if self.center else actor.position candidates = [] cx, cy = center for dx in range(-self.max_distance, self.max_distance + 1): for dy in range(-self.max_distance, self.max_distance + 1): if dx == 0 and dy == 0: continue x, y = cx + dx, cy + dy if world.is_valid_position(x, y) and world.is_passable(x, y): distance = math.sqrt(dx**2 + dy**2) if distance <= self.max_distance: candidates.append((x, y)) if not candidates: return False import random self.target_position = random.choice(candidates) return True
[docs] def execute(self, actor: Entity, world: World) -> ActionResult: if not self.target_position: if not self.can_execute(actor, world): return ActionResult.failure("No valid wandering destination") # Create pathfind action once and reuse it if not self.pathfind_action: self.pathfind_action = PathfindAction(self.actor_id, self.target_position) self.pathfind_action.start(self.start_tick) self.pathfind_action.is_active = True return self.pathfind_action.execute(actor, world)
[docs] def get_duration(self) -> int: # Duration is dynamic based on pathfinding, but we need a reasonable estimate if self.pathfind_action: return self.pathfind_action.get_duration() elif self.target_position: # Rough estimate based on max distance return self.max_distance + 2 return 5 # Default estimate
[docs] def get_cost(self) -> ResourceCost: return ResourceCost(stamina=2)
def __repr__(self) -> str: return f"WanderAction(center={self.center}, max_dist={self.max_distance})"