Files
Timmy-time-dashboard/tests/unit/test_bannerlord/test_models.py
Alexander Whitestone 0cd686cefb
Some checks failed
Tests / lint (pull_request) Failing after 23s
Tests / test (pull_request) Has been skipped
WIP: Claude Code progress on #1097
Automated salvage commit — agent session ended (exit 124).
Work in progress, may need continuation.
2026-03-23 14:24:56 -04:00

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