"""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