artifacts-dashboard/backend/app/engine/decision/resource_selector.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

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