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)
156 lines
5.4 KiB
Python
156 lines
5.4 KiB
Python
"""Tests for MonsterSelector."""
|
|
|
|
from app.engine.decision.monster_selector import MonsterSelector
|
|
|
|
|
|
class TestMonsterSelectorSelectOptimal:
|
|
"""Tests for MonsterSelector.select_optimal()."""
|
|
|
|
def test_select_optimal_near_level(self, make_character, make_monster):
|
|
"""Prefers monsters within +/- 5 levels of the character."""
|
|
selector = MonsterSelector()
|
|
char = make_character(level=10)
|
|
monsters = [
|
|
make_monster(code="chicken", level=1),
|
|
make_monster(code="wolf", level=8),
|
|
make_monster(code="bear", level=12),
|
|
make_monster(code="dragon", level=30),
|
|
]
|
|
|
|
result = selector.select_optimal(char, monsters)
|
|
|
|
# wolf (8) and bear (12) are within +/- 5 of level 10
|
|
# bear (12) should be preferred because higher level = more XP
|
|
assert result is not None
|
|
assert result.code == "bear"
|
|
|
|
def test_select_optimal_no_monsters(self, make_character):
|
|
"""Returns None for an empty monster list."""
|
|
selector = MonsterSelector()
|
|
char = make_character(level=10)
|
|
|
|
result = selector.select_optimal(char, [])
|
|
|
|
assert result is None
|
|
|
|
def test_prefer_higher_level(self, make_character, make_monster):
|
|
"""Among candidates within range, prefers higher level."""
|
|
selector = MonsterSelector()
|
|
char = make_character(level=10)
|
|
monsters = [
|
|
make_monster(code="wolf", level=8),
|
|
make_monster(code="ogre", level=13),
|
|
make_monster(code="bear", level=11),
|
|
]
|
|
|
|
result = selector.select_optimal(char, monsters)
|
|
|
|
# All within +/- 5 of 10; ogre at 13 is highest
|
|
assert result is not None
|
|
assert result.code == "ogre"
|
|
|
|
def test_select_exact_level(self, make_character, make_monster):
|
|
"""A monster at exactly the character's level should be a valid candidate."""
|
|
selector = MonsterSelector()
|
|
char = make_character(level=5)
|
|
monsters = [
|
|
make_monster(code="goblin", level=5),
|
|
]
|
|
|
|
result = selector.select_optimal(char, monsters)
|
|
|
|
assert result is not None
|
|
assert result.code == "goblin"
|
|
|
|
def test_fallback_below_level(self, make_character, make_monster):
|
|
"""When no monsters are in range, falls back to the best below-level monster."""
|
|
selector = MonsterSelector()
|
|
char = make_character(level=20)
|
|
monsters = [
|
|
make_monster(code="chicken", level=1),
|
|
make_monster(code="rat", level=3),
|
|
make_monster(code="wolf", level=10),
|
|
]
|
|
|
|
result = selector.select_optimal(char, monsters)
|
|
|
|
# All are more than 5 levels below 20, so fallback to highest below-level
|
|
assert result is not None
|
|
assert result.code == "wolf"
|
|
|
|
def test_fallback_all_above(self, make_character, make_monster):
|
|
"""When all monsters are above the character, picks the lowest-level one."""
|
|
selector = MonsterSelector()
|
|
char = make_character(level=1)
|
|
monsters = [
|
|
make_monster(code="dragon", level=30),
|
|
make_monster(code="demon", level=25),
|
|
make_monster(code="ogre", level=20),
|
|
]
|
|
|
|
result = selector.select_optimal(char, monsters)
|
|
|
|
# All above and out of range; within range [1-5..1+5] = [-4..6] none qualify.
|
|
# No monsters at or below level 1, so absolute fallback to lowest
|
|
assert result is not None
|
|
assert result.code == "ogre"
|
|
|
|
def test_boundary_level_included(self, make_character, make_monster):
|
|
"""Monsters exactly 5 levels away should be included in candidates."""
|
|
selector = MonsterSelector()
|
|
char = make_character(level=10)
|
|
monsters = [
|
|
make_monster(code="exactly_minus_5", level=5),
|
|
make_monster(code="exactly_plus_5", level=15),
|
|
]
|
|
|
|
result = selector.select_optimal(char, monsters)
|
|
|
|
# Both are exactly at the boundary; prefer higher level
|
|
assert result is not None
|
|
assert result.code == "exactly_plus_5"
|
|
|
|
def test_single_monster(self, make_character, make_monster):
|
|
"""With a single monster, it should always be selected."""
|
|
selector = MonsterSelector()
|
|
char = make_character(level=10)
|
|
monsters = [make_monster(code="solo", level=50)]
|
|
|
|
result = selector.select_optimal(char, monsters)
|
|
|
|
assert result is not None
|
|
assert result.code == "solo"
|
|
|
|
|
|
class TestMonsterSelectorFilterByCode:
|
|
"""Tests for MonsterSelector.filter_by_code()."""
|
|
|
|
def test_filter_by_code_found(self, make_monster):
|
|
"""filter_by_code should return the matching monster."""
|
|
selector = MonsterSelector()
|
|
monsters = [
|
|
make_monster(code="chicken"),
|
|
make_monster(code="wolf"),
|
|
]
|
|
|
|
result = selector.filter_by_code(monsters, "wolf")
|
|
|
|
assert result is not None
|
|
assert result.code == "wolf"
|
|
|
|
def test_filter_by_code_not_found(self, make_monster):
|
|
"""filter_by_code should return None when no monster matches."""
|
|
selector = MonsterSelector()
|
|
monsters = [make_monster(code="chicken")]
|
|
|
|
result = selector.filter_by_code(monsters, "dragon")
|
|
|
|
assert result is None
|
|
|
|
def test_filter_by_code_empty_list(self):
|
|
"""filter_by_code should return None for an empty list."""
|
|
selector = MonsterSelector()
|
|
|
|
result = selector.filter_by_code([], "chicken")
|
|
|
|
assert result is None
|