artifacts-dashboard/backend/app/engine/pathfinder.py
Paweł Orzech f845647934
Some checks failed
Release / release (push) Has been cancelled
Initial release: Artifacts MMO Dashboard & Automation Platform
Full-stack dashboard for controlling, automating, and analyzing
Artifacts MMO characters via the game's HTTP API.

Backend (FastAPI):
- Async Artifacts API client with rate limiting and retry
- 6 automation strategies (combat, gathering, crafting, trading, task, leveling)
- Automation engine with runner, manager, cooldown tracker, pathfinder
- WebSocket relay (game server -> frontend)
- Game data cache, character snapshots, price history, analytics
- 9 API routers, 7 database tables, 3 Alembic migrations
- 108 unit tests

Frontend (Next.js 15 + shadcn/ui):
- Live character dashboard with HP/XP bars and cooldowns
- Character detail with stats, equipment, inventory, skills, manual actions
- Automation management with live log streaming
- Interactive canvas map with content-type coloring and zoom/pan
- Bank management, Grand Exchange with price charts
- Events, logs, analytics pages with Recharts
- WebSocket auto-reconnect with query cache invalidation
- Settings page, error boundaries, dark theme

Infrastructure:
- Docker Compose (dev + prod)
- GitHub Actions CI/CD
- Documentation (Architecture, Automation, Deployment, API)
2026-03-01 19:46:45 +01:00

130 lines
4.5 KiB
Python

import logging
from app.schemas.game import MapSchema
logger = logging.getLogger(__name__)
class Pathfinder:
"""Spatial index over the game map for finding tiles by content.
Uses Manhattan distance (since the Artifacts MMO API ``move`` action
performs a direct teleport with a cooldown proportional to Manhattan
distance). A* path-finding over walkable tiles is therefore unnecessary;
the optimal strategy is always to move directly to the target.
"""
def __init__(self) -> None:
self._maps: list[MapSchema] = []
self._map_index: dict[tuple[int, int], MapSchema] = {}
# ------------------------------------------------------------------
# Initialization
# ------------------------------------------------------------------
def load_maps(self, maps: list[MapSchema]) -> None:
"""Load map data (typically from the game data cache)."""
self._maps = list(maps)
self._map_index = {(m.x, m.y): m for m in self._maps}
logger.info("Pathfinder loaded %d map tiles", len(self._maps))
@property
def is_loaded(self) -> bool:
return len(self._maps) > 0
# ------------------------------------------------------------------
# Tile lookup
# ------------------------------------------------------------------
def get_tile(self, x: int, y: int) -> MapSchema | None:
"""Return the map tile at the given coordinates, or None."""
return self._map_index.get((x, y))
def tile_has_content(self, x: int, y: int, content_type: str, content_code: str) -> bool:
"""Check whether the tile at (x, y) has the specified content."""
tile = self._map_index.get((x, y))
if tile is None or tile.content is None:
return False
return tile.content.type == content_type and tile.content.code == content_code
def tile_has_content_type(self, x: int, y: int, content_type: str) -> bool:
"""Check whether the tile at (x, y) has any content of the given type."""
tile = self._map_index.get((x, y))
if tile is None or tile.content is None:
return False
return tile.content.type == content_type
# ------------------------------------------------------------------
# Nearest-tile search
# ------------------------------------------------------------------
def find_nearest(
self,
from_x: int,
from_y: int,
content_type: str,
content_code: str,
) -> tuple[int, int] | None:
"""Find the nearest tile whose content matches type *and* code.
Returns ``(x, y)`` of the closest match, or ``None`` if not found.
"""
best: tuple[int, int] | None = None
best_dist = float("inf")
for m in self._maps:
if (
m.content is not None
and m.content.type == content_type
and m.content.code == content_code
):
dist = abs(m.x - from_x) + abs(m.y - from_y)
if dist < best_dist:
best_dist = dist
best = (m.x, m.y)
return best
def find_nearest_by_type(
self,
from_x: int,
from_y: int,
content_type: str,
) -> tuple[int, int] | None:
"""Find the nearest tile that has any content of *content_type*.
Returns ``(x, y)`` of the closest match, or ``None`` if not found.
"""
best: tuple[int, int] | None = None
best_dist = float("inf")
for m in self._maps:
if m.content is not None and m.content.type == content_type:
dist = abs(m.x - from_x) + abs(m.y - from_y)
if dist < best_dist:
best_dist = dist
best = (m.x, m.y)
return best
def find_all(
self,
content_type: str,
content_code: str | None = None,
) -> list[tuple[int, int]]:
"""Return coordinates of all tiles matching the given content filter."""
results: list[tuple[int, int]] = []
for m in self._maps:
if m.content is None:
continue
if m.content.type != content_type:
continue
if content_code is not None and m.content.code != content_code:
continue
results.append((m.x, m.y))
return results
@staticmethod
def manhattan_distance(x1: int, y1: int, x2: int, y2: int) -> int:
"""Compute the Manhattan distance between two points."""
return abs(x1 - x2) + abs(y1 - y2)