Initial project structure: reusable isometric bot engine with D2R implementation
This commit is contained in:
commit
e0282a7111
44 changed files with 3433 additions and 0 deletions
78
engine/navigation/pathfinder.py
Normal file
78
engine/navigation/pathfinder.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""Pathfinding for isometric game navigation.
|
||||
|
||||
Implements A* and click-to-move navigation for isometric games
|
||||
where the bot needs to move between known locations.
|
||||
"""
|
||||
|
||||
from typing import List, Tuple, Optional, Dict
|
||||
from dataclasses import dataclass
|
||||
import heapq
|
||||
import math
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Waypoint:
|
||||
"""A named location in the game world."""
|
||||
name: str
|
||||
screen_x: int
|
||||
screen_y: int
|
||||
metadata: Dict = None
|
||||
|
||||
|
||||
class WaypointGraph:
|
||||
"""Graph of connected waypoints for navigation."""
|
||||
|
||||
def __init__(self):
|
||||
self._waypoints: Dict[str, Waypoint] = {}
|
||||
self._edges: Dict[str, List[str]] = {}
|
||||
|
||||
def add_waypoint(self, waypoint: Waypoint) -> None:
|
||||
self._waypoints[waypoint.name] = waypoint
|
||||
self._edges.setdefault(waypoint.name, [])
|
||||
|
||||
def connect(self, name_a: str, name_b: str, bidirectional: bool = True) -> None:
|
||||
self._edges.setdefault(name_a, []).append(name_b)
|
||||
if bidirectional:
|
||||
self._edges.setdefault(name_b, []).append(name_a)
|
||||
|
||||
def find_path(self, start: str, goal: str) -> Optional[List[Waypoint]]:
|
||||
"""A* pathfinding between waypoints."""
|
||||
if start not in self._waypoints or goal not in self._waypoints:
|
||||
return None
|
||||
|
||||
goal_wp = self._waypoints[goal]
|
||||
|
||||
def heuristic(name: str) -> float:
|
||||
wp = self._waypoints[name]
|
||||
return math.hypot(wp.screen_x - goal_wp.screen_x, wp.screen_y - goal_wp.screen_y)
|
||||
|
||||
open_set = [(heuristic(start), 0, start)]
|
||||
came_from: Dict[str, str] = {}
|
||||
g_score: Dict[str, float] = {start: 0}
|
||||
|
||||
while open_set:
|
||||
_, cost, current = heapq.heappop(open_set)
|
||||
|
||||
if current == goal:
|
||||
path = []
|
||||
while current in came_from:
|
||||
path.append(self._waypoints[current])
|
||||
current = came_from[current]
|
||||
path.append(self._waypoints[start])
|
||||
return list(reversed(path))
|
||||
|
||||
for neighbor in self._edges.get(current, []):
|
||||
n_wp = self._waypoints[neighbor]
|
||||
c_wp = self._waypoints[current]
|
||||
edge_cost = math.hypot(n_wp.screen_x - c_wp.screen_x, n_wp.screen_y - c_wp.screen_y)
|
||||
tentative_g = g_score[current] + edge_cost
|
||||
|
||||
if tentative_g < g_score.get(neighbor, float('inf')):
|
||||
came_from[neighbor] = current
|
||||
g_score[neighbor] = tentative_g
|
||||
heapq.heappush(open_set, (tentative_g + heuristic(neighbor), tentative_g, neighbor))
|
||||
|
||||
return None
|
||||
Loading…
Add table
Add a link
Reference in a new issue