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)
149 lines
5.2 KiB
Python
149 lines
5.2 KiB
Python
"""Resource selector for choosing optimal gathering targets based on character skill level."""
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
|
|
from app.schemas.game import CharacterSchema, ResourceSchema
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class ResourceSelection:
|
|
"""Result of a resource selection decision."""
|
|
|
|
resource: ResourceSchema
|
|
score: float
|
|
reason: str
|
|
|
|
|
|
class ResourceSelector:
|
|
"""Selects the optimal resource for a character to gather based on skill level.
|
|
|
|
Prefers resources within +/- 3 levels of the character's skill.
|
|
Among eligible resources, prefers higher-level ones for better XP.
|
|
"""
|
|
|
|
# How many levels above/below the character's skill level to consider
|
|
LEVEL_RANGE: int = 3
|
|
|
|
def select_optimal(
|
|
self,
|
|
character: CharacterSchema,
|
|
resources: list[ResourceSchema],
|
|
skill: str,
|
|
) -> ResourceSelection | None:
|
|
"""Select the best resource for the character's skill level.
|
|
|
|
Parameters
|
|
----------
|
|
character:
|
|
The character whose skill level determines the selection.
|
|
resources:
|
|
Available resources to choose from.
|
|
skill:
|
|
The gathering skill to optimize for (e.g. "mining", "woodcutting", "fishing").
|
|
|
|
Returns
|
|
-------
|
|
ResourceSelection or None if no suitable resource is found.
|
|
"""
|
|
skill_level = self._get_skill_level(character, skill)
|
|
if skill_level is None:
|
|
logger.warning("Unknown skill %r for resource selection", skill)
|
|
return None
|
|
|
|
# Filter to resources that match the skill
|
|
skill_resources = [r for r in resources if r.skill == skill]
|
|
if not skill_resources:
|
|
logger.info("No resources found for skill %s", skill)
|
|
return None
|
|
|
|
# Score each resource
|
|
scored: list[tuple[ResourceSchema, float, str]] = []
|
|
for resource in skill_resources:
|
|
score, reason = self._score_resource(resource, skill_level)
|
|
if score > 0:
|
|
scored.append((resource, score, reason))
|
|
|
|
if not scored:
|
|
# Fallback: pick the highest-level resource we can actually gather
|
|
gatherable = [
|
|
r for r in skill_resources if r.level <= skill_level
|
|
]
|
|
if gatherable:
|
|
best = max(gatherable, key=lambda r: r.level)
|
|
return ResourceSelection(
|
|
resource=best,
|
|
score=0.1,
|
|
reason=f"Fallback: highest gatherable resource (level {best.level}, skill {skill_level})",
|
|
)
|
|
# Pick the lowest-level resource as absolute fallback
|
|
lowest = min(skill_resources, key=lambda r: r.level)
|
|
return ResourceSelection(
|
|
resource=lowest,
|
|
score=0.01,
|
|
reason=f"Absolute fallback: lowest resource (level {lowest.level}, skill {skill_level})",
|
|
)
|
|
|
|
# Sort by score descending and pick the best
|
|
scored.sort(key=lambda x: x[1], reverse=True)
|
|
best_resource, best_score, best_reason = scored[0]
|
|
|
|
logger.info(
|
|
"Selected resource %s (level %d) for %s level %d: %s (score=%.2f)",
|
|
best_resource.code,
|
|
best_resource.level,
|
|
skill,
|
|
skill_level,
|
|
best_reason,
|
|
best_score,
|
|
)
|
|
|
|
return ResourceSelection(
|
|
resource=best_resource,
|
|
score=best_score,
|
|
reason=best_reason,
|
|
)
|
|
|
|
def _score_resource(
|
|
self,
|
|
resource: ResourceSchema,
|
|
skill_level: int,
|
|
) -> tuple[float, str]:
|
|
"""Score a resource based on how well it matches the character's skill level.
|
|
|
|
Returns (score, reason). Score of 0 means the resource is not suitable.
|
|
"""
|
|
level_diff = resource.level - skill_level
|
|
|
|
# Cannot gather resources more than LEVEL_RANGE levels above skill
|
|
if level_diff > self.LEVEL_RANGE:
|
|
return 0.0, f"Too high level (resource {resource.level}, skill {skill_level})"
|
|
|
|
# Ideal range: within +/- LEVEL_RANGE
|
|
if abs(level_diff) <= self.LEVEL_RANGE:
|
|
# Higher level within range = more XP = better score
|
|
# Base score from level closeness (prefer higher)
|
|
base_score = 10.0 + level_diff # Range: [7, 13]
|
|
|
|
# Bonus for being at or slightly above skill level (best XP)
|
|
if 0 <= level_diff <= self.LEVEL_RANGE:
|
|
base_score += 5.0 # Prefer resources at or above skill level
|
|
|
|
reason = f"In optimal range (diff={level_diff:+d})"
|
|
return base_score, reason
|
|
|
|
# Resource is far below skill level -- still works but less XP
|
|
# level_diff < -LEVEL_RANGE
|
|
penalty = abs(level_diff) - self.LEVEL_RANGE
|
|
score = max(5.0 - penalty, 0.1)
|
|
return score, f"Below optimal range (diff={level_diff:+d})"
|
|
|
|
@staticmethod
|
|
def _get_skill_level(character: CharacterSchema, skill: str) -> int | None:
|
|
"""Extract the level for a given skill from the character schema."""
|
|
skill_attr = f"{skill}_level"
|
|
if hasattr(character, skill_attr):
|
|
return getattr(character, skill_attr)
|
|
return None
|