Automated salvage commit — agent session ended (exit 124). Work in progress, may need continuation.
191 lines
6.3 KiB
Python
191 lines
6.3 KiB
Python
"""Unit tests for bannerlord.models — data contracts and reward functions."""
|
|
|
|
import pytest
|
|
|
|
from bannerlord.models import (
|
|
SUBGOAL_TOKENS,
|
|
DiplomacyReward,
|
|
EconomyReward,
|
|
KingSubgoal,
|
|
ResultMessage,
|
|
StateUpdateMessage,
|
|
SubgoalMessage,
|
|
TaskMessage,
|
|
VictoryCondition,
|
|
WarReward,
|
|
)
|
|
|
|
|
|
# ── KingSubgoal ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestKingSubgoal:
|
|
def test_valid_token(self):
|
|
s = KingSubgoal(token="EXPAND_TERRITORY", target="Epicrotea")
|
|
assert s.token == "EXPAND_TERRITORY"
|
|
assert s.target == "Epicrotea"
|
|
assert s.priority == 1.0
|
|
|
|
def test_all_tokens_valid(self):
|
|
for token in SUBGOAL_TOKENS:
|
|
KingSubgoal(token=token)
|
|
|
|
def test_invalid_token_raises(self):
|
|
with pytest.raises(ValueError, match="Unknown subgoal token"):
|
|
KingSubgoal(token="NUKE_CALRADIA")
|
|
|
|
def test_priority_clamp(self):
|
|
with pytest.raises(Exception):
|
|
KingSubgoal(token="TRADE", priority=3.0)
|
|
|
|
def test_optional_fields_default_none(self):
|
|
s = KingSubgoal(token="HEAL")
|
|
assert s.target is None
|
|
assert s.quantity is None
|
|
assert s.deadline_days is None
|
|
assert s.context is None
|
|
|
|
|
|
# ── Messages ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestSubgoalMessage:
|
|
def test_defaults(self):
|
|
msg = SubgoalMessage(
|
|
to_agent="war_vassal",
|
|
subgoal=KingSubgoal(token="RAID_ECONOMY"),
|
|
)
|
|
assert msg.msg_type == "subgoal"
|
|
assert msg.from_agent == "king"
|
|
assert msg.to_agent == "war_vassal"
|
|
assert msg.issued_at is not None
|
|
|
|
def test_subgoal_roundtrip(self):
|
|
subgoal = KingSubgoal(token="RECRUIT", quantity=30, priority=1.5)
|
|
msg = SubgoalMessage(to_agent="war_vassal", subgoal=subgoal)
|
|
assert msg.subgoal.quantity == 30
|
|
assert msg.subgoal.priority == 1.5
|
|
|
|
|
|
class TestTaskMessage:
|
|
def test_construction(self):
|
|
t = TaskMessage(
|
|
from_agent="war_vassal",
|
|
to_agent="logistics_companion",
|
|
primitive="recruit_troop",
|
|
args={"troop_type": "cavalry", "quantity": 5},
|
|
priority=1.2,
|
|
)
|
|
assert t.msg_type == "task"
|
|
assert t.primitive == "recruit_troop"
|
|
assert t.args["quantity"] == 5
|
|
|
|
|
|
class TestResultMessage:
|
|
def test_success(self):
|
|
r = ResultMessage(
|
|
from_agent="logistics_companion",
|
|
to_agent="war_vassal",
|
|
success=True,
|
|
outcome={"recruited": 10},
|
|
reward_delta=0.15,
|
|
)
|
|
assert r.success is True
|
|
assert r.reward_delta == 0.15
|
|
|
|
def test_failure(self):
|
|
r = ResultMessage(
|
|
from_agent="scout_companion",
|
|
to_agent="diplomacy_vassal",
|
|
success=False,
|
|
outcome={"error": "GABS unavailable"},
|
|
)
|
|
assert r.success is False
|
|
assert r.reward_delta == 0.0
|
|
|
|
|
|
class TestStateUpdateMessage:
|
|
def test_construction(self):
|
|
msg = StateUpdateMessage(
|
|
game_state={"campaign_day": 42, "player_title": "Lord"},
|
|
tick=42,
|
|
)
|
|
assert msg.msg_type == "state"
|
|
assert msg.tick == 42
|
|
assert msg.game_state["campaign_day"] == 42
|
|
|
|
|
|
# ── Reward functions ──────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestWarReward:
|
|
def test_positive_expansion(self):
|
|
r = WarReward(territory_delta=2.0, army_strength_ratio=1.2, subgoal_bonus=0.1)
|
|
assert r.total > 0
|
|
|
|
def test_casualty_cost_penalizes(self):
|
|
no_cost = WarReward(territory_delta=1.0, army_strength_ratio=1.0)
|
|
with_cost = WarReward(territory_delta=1.0, army_strength_ratio=1.0, casualty_cost=5.0)
|
|
assert with_cost.total < no_cost.total
|
|
|
|
def test_zero_state(self):
|
|
r = WarReward()
|
|
# army_strength_ratio default 1.0, rest 0 → 0.25 * 1.0 = 0.25
|
|
assert abs(r.total - 0.25) < 1e-9
|
|
|
|
|
|
class TestEconomyReward:
|
|
def test_income_positive(self):
|
|
r = EconomyReward(daily_denars_income=100.0, food_stock_buffer=7.0, loyalty_average=80.0)
|
|
assert r.total > 0
|
|
|
|
def test_construction_queue_penalizes(self):
|
|
no_queue = EconomyReward(daily_denars_income=50.0)
|
|
long_queue = EconomyReward(daily_denars_income=50.0, construction_queue_length=10)
|
|
assert long_queue.total < no_queue.total
|
|
|
|
def test_loyalty_contributes(self):
|
|
low_loyalty = EconomyReward(loyalty_average=10.0)
|
|
high_loyalty = EconomyReward(loyalty_average=90.0)
|
|
assert high_loyalty.total > low_loyalty.total
|
|
|
|
|
|
class TestDiplomacyReward:
|
|
def test_allies_positive(self):
|
|
r = DiplomacyReward(allies_count=3)
|
|
assert r.total > 0
|
|
|
|
def test_active_wars_penalizes(self):
|
|
peace = DiplomacyReward(allies_count=2)
|
|
war = DiplomacyReward(allies_count=2, active_wars_front=4)
|
|
assert war.total < peace.total
|
|
|
|
|
|
# ── Victory condition ─────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestVictoryCondition:
|
|
def test_not_achieved_without_title(self):
|
|
v = VictoryCondition(holds_king_title=False, territory_control_pct=70.0)
|
|
assert not v.achieved
|
|
|
|
def test_not_achieved_without_majority(self):
|
|
v = VictoryCondition(holds_king_title=True, territory_control_pct=40.0)
|
|
assert not v.achieved
|
|
|
|
def test_achieved_when_king_with_majority(self):
|
|
v = VictoryCondition(holds_king_title=True, territory_control_pct=55.0)
|
|
assert v.achieved
|
|
|
|
def test_exact_threshold(self):
|
|
v = VictoryCondition(holds_king_title=True, territory_control_pct=51.0)
|
|
assert v.achieved
|
|
|
|
def test_custom_threshold(self):
|
|
v = VictoryCondition(
|
|
holds_king_title=True,
|
|
territory_control_pct=70.0,
|
|
majority_threshold=75.0,
|
|
)
|
|
assert not v.achieved
|