Some checks failed
Release / release (push) Has been cancelled
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)
190 lines
7.2 KiB
Python
190 lines
7.2 KiB
Python
"""Tests for the Pathfinder spatial index."""
|
|
|
|
from app.engine.pathfinder import Pathfinder
|
|
from app.schemas.game import ContentSchema, MapSchema
|
|
|
|
|
|
class TestPathfinderFindNearest:
|
|
"""Tests for Pathfinder.find_nearest()."""
|
|
|
|
def test_find_nearest(self, pathfinder_with_maps):
|
|
"""find_nearest should return the closest tile matching type and code."""
|
|
pf = pathfinder_with_maps([
|
|
(10, 10, "monster", "chicken"),
|
|
(2, 2, "monster", "chicken"),
|
|
(20, 20, "monster", "chicken"),
|
|
])
|
|
result = pf.find_nearest(0, 0, "monster", "chicken")
|
|
assert result == (2, 2)
|
|
|
|
def test_find_nearest_prefers_manhattan_distance(self, pathfinder_with_maps):
|
|
"""find_nearest should use Manhattan distance, not Euclidean."""
|
|
pf = pathfinder_with_maps([
|
|
(3, 0, "monster", "wolf"), # Manhattan=3
|
|
(2, 2, "monster", "wolf"), # Manhattan=4
|
|
])
|
|
result = pf.find_nearest(0, 0, "monster", "wolf")
|
|
assert result == (3, 0)
|
|
|
|
def test_find_nearest_no_match(self, pathfinder_with_maps):
|
|
"""find_nearest should return None when no tile matches."""
|
|
pf = pathfinder_with_maps([
|
|
(1, 1, "monster", "chicken"),
|
|
])
|
|
result = pf.find_nearest(0, 0, "monster", "dragon")
|
|
assert result is None
|
|
|
|
def test_find_nearest_empty_map(self):
|
|
"""find_nearest should return None on an empty map."""
|
|
pf = Pathfinder()
|
|
result = pf.find_nearest(0, 0, "monster", "chicken")
|
|
assert result is None
|
|
|
|
def test_find_nearest_ignores_different_type(self, pathfinder_with_maps):
|
|
"""find_nearest should not match tiles with a different content type."""
|
|
pf = pathfinder_with_maps([
|
|
(1, 1, "resource", "chicken"), # same code, different type
|
|
])
|
|
result = pf.find_nearest(0, 0, "monster", "chicken")
|
|
assert result is None
|
|
|
|
def test_find_nearest_at_origin(self, pathfinder_with_maps):
|
|
"""find_nearest should return a tile at (0, 0) if it matches."""
|
|
pf = pathfinder_with_maps([
|
|
(0, 0, "bank", "bank"),
|
|
(5, 5, "bank", "bank"),
|
|
])
|
|
result = pf.find_nearest(0, 0, "bank", "bank")
|
|
assert result == (0, 0)
|
|
|
|
|
|
class TestPathfinderFindNearestByType:
|
|
"""Tests for Pathfinder.find_nearest_by_type()."""
|
|
|
|
def test_find_nearest_by_type(self, pathfinder_with_maps):
|
|
"""find_nearest_by_type should find by type regardless of code."""
|
|
pf = pathfinder_with_maps([
|
|
(10, 10, "bank", "city_bank"),
|
|
(3, 3, "bank", "village_bank"),
|
|
])
|
|
result = pf.find_nearest_by_type(0, 0, "bank")
|
|
assert result == (3, 3)
|
|
|
|
def test_find_nearest_by_type_no_match(self, pathfinder_with_maps):
|
|
"""find_nearest_by_type should return None when no type matches."""
|
|
pf = pathfinder_with_maps([
|
|
(1, 1, "monster", "chicken"),
|
|
])
|
|
result = pf.find_nearest_by_type(0, 0, "bank")
|
|
assert result is None
|
|
|
|
|
|
class TestPathfinderFindAll:
|
|
"""Tests for Pathfinder.find_all()."""
|
|
|
|
def test_find_all(self, pathfinder_with_maps):
|
|
"""find_all should return all tiles matching type and code."""
|
|
pf = pathfinder_with_maps([
|
|
(1, 1, "monster", "chicken"),
|
|
(5, 5, "monster", "chicken"),
|
|
(3, 3, "monster", "wolf"),
|
|
(7, 7, "resource", "copper"),
|
|
])
|
|
result = pf.find_all("monster", "chicken")
|
|
assert sorted(result) == [(1, 1), (5, 5)]
|
|
|
|
def test_find_all_by_type_only(self, pathfinder_with_maps):
|
|
"""find_all with code=None should return all tiles of the given type."""
|
|
pf = pathfinder_with_maps([
|
|
(1, 1, "monster", "chicken"),
|
|
(5, 5, "monster", "wolf"),
|
|
(7, 7, "resource", "copper"),
|
|
])
|
|
result = pf.find_all("monster")
|
|
assert sorted(result) == [(1, 1), (5, 5)]
|
|
|
|
def test_find_all_no_match(self, pathfinder_with_maps):
|
|
"""find_all should return an empty list when nothing matches."""
|
|
pf = pathfinder_with_maps([
|
|
(1, 1, "monster", "chicken"),
|
|
])
|
|
result = pf.find_all("bank", "bank")
|
|
assert result == []
|
|
|
|
def test_find_all_empty_map(self):
|
|
"""find_all should return an empty list on an empty map."""
|
|
pf = Pathfinder()
|
|
result = pf.find_all("monster")
|
|
assert result == []
|
|
|
|
|
|
class TestPathfinderTileHasContent:
|
|
"""Tests for Pathfinder.tile_has_content() and tile_has_content_type()."""
|
|
|
|
def test_tile_has_content(self, pathfinder_with_maps):
|
|
"""tile_has_content should return True for an exact match."""
|
|
pf = pathfinder_with_maps([
|
|
(5, 5, "monster", "chicken"),
|
|
])
|
|
assert pf.tile_has_content(5, 5, "monster", "chicken") is True
|
|
|
|
def test_tile_has_content_wrong_code(self, pathfinder_with_maps):
|
|
"""tile_has_content should return False for a code mismatch."""
|
|
pf = pathfinder_with_maps([
|
|
(5, 5, "monster", "chicken"),
|
|
])
|
|
assert pf.tile_has_content(5, 5, "monster", "wolf") is False
|
|
|
|
def test_tile_has_content_missing_tile(self):
|
|
"""tile_has_content should return False for a non-existent tile."""
|
|
pf = Pathfinder()
|
|
assert pf.tile_has_content(99, 99, "monster", "chicken") is False
|
|
|
|
def test_tile_has_content_no_content(self, make_map_tile):
|
|
"""tile_has_content should return False for a tile with no content."""
|
|
pf = Pathfinder()
|
|
tile = make_map_tile(1, 1) # No content_type/content_code
|
|
pf.load_maps([tile])
|
|
assert pf.tile_has_content(1, 1, "monster", "chicken") is False
|
|
|
|
def test_tile_has_content_type(self, pathfinder_with_maps):
|
|
"""tile_has_content_type should match on type alone."""
|
|
pf = pathfinder_with_maps([
|
|
(5, 5, "monster", "chicken"),
|
|
])
|
|
assert pf.tile_has_content_type(5, 5, "monster") is True
|
|
assert pf.tile_has_content_type(5, 5, "bank") is False
|
|
|
|
|
|
class TestPathfinderMisc:
|
|
"""Tests for miscellaneous Pathfinder methods."""
|
|
|
|
def test_is_loaded_false_initially(self):
|
|
"""is_loaded should be False before any maps are loaded."""
|
|
pf = Pathfinder()
|
|
assert pf.is_loaded is False
|
|
|
|
def test_is_loaded_true_after_load(self, pathfinder_with_maps):
|
|
"""is_loaded should be True after loading maps."""
|
|
pf = pathfinder_with_maps([(0, 0, "bank", "bank")])
|
|
assert pf.is_loaded is True
|
|
|
|
def test_get_tile(self, pathfinder_with_maps):
|
|
"""get_tile should return the MapSchema at the given coordinates."""
|
|
pf = pathfinder_with_maps([(3, 7, "monster", "chicken")])
|
|
tile = pf.get_tile(3, 7)
|
|
assert tile is not None
|
|
assert tile.x == 3
|
|
assert tile.y == 7
|
|
assert tile.content.code == "chicken"
|
|
|
|
def test_get_tile_missing(self):
|
|
"""get_tile should return None for coordinates not in the index."""
|
|
pf = Pathfinder()
|
|
assert pf.get_tile(99, 99) is None
|
|
|
|
def test_manhattan_distance(self):
|
|
"""manhattan_distance should compute |x1-x2| + |y1-y2|."""
|
|
assert Pathfinder.manhattan_distance(0, 0, 3, 4) == 7
|
|
assert Pathfinder.manhattan_distance(5, 5, 5, 5) == 0
|
|
assert Pathfinder.manhattan_distance(-2, 3, 1, -1) == 7
|