Automated salvage commit — agent session ended (exit 124). Work in progress, may need continuation.
152 lines
4.7 KiB
Python
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,
|
|
)
|