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)
83 lines
2.5 KiB
Python
83 lines
2.5 KiB
Python
import logging
|
|
|
|
from app.schemas.game import CharacterSchema, MonsterSchema
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Maximum level difference when selecting an "optimal" monster
|
|
_MAX_LEVEL_DELTA: int = 5
|
|
|
|
|
|
class MonsterSelector:
|
|
"""Select the best monster for a character to fight.
|
|
|
|
The selection heuristic prefers monsters within +/- 5 levels of the
|
|
character's combat level. Among those candidates, higher-level monsters
|
|
are preferred because they yield more XP.
|
|
"""
|
|
|
|
def select_optimal(
|
|
self,
|
|
character: CharacterSchema,
|
|
monsters: list[MonsterSchema],
|
|
) -> MonsterSchema | None:
|
|
"""Return the best monster for the character, or ``None`` if the
|
|
list is empty or no suitable monster exists.
|
|
|
|
Parameters
|
|
----------
|
|
character:
|
|
The character that will be fighting.
|
|
monsters:
|
|
All available monsters (typically from the game data cache).
|
|
|
|
Returns
|
|
-------
|
|
The selected monster, or None.
|
|
"""
|
|
if not monsters:
|
|
return None
|
|
|
|
char_level = character.level
|
|
|
|
# First pass: prefer monsters within the level window
|
|
candidates = [
|
|
m
|
|
for m in monsters
|
|
if abs(m.level - char_level) <= _MAX_LEVEL_DELTA
|
|
]
|
|
|
|
if not candidates:
|
|
# No monster in the preferred window -- fall back to the
|
|
# highest-level monster that is still at or below the character
|
|
below = [m for m in monsters if m.level <= char_level]
|
|
if below:
|
|
candidates = below
|
|
else:
|
|
# All monsters are higher-level; pick the lowest available
|
|
candidates = sorted(monsters, key=lambda m: m.level)
|
|
return candidates[0] if candidates else None
|
|
|
|
# Among candidates, prefer higher level for better XP
|
|
candidates.sort(key=lambda m: m.level, reverse=True)
|
|
selected = candidates[0]
|
|
|
|
logger.debug(
|
|
"Selected monster %s (level %d) for character %s (level %d)",
|
|
selected.code,
|
|
selected.level,
|
|
character.name,
|
|
char_level,
|
|
)
|
|
return selected
|
|
|
|
def filter_by_code(
|
|
self,
|
|
monsters: list[MonsterSchema],
|
|
code: str,
|
|
) -> MonsterSchema | None:
|
|
"""Return the monster with the given code, or ``None``."""
|
|
for m in monsters:
|
|
if m.code == code:
|
|
return m
|
|
return None
|