Files
Timmy-time-dashboard/src/bannerlord/agents/economy_vassal.py
Alexander Whitestone 7385c46a78 WIP: Claude Code progress on #1095
Automated salvage commit — agent session ended (exit 124).
Work in progress, may need continuation.
2026-03-23 14:25:01 -04:00

152 lines
4.7 KiB
Python

"""Bannerlord M3 — Economy Vassal agent.
Handles settlement management, tax collection, construction, and food supply.
Responds to FORTIFY and CONSOLIDATE subgoals.
Reward function:
R_econ = w1 * DailyDenarsIncome
+ w2 * FoodStockBuffer
+ w3 * LoyaltyAverage
- w4 * ConstructionQueueLength
+ w5 * SubgoalBonus
"""
from __future__ import annotations
import logging
from bannerlord.types import (
GameState,
KingSubgoal,
SubgoalToken,
TaskMessage,
VassalReward,
)
logger = logging.getLogger(__name__)
_W1_INCOME = 0.35
_W2_FOOD = 0.25
_W3_LOYALTY = 0.20
_W4_CONSTRUCTION = 0.15
_W5_SUBGOAL = 0.05
_SUBGOAL_TRIGGERS = {SubgoalToken.FORTIFY, SubgoalToken.CONSOLIDATE}
_LOW_FOOD_THRESHOLD = 3 # days of food remaining
_INCOME_TARGET = 200 # daily net income target (denars)
class EconomyVassal:
"""Mid-tier agent responsible for settlement economy."""
AGENT_ID = "economy_vassal"
def is_relevant(self, subgoal: KingSubgoal) -> bool:
return subgoal.token in _SUBGOAL_TRIGGERS
def plan(self, state: GameState, subgoal: KingSubgoal) -> list[TaskMessage]:
"""Return TaskMessages for the current economic subgoal."""
tasks: list[TaskMessage] = []
# Always maintain food supply
if state.party.food_days < _LOW_FOOD_THRESHOLD:
tasks.append(
TaskMessage(
from_agent=self.AGENT_ID,
to_agent="logistics_companion",
primitive="buy_supplies",
args={"qty": 10},
priority=2.0,
)
)
if subgoal.token == SubgoalToken.FORTIFY:
tasks.extend(self._plan_fortify(state, subgoal))
elif subgoal.token == SubgoalToken.CONSOLIDATE:
tasks.extend(self._plan_consolidate(state))
return tasks
def _plan_fortify(
self, state: GameState, subgoal: KingSubgoal
) -> list[TaskMessage]:
"""Queue construction projects in owned settlements."""
tasks: list[TaskMessage] = []
target = subgoal.target or (state.kingdom.fiefs[0] if state.kingdom.fiefs else None)
if not target:
return tasks
tasks.append(
TaskMessage(
from_agent=self.AGENT_ID,
to_agent="gabs",
primitive="build_project",
args={"settlement": target, "project": "granary"},
priority=1.2,
)
)
tasks.append(
TaskMessage(
from_agent=self.AGENT_ID,
to_agent="gabs",
primitive="set_tax_policy",
args={"settlement": target, "policy": "normal"},
priority=1.0,
)
)
return tasks
def _plan_consolidate(self, state: GameState) -> list[TaskMessage]:
"""Stabilise: optimise tax and food across all fiefs."""
tasks: list[TaskMessage] = []
net_income = state.kingdom.daily_income - state.kingdom.daily_expenses
for fief in state.kingdom.fiefs:
policy = "normal" if net_income >= _INCOME_TARGET else "low"
tasks.append(
TaskMessage(
from_agent=self.AGENT_ID,
to_agent="gabs",
primitive="set_tax_policy",
args={"settlement": fief, "policy": policy},
priority=0.8,
)
)
return tasks
def compute_reward(
self,
prev_state: GameState,
curr_state: GameState,
active_subgoal: KingSubgoal,
) -> VassalReward:
"""Compute Economy Vassal reward."""
income_delta = (
curr_state.kingdom.daily_income - prev_state.kingdom.daily_income
)
food_buffer = curr_state.party.food_days
loyalty_avg = 70.0 # placeholder — real value from GABS raw data
queue_len = 0 # placeholder
subgoal_bonus = 1.0 if active_subgoal.token in _SUBGOAL_TRIGGERS else 0.0
total = (
_W1_INCOME * income_delta
+ _W2_FOOD * food_buffer
+ _W3_LOYALTY * loyalty_avg / 100
- _W4_CONSTRUCTION * queue_len
+ _W5_SUBGOAL * subgoal_bonus * 10
)
return VassalReward(
agent_id=self.AGENT_ID,
component_scores={
"income_delta": income_delta,
"food_buffer": food_buffer,
"loyalty_avg": loyalty_avg,
"queue_len": -queue_len,
"subgoal_bonus": subgoal_bonus,
},
subgoal_bonus=subgoal_bonus,
total=total,
)