- Market tab now calls GET /grandexchange/orders (all public orders) instead of /my/grandexchange/orders (own orders only), fixing the empty exchange issue - Fix capture_prices reading "type" field instead of wrong "order" field - Add proper pagination to all GE queries via _get_paginated - Separate My Orders (active own orders) from Trade History (transaction log) - Add GEHistoryEntry type matching GeOrderHistorySchema (order_id, seller, buyer, sold_at) - Add /api/exchange/my-orders and /api/exchange/sell-history endpoints - Exchange page now has 4 tabs: Market, My Orders, Trade History, Price History
226 lines
8.2 KiB
Python
226 lines
8.2 KiB
Python
"""Tests for the EquipmentOptimizer decision maker."""
|
|
|
|
import pytest
|
|
|
|
from app.engine.decision.equipment_optimizer import (
|
|
EquipmentOptimizer,
|
|
EquipmentSuggestion,
|
|
)
|
|
from app.schemas.game import EffectSchema, InventorySlot, ItemSchema
|
|
|
|
|
|
class TestScoreItem:
|
|
"""Tests for the _score_item static method."""
|
|
|
|
def test_score_none_item(self):
|
|
assert EquipmentOptimizer._score_item(None) == 0.0
|
|
|
|
def test_score_item_with_attack_effects(self, make_item):
|
|
item = make_item(
|
|
code="fire_sword",
|
|
level=10,
|
|
effects=[
|
|
EffectSchema(name="attack_fire", value=20),
|
|
EffectSchema(name="attack_earth", value=10),
|
|
],
|
|
)
|
|
score = EquipmentOptimizer._score_item(item)
|
|
# 20 + 10 + (10 * 0.1 level bonus) = 31.0
|
|
assert score == pytest.approx(31.0)
|
|
|
|
def test_score_item_with_defense_effects(self, make_item):
|
|
item = make_item(
|
|
code="iron_shield",
|
|
level=5,
|
|
type="shield",
|
|
effects=[
|
|
EffectSchema(name="res_fire", value=15),
|
|
EffectSchema(name="res_water", value=10),
|
|
],
|
|
)
|
|
score = EquipmentOptimizer._score_item(item)
|
|
# 15 + 10 + (5 * 0.1) = 25.5
|
|
assert score == pytest.approx(25.5)
|
|
|
|
def test_score_item_with_hp_weighted_less(self, make_item):
|
|
item = make_item(
|
|
code="hp_ring",
|
|
level=1,
|
|
type="ring",
|
|
effects=[EffectSchema(name="hp", value=100)],
|
|
)
|
|
score = EquipmentOptimizer._score_item(item)
|
|
# 100 * 0.5 + (1 * 0.1) = 50.1
|
|
assert score == pytest.approx(50.1)
|
|
|
|
def test_score_item_with_damage_weighted_more(self, make_item):
|
|
item = make_item(
|
|
code="dmg_amulet",
|
|
level=1,
|
|
type="amulet",
|
|
effects=[EffectSchema(name="dmg_fire", value=10)],
|
|
)
|
|
score = EquipmentOptimizer._score_item(item)
|
|
# 10 * 1.5 + (1 * 0.1) = 15.1
|
|
assert score == pytest.approx(15.1)
|
|
|
|
def test_score_item_level_bonus_as_tiebreaker(self, make_item):
|
|
low = make_item(code="sword_5", level=5, effects=[EffectSchema(name="attack_fire", value=10)])
|
|
high = make_item(code="sword_10", level=10, effects=[EffectSchema(name="attack_fire", value=10)])
|
|
|
|
assert EquipmentOptimizer._score_item(high) > EquipmentOptimizer._score_item(low)
|
|
|
|
def test_score_item_with_no_effects(self, make_item):
|
|
item = make_item(code="plain_sword", level=5, effects=[])
|
|
score = EquipmentOptimizer._score_item(item)
|
|
# Only level bonus: 5 * 0.1 = 0.5
|
|
assert score == pytest.approx(0.5)
|
|
|
|
def test_score_item_with_unknown_effects(self, make_item):
|
|
item = make_item(
|
|
code="weird_item",
|
|
level=1,
|
|
effects=[EffectSchema(name="unknown_effect", value=100)],
|
|
)
|
|
score = EquipmentOptimizer._score_item(item)
|
|
# Unknown effect not counted: only level bonus 0.1
|
|
assert score == pytest.approx(0.1)
|
|
|
|
|
|
class TestSuggestEquipment:
|
|
"""Tests for the suggest_equipment method."""
|
|
|
|
def test_empty_available_items(self, make_character):
|
|
optimizer = EquipmentOptimizer()
|
|
char = make_character(level=10)
|
|
analysis = optimizer.suggest_equipment(char, [])
|
|
|
|
assert analysis.suggestions == []
|
|
assert analysis.total_current_score == 0.0
|
|
assert analysis.total_best_score == 0.0
|
|
|
|
def test_suggest_better_weapon(self, make_character, make_item):
|
|
optimizer = EquipmentOptimizer()
|
|
|
|
current_weapon = make_item(
|
|
code="rusty_sword",
|
|
level=1,
|
|
type="weapon",
|
|
effects=[EffectSchema(name="attack_fire", value=5)],
|
|
)
|
|
better_weapon = make_item(
|
|
code="iron_sword",
|
|
level=5,
|
|
type="weapon",
|
|
effects=[EffectSchema(name="attack_fire", value=20)],
|
|
)
|
|
char = make_character(level=10, weapon_slot="rusty_sword")
|
|
analysis = optimizer.suggest_equipment(char, [current_weapon, better_weapon])
|
|
|
|
weapon_suggestions = [s for s in analysis.suggestions if s.slot == "weapon_slot"]
|
|
assert len(weapon_suggestions) == 1
|
|
assert weapon_suggestions[0].suggested_item_code == "iron_sword"
|
|
assert weapon_suggestions[0].improvement > 0
|
|
|
|
def test_no_suggestion_when_best_is_equipped(self, make_character, make_item):
|
|
optimizer = EquipmentOptimizer()
|
|
|
|
best_weapon = make_item(
|
|
code="best_sword",
|
|
level=5,
|
|
type="weapon",
|
|
effects=[EffectSchema(name="attack_fire", value=50)],
|
|
)
|
|
char = make_character(level=10, weapon_slot="best_sword")
|
|
analysis = optimizer.suggest_equipment(char, [best_weapon])
|
|
|
|
weapon_suggestions = [s for s in analysis.suggestions if s.slot == "weapon_slot"]
|
|
assert len(weapon_suggestions) == 0
|
|
|
|
def test_item_too_high_level_not_suggested(self, make_character, make_item):
|
|
optimizer = EquipmentOptimizer()
|
|
|
|
high_level_weapon = make_item(
|
|
code="dragon_sword",
|
|
level=50,
|
|
type="weapon",
|
|
effects=[EffectSchema(name="attack_fire", value=100)],
|
|
)
|
|
char = make_character(level=10, weapon_slot="")
|
|
analysis = optimizer.suggest_equipment(char, [high_level_weapon])
|
|
|
|
# Too high level, should not be suggested
|
|
weapon_suggestions = [s for s in analysis.suggestions if s.slot == "weapon_slot"]
|
|
assert len(weapon_suggestions) == 0
|
|
|
|
def test_suggestions_sorted_by_improvement(self, make_character, make_item):
|
|
optimizer = EquipmentOptimizer()
|
|
|
|
weapon = make_item(
|
|
code="great_sword",
|
|
level=5,
|
|
type="weapon",
|
|
effects=[EffectSchema(name="attack_fire", value=30)],
|
|
)
|
|
shield = make_item(
|
|
code="great_shield",
|
|
level=5,
|
|
type="shield",
|
|
effects=[EffectSchema(name="res_fire", value=50)],
|
|
)
|
|
char = make_character(level=10, weapon_slot="", shield_slot="")
|
|
analysis = optimizer.suggest_equipment(char, [weapon, shield])
|
|
|
|
# Both should be suggested; shield has higher improvement
|
|
assert len(analysis.suggestions) >= 2
|
|
# Sorted descending by improvement
|
|
for i in range(len(analysis.suggestions) - 1):
|
|
assert analysis.suggestions[i].improvement >= analysis.suggestions[i + 1].improvement
|
|
|
|
def test_multiple_slot_types_for_rings(self, make_character, make_item):
|
|
optimizer = EquipmentOptimizer()
|
|
|
|
ring = make_item(
|
|
code="power_ring",
|
|
level=5,
|
|
type="ring",
|
|
effects=[EffectSchema(name="attack_fire", value=10)],
|
|
)
|
|
char = make_character(level=10, ring1_slot="", ring2_slot="")
|
|
analysis = optimizer.suggest_equipment(char, [ring])
|
|
|
|
ring_suggestions = [
|
|
s for s in analysis.suggestions if s.slot in ("ring1_slot", "ring2_slot")
|
|
]
|
|
# Both ring slots should get the suggestion
|
|
assert len(ring_suggestions) == 2
|
|
|
|
def test_total_scores_computed(self, make_character, make_item):
|
|
optimizer = EquipmentOptimizer()
|
|
|
|
weapon = make_item(
|
|
code="iron_sword",
|
|
level=5,
|
|
type="weapon",
|
|
effects=[EffectSchema(name="attack_fire", value=10)],
|
|
)
|
|
char = make_character(level=10, weapon_slot="")
|
|
analysis = optimizer.suggest_equipment(char, [weapon])
|
|
|
|
assert analysis.total_best_score >= analysis.total_current_score
|
|
|
|
def test_empty_slot_shows_empty_in_suggestion(self, make_character, make_item):
|
|
optimizer = EquipmentOptimizer()
|
|
|
|
weapon = make_item(
|
|
code="iron_sword",
|
|
level=5,
|
|
type="weapon",
|
|
effects=[EffectSchema(name="attack_fire", value=10)],
|
|
)
|
|
char = make_character(level=10, weapon_slot="")
|
|
analysis = optimizer.suggest_equipment(char, [weapon])
|
|
|
|
weapon_suggestions = [s for s in analysis.suggestions if s.slot == "weapon_slot"]
|
|
assert len(weapon_suggestions) == 1
|
|
assert weapon_suggestions[0].current_item_code == "(empty)"
|