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)
220 lines
8.2 KiB
Python
220 lines
8.2 KiB
Python
"""Tests for the GatheringStrategy state machine."""
|
|
|
|
import pytest
|
|
|
|
from app.engine.strategies.base import ActionType
|
|
from app.engine.strategies.gathering import GatheringStrategy
|
|
from app.schemas.game import InventorySlot
|
|
|
|
|
|
class TestGatheringStrategyMovement:
|
|
"""Tests for movement to resource tiles."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_move_to_resource(self, make_character, pathfinder_with_maps):
|
|
"""When not at resource location, the strategy should return MOVE."""
|
|
pf = pathfinder_with_maps([
|
|
(3, 4, "resource", "copper_rocks"),
|
|
(10, 0, "bank", "bank"),
|
|
])
|
|
strategy = GatheringStrategy({"resource_code": "copper_rocks"}, pf)
|
|
char = make_character(x=0, y=0)
|
|
|
|
plan = await strategy.next_action(char)
|
|
|
|
assert plan.action_type == ActionType.MOVE
|
|
assert plan.params == {"x": 3, "y": 4}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_idle_when_no_resource_found(self, make_character, pathfinder_with_maps):
|
|
"""When no matching resource tile exists, the strategy should IDLE."""
|
|
pf = pathfinder_with_maps([
|
|
(10, 0, "bank", "bank"),
|
|
])
|
|
strategy = GatheringStrategy({"resource_code": "gold_rocks"}, pf)
|
|
char = make_character(x=0, y=0)
|
|
|
|
plan = await strategy.next_action(char)
|
|
|
|
assert plan.action_type == ActionType.IDLE
|
|
|
|
|
|
class TestGatheringStrategyGathering:
|
|
"""Tests for gathering behavior at the resource tile."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gather_when_at_resource(self, make_character, pathfinder_with_maps):
|
|
"""When at resource and inventory has space, the strategy should GATHER."""
|
|
pf = pathfinder_with_maps([
|
|
(3, 4, "resource", "copper_rocks"),
|
|
(10, 0, "bank", "bank"),
|
|
])
|
|
strategy = GatheringStrategy({"resource_code": "copper_rocks"}, pf)
|
|
char = make_character(x=3, y=4, inventory_max_items=20, inventory=[])
|
|
|
|
plan = await strategy.next_action(char)
|
|
|
|
assert plan.action_type == ActionType.GATHER
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gather_when_inventory_has_some_items(self, make_character, pathfinder_with_maps):
|
|
"""Gathering should continue as long as inventory is not completely full."""
|
|
pf = pathfinder_with_maps([
|
|
(3, 4, "resource", "copper_rocks"),
|
|
(10, 0, "bank", "bank"),
|
|
])
|
|
strategy = GatheringStrategy({"resource_code": "copper_rocks"}, pf)
|
|
items = [InventorySlot(slot=0, code="copper_ore", quantity=5)]
|
|
char = make_character(x=3, y=4, inventory_max_items=20, inventory=items)
|
|
|
|
plan = await strategy.next_action(char)
|
|
|
|
assert plan.action_type == ActionType.GATHER
|
|
|
|
|
|
class TestGatheringStrategyDeposit:
|
|
"""Tests for deposit behavior when inventory is full."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_deposit_when_full(self, make_character, pathfinder_with_maps):
|
|
"""When inventory is full and deposit_on_full is True, move to bank."""
|
|
pf = pathfinder_with_maps([
|
|
(3, 4, "resource", "copper_rocks"),
|
|
(10, 0, "bank", "bank"),
|
|
])
|
|
strategy = GatheringStrategy(
|
|
{"resource_code": "copper_rocks", "deposit_on_full": True},
|
|
pf,
|
|
)
|
|
items = [InventorySlot(slot=i, code="copper_ore", quantity=1) for i in range(20)]
|
|
char = make_character(x=3, y=4, inventory_max_items=20, inventory=items)
|
|
|
|
plan = await strategy.next_action(char)
|
|
|
|
# When at resource with full inventory, the gather handler detects full inventory
|
|
# and transitions to check_inventory -> move_to_bank -> MOVE
|
|
assert plan.action_type == ActionType.MOVE
|
|
assert plan.params == {"x": 10, "y": 0}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_deposit_items_at_bank(self, make_character, pathfinder_with_maps):
|
|
"""When at bank with items, the strategy should DEPOSIT_ITEM."""
|
|
pf = pathfinder_with_maps([
|
|
(3, 4, "resource", "copper_rocks"),
|
|
(10, 0, "bank", "bank"),
|
|
])
|
|
strategy = GatheringStrategy(
|
|
{"resource_code": "copper_rocks", "deposit_on_full": True},
|
|
pf,
|
|
)
|
|
items = [InventorySlot(slot=i, code="copper_ore", quantity=1) for i in range(20)]
|
|
|
|
# First, move to bank
|
|
char_full = make_character(x=3, y=4, inventory_max_items=20, inventory=items)
|
|
await strategy.next_action(char_full) # MOVE to bank
|
|
|
|
# Now at bank
|
|
char_at_bank = make_character(x=10, y=0, inventory_max_items=20, inventory=items)
|
|
plan = await strategy.next_action(char_at_bank)
|
|
|
|
assert plan.action_type == ActionType.DEPOSIT_ITEM
|
|
assert plan.params["code"] == "copper_ore"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_complete_when_full_no_deposit(self, make_character, pathfinder_with_maps):
|
|
"""When inventory is full and deposit_on_full is False, COMPLETE."""
|
|
pf = pathfinder_with_maps([
|
|
(3, 4, "resource", "copper_rocks"),
|
|
])
|
|
strategy = GatheringStrategy(
|
|
{"resource_code": "copper_rocks", "deposit_on_full": False},
|
|
pf,
|
|
)
|
|
items = [InventorySlot(slot=i, code="copper_ore", quantity=1) for i in range(20)]
|
|
char = make_character(x=3, y=4, inventory_max_items=20, inventory=items)
|
|
|
|
plan = await strategy.next_action(char)
|
|
|
|
assert plan.action_type == ActionType.COMPLETE
|
|
|
|
|
|
class TestGatheringStrategyMaxLoops:
|
|
"""Tests for the max_loops limit."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_max_loops(self, make_character, pathfinder_with_maps):
|
|
"""Strategy should return COMPLETE after max_loops deposit cycles."""
|
|
pf = pathfinder_with_maps([
|
|
(3, 4, "resource", "copper_rocks"),
|
|
(10, 0, "bank", "bank"),
|
|
])
|
|
strategy = GatheringStrategy(
|
|
{"resource_code": "copper_rocks", "max_loops": 1},
|
|
pf,
|
|
)
|
|
|
|
# Simulate a complete gather-deposit cycle to increment _loop_count
|
|
strategy._loop_count = 1 # Simulate one completed cycle
|
|
|
|
char = make_character(x=3, y=4)
|
|
plan = await strategy.next_action(char)
|
|
|
|
assert plan.action_type == ActionType.COMPLETE
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_max_loops(self, make_character, pathfinder_with_maps):
|
|
"""With max_loops=0 (default), the strategy should never COMPLETE due to loops."""
|
|
pf = pathfinder_with_maps([
|
|
(3, 4, "resource", "copper_rocks"),
|
|
(10, 0, "bank", "bank"),
|
|
])
|
|
strategy = GatheringStrategy(
|
|
{"resource_code": "copper_rocks", "max_loops": 0},
|
|
pf,
|
|
)
|
|
|
|
# Even with many loops completed, max_loops=0 means infinite
|
|
strategy._loop_count = 999
|
|
|
|
char = make_character(x=3, y=4)
|
|
plan = await strategy.next_action(char)
|
|
|
|
# Should still gather, not COMPLETE
|
|
assert plan.action_type != ActionType.COMPLETE
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_loop_count_increments_after_deposit(self, make_character, pathfinder_with_maps):
|
|
"""The loop counter should increment after depositing all items."""
|
|
pf = pathfinder_with_maps([
|
|
(3, 4, "resource", "copper_rocks"),
|
|
(10, 0, "bank", "bank"),
|
|
])
|
|
strategy = GatheringStrategy(
|
|
{"resource_code": "copper_rocks", "max_loops": 5},
|
|
pf,
|
|
)
|
|
|
|
assert strategy._loop_count == 0
|
|
|
|
# Simulate being at bank with empty inventory (all items deposited)
|
|
# and in DEPOSIT state
|
|
strategy._state = strategy._state.__class__("deposit")
|
|
strategy._resource_pos = (3, 4)
|
|
strategy._bank_pos = (10, 0)
|
|
|
|
char = make_character(x=10, y=0, inventory=[])
|
|
await strategy.next_action(char)
|
|
|
|
assert strategy._loop_count == 1
|
|
|
|
|
|
class TestGatheringStrategyGetState:
|
|
"""Tests for get_state() reporting."""
|
|
|
|
def test_initial_state(self, pathfinder_with_maps):
|
|
"""Initial state should be move_to_resource."""
|
|
pf = pathfinder_with_maps([
|
|
(3, 4, "resource", "copper_rocks"),
|
|
])
|
|
strategy = GatheringStrategy({"resource_code": "copper_rocks"}, pf)
|
|
assert strategy.get_state() == "move_to_resource"
|