1272 lines
51 KiB
Python

"""AI decision system for agents in the Village Simulation.
Major rework to create diverse, personality-driven economy:
- Each agent has unique personality traits affecting all decisions
- Emergent professions: Hunters, Gatherers, Traders, Generalists, Oil Workers, Priests
- Class inequality through varied strategies and skills
- Traders focus on arbitrage (buy low, sell high)
- Personality affects: risk tolerance, hoarding, market participation
NEW: Religion and diplomacy integration:
- Agents with high faith perform religious actions (pray, preach)
- Agents consider faction relations in trading decisions
- Diplomatic agents negotiate, declare war, or make peace
- Oil workers focus on drilling and refining
"""
import random
from dataclasses import dataclass, field
from typing import Optional, TYPE_CHECKING
from backend.domain.agent import Agent
from backend.domain.action import ActionType, ACTION_CONFIG
from backend.domain.resources import ResourceType
from backend.domain.personality import get_trade_price_modifier
from backend.domain.religion import get_religion_action_bonus
from backend.domain.diplomacy import (
FactionType, get_faction_relations, DiplomaticStatus
)
if TYPE_CHECKING:
from backend.core.market import OrderBook
@dataclass
class TradeItem:
"""A single item to buy/sell in a trade."""
order_id: str
resource_type: ResourceType
quantity: int
price_per_unit: int
@dataclass
class AIDecision:
"""A decision made by the AI for an agent."""
action: ActionType
target_resource: Optional[ResourceType] = None
order_id: Optional[str] = None
quantity: int = 1
price: int = 0
reason: str = ""
# For multi-item trades
trade_items: list[TradeItem] = field(default_factory=list)
# For price adjustments
adjust_order_id: Optional[str] = None
new_price: Optional[int] = None
# NEW: For diplomatic/religious actions
target_agent_id: Optional[str] = None
target_faction: Optional[FactionType] = None
def to_dict(self) -> dict:
return {
"action": self.action.value,
"target_resource": self.target_resource.value if self.target_resource else None,
"order_id": self.order_id,
"quantity": self.quantity,
"price": self.price,
"reason": self.reason,
"trade_items": [
{
"order_id": t.order_id,
"resource_type": t.resource_type.value,
"quantity": t.quantity,
"price_per_unit": t.price_per_unit,
}
for t in self.trade_items
],
"adjust_order_id": self.adjust_order_id,
"new_price": self.new_price,
"target_agent_id": self.target_agent_id,
"target_faction": self.target_faction.value if self.target_faction else None,
}
# Resource to action for gathering
RESOURCE_ACTIONS: dict[ResourceType, ActionType] = {
ResourceType.MEAT: ActionType.HUNT,
ResourceType.BERRIES: ActionType.GATHER,
ResourceType.WATER: ActionType.GET_WATER,
ResourceType.WOOD: ActionType.CHOP_WOOD,
ResourceType.HIDE: ActionType.HUNT,
ResourceType.CLOTHES: ActionType.WEAVE,
ResourceType.OIL: ActionType.DRILL_OIL,
ResourceType.FUEL: ActionType.REFINE,
}
def get_energy_cost(resource_type: ResourceType) -> int:
"""Get the energy cost to produce one unit of a resource."""
action = RESOURCE_ACTIONS.get(resource_type)
if not action:
return 10
config = ACTION_CONFIG.get(action)
if not config:
return 10
energy_cost = abs(config.energy_cost)
avg_output = max(1, (config.min_output + config.max_output) / 2) if config.output_resource else 1
return int(energy_cost / avg_output)
def _get_ai_config():
"""Get AI-relevant configuration values."""
from backend.config import get_config
config = get_config()
return config.agent_stats
def _get_economy_config():
"""Get economy/market configuration values."""
from backend.config import get_config
config = get_config()
return getattr(config, 'economy', None)
def _get_religion_config():
"""Get religion configuration values."""
from backend.config import get_config
config = get_config()
return getattr(config, 'religion', None)
def _get_diplomacy_config():
"""Get diplomacy configuration values."""
from backend.config import get_config
config = get_config()
return getattr(config, 'diplomacy', None)
class AgentAI:
"""AI decision maker with personality-driven economy behavior.
Now includes religion and diplomacy considerations.
"""
# Thresholds for stat management
LOW_THRESHOLD = 0.55 # Increased from 0.45 to be more proactive about survival
COMFORT_THRESHOLD = 0.70 # Increased from 0.60
REST_ENERGY_THRESHOLD = 18
WORK_ENERGY_MINIMUM = 20
# Resource stockpile targets
BASE_WATER_STOCK = 2
BASE_FOOD_STOCK = 3
BASE_WOOD_STOCK = 2
# Heat thresholds
HEAT_PROACTIVE_THRESHOLD = 0.50
# Faith thresholds
LOW_FAITH_THRESHOLD = 0.30
HIGH_FAITH_THRESHOLD = 0.70
# Economy settings
ENERGY_TO_MONEY_RATIO = 1.5
MAX_PRICE_MARKUP = 2.0
MIN_PRICE_DISCOUNT = 0.5
def __init__(self, agent: Agent, market: "OrderBook", step_in_day: int = 1,
day_steps: int = 10, current_turn: int = 0, world = None):
self.agent = agent
self.market = market
self.step_in_day = step_in_day
self.day_steps = day_steps
self.current_turn = current_turn
self.world = world # NEW: Reference to world for nearby agents
self.p = agent.personality
self.skills = agent.skills
self.religion = agent.religion
self.diplomacy = agent.diplomacy
# Load thresholds from config
config = _get_ai_config()
self.CRITICAL_THRESHOLD = config.critical_threshold
self.LOW_ENERGY_THRESHOLD = config.low_energy_threshold
# Personality-adjusted values
self.WEALTH_DESIRE = self.p.wealth_desire
economy = _get_economy_config()
base_threshold = getattr(economy, 'buy_efficiency_threshold', 0.7) if economy else 0.7
self.BUY_EFFICIENCY_THRESHOLD = base_threshold / self.p.price_sensitivity
base_target = getattr(economy, 'min_wealth_target', 50) if economy else 50
self.MIN_WEALTH_TARGET = int(base_target * (0.5 + self.p.wealth_desire))
hoarding_mult = 0.5 + self.p.hoarding_rate
self.MIN_WATER_STOCK = max(1, int(self.BASE_WATER_STOCK * hoarding_mult))
self.MIN_FOOD_STOCK = max(2, int(self.BASE_FOOD_STOCK * hoarding_mult))
self.MIN_WOOD_STOCK = max(1, int(self.BASE_WOOD_STOCK * hoarding_mult))
self.is_trader = self.p.trade_preference > 1.3 and self.p.market_affinity > 0.5
# NEW: Check for special roles
self.is_religious = self.religion.is_religious and self.agent.stats.faith > 40
self.is_zealot = self.religion.is_zealot
self.is_diplomat = self.diplomacy.diplomacy_skill > 0.6
@property
def is_evening(self) -> bool:
return self.step_in_day >= self.day_steps - 1
@property
def is_late_day(self) -> bool:
return self.step_in_day >= self.day_steps // 2
def _get_resource_fair_value(self, resource_type: ResourceType) -> int:
"""Calculate the 'fair value' of a resource."""
energy_cost = get_energy_cost(resource_type)
base_value = max(1, int(energy_cost * self.ENERGY_TO_MONEY_RATIO))
# Oil and fuel have special pricing
economy = _get_economy_config()
if economy:
if resource_type == ResourceType.OIL:
return economy.oil_base_price
elif resource_type == ResourceType.FUEL:
return economy.fuel_base_price
return base_value
def _is_good_buy(self, resource_type: ResourceType, price: int) -> bool:
fair_value = self._get_resource_fair_value(resource_type)
return price <= fair_value * self.BUY_EFFICIENCY_THRESHOLD
def _is_wealthy(self) -> bool:
return self.agent.money >= self.MIN_WEALTH_TARGET
def decide(self) -> AIDecision:
"""Make a decision based on survival, personality, religion, and diplomacy."""
# Priority 1: Critical survival needs
decision = self._check_critical_needs()
if decision:
return decision
# Priority 2: Proactive survival
decision = self._check_proactive_needs()
if decision:
return decision
# Priority 3: Basic stockpiling - ensure we have food and water before other activities
decision = self._check_basic_stockpile()
if decision:
return decision
# Priority 4: Religious actions (for religious agents) - only if well-fed
if self.is_religious:
decision = self._check_religious_actions()
if decision:
return decision
# Priority 5: Diplomatic actions (all non-neutral faction members)
# War can be declared by any aggressive agent, diplomacy by skilled ones
if self.diplomacy.faction != FactionType.NEUTRAL:
decision = self._check_diplomatic_actions()
if decision:
return decision
# Priority 6: Trader behavior
if self.is_trader:
decision = self._do_trader_behavior()
if decision:
return decision
# Priority 7: Oil industry (if have oil skills or near oil field)
decision = self._check_oil_industry()
if decision:
return decision
# Priority 8: Price adjustment
decision = self._check_price_adjustments()
if decision:
return decision
# Priority 9: Smart shopping
if random.random() < self.p.market_affinity:
decision = self._check_market_opportunities()
if decision:
return decision
# Priority 10: Craft clothes
decision = self._check_clothes_crafting()
if decision:
return decision
# Priority 11: Energy management
decision = self._check_energy()
if decision:
return decision
# Priority 12: Economic activities
if random.random() > self.p.hoarding_rate * 0.5:
decision = self._check_economic()
if decision:
return decision
# Priority 13: Routine survival work
return self._do_survival_work()
def _check_basic_stockpile(self) -> Optional[AIDecision]:
"""Ensure basic food and water supplies before other activities."""
water_count = self.agent.get_resource_count(ResourceType.WATER)
meat_count = self.agent.get_resource_count(ResourceType.MEAT)
berry_count = self.agent.get_resource_count(ResourceType.BERRIES)
food_count = meat_count + berry_count
# Minimum stockpile requirements (more aggressive than hoarding-adjusted values)
MIN_WATER = 2
MIN_FOOD = 3
# Check if we're dangerously low on resources
needs_water = water_count < MIN_WATER
needs_food = food_count < MIN_FOOD
if not needs_water and not needs_food:
return None
# Prioritize water since thirst is more urgent
if needs_water:
water_config = ACTION_CONFIG[ActionType.GET_WATER]
if self.agent.stats.can_work(abs(water_config.energy_cost)):
return AIDecision(
action=ActionType.GET_WATER,
target_resource=ResourceType.WATER,
reason=f"Stockpiling water ({water_count} < {MIN_WATER})",
)
# Then food
if needs_food:
# Try to gather berries first (cheaper energy cost)
gather_config = ACTION_CONFIG[ActionType.GATHER]
if self.agent.stats.can_work(abs(gather_config.energy_cost)):
return AIDecision(
action=ActionType.GATHER,
target_resource=ResourceType.BERRIES,
reason=f"Stockpiling food ({food_count} < {MIN_FOOD})",
)
# If can afford hunting and have energy
hunt_config = ACTION_CONFIG[ActionType.HUNT]
if self.agent.stats.can_work(abs(hunt_config.energy_cost)):
return AIDecision(
action=ActionType.HUNT,
target_resource=ResourceType.MEAT,
reason=f"Stockpiling food by hunting ({food_count} < {MIN_FOOD})",
)
return None
def _check_religious_actions(self) -> Optional[AIDecision]:
"""Check if agent should perform religious actions."""
stats = self.agent.stats
# Low faith - should pray
if stats.faith < stats.MAX_FAITH * self.LOW_FAITH_THRESHOLD:
pray_config = ACTION_CONFIG[ActionType.PRAY]
if stats.can_work(abs(pray_config.energy_cost)):
return AIDecision(
action=ActionType.PRAY,
reason=f"Low faith ({stats.faith}): praying at temple",
)
# Zealots want to preach and convert (but not too often to preserve energy)
if self.is_zealot:
if random.random() < 0.15: # Reduced from 30% to 15% to conserve energy
preach_config = ACTION_CONFIG[ActionType.PREACH]
if stats.can_work(abs(preach_config.energy_cost)):
return AIDecision(
action=ActionType.PREACH,
reason=f"Zealot spreading the word of {self.religion.religion.value}",
)
# Religious agents pray occasionally to maintain faith
if stats.faith < stats.MAX_FAITH * self.HIGH_FAITH_THRESHOLD:
if random.random() < 0.15: # 15% chance
pray_config = ACTION_CONFIG[ActionType.PRAY]
if stats.can_work(abs(pray_config.energy_cost)):
return AIDecision(
action=ActionType.PRAY,
reason="Maintaining faith through prayer",
)
return None
def _check_diplomatic_actions(self) -> Optional[AIDecision]:
"""Check if agent should perform diplomatic actions."""
faction_relations = get_faction_relations()
my_faction = self.diplomacy.faction
if my_faction == FactionType.NEUTRAL:
return None
# Check each other faction
for other_faction in FactionType:
if other_faction == my_faction or other_faction == FactionType.NEUTRAL:
continue
status = faction_relations.get_status(my_faction, other_faction)
# At war and exhausted - try to make peace
if status == DiplomaticStatus.WAR:
exhaustion = faction_relations.war_exhaustion.get(my_faction, 0)
if exhaustion > 30 and random.random() < 0.2:
peace_config = ACTION_CONFIG[ActionType.MAKE_PEACE]
if self.agent.stats.can_work(abs(peace_config.energy_cost)):
return AIDecision(
action=ActionType.MAKE_PEACE,
target_faction=other_faction,
reason=f"War exhaustion ({exhaustion}): seeking peace with {other_faction.value}",
)
# Very hostile and aggressive - might declare war (but less frequently)
elif status == DiplomaticStatus.HOSTILE:
# War probability scales with aggression (reduced to promote stability)
# Base 2% chance + up to 8% from aggression
war_prob = 0.08 + (self.diplomacy.aggression * 0.08)
if self.diplomacy.aggression > 0.25 and random.random() < war_prob:
war_cfg = ACTION_CONFIG[ActionType.DECLARE_WAR]
if self.agent.stats.can_work(abs(war_cfg.energy_cost)):
return AIDecision(
action=ActionType.DECLARE_WAR,
target_faction=other_faction,
reason=f"Tensions: declaring war on {other_faction.value}",
)
# Cold relations - try to improve
elif status in (DiplomaticStatus.COLD, DiplomaticStatus.NEUTRAL):
if self.diplomacy.diplomacy_skill > 0.6 and random.random() < 0.1:
neg_cfg = ACTION_CONFIG[ActionType.NEGOTIATE]
if self.agent.stats.can_work(abs(neg_cfg.energy_cost)):
return AIDecision(
action=ActionType.NEGOTIATE,
target_faction=other_faction,
reason=f"Improving relations: {other_faction.value}",
)
return None
def _check_oil_industry(self) -> Optional[AIDecision]:
"""Check if agent should work in oil industry."""
stats = self.agent.stats
# Check if we have oil to refine
if self.agent.has_resource(ResourceType.OIL, 2):
refine_config = ACTION_CONFIG[ActionType.REFINE]
if stats.can_work(abs(refine_config.energy_cost)):
return AIDecision(
action=ActionType.REFINE,
target_resource=ResourceType.FUEL,
reason="Refining oil into fuel",
)
# Check if we should burn fuel for heat/energy
if self.agent.has_resource(ResourceType.FUEL):
if stats.heat < stats.MAX_HEAT * 0.5 or stats.energy < 20:
burn_config = ACTION_CONFIG[ActionType.BURN_FUEL]
if stats.can_work(abs(burn_config.energy_cost)):
return AIDecision(
action=ActionType.BURN_FUEL,
target_resource=ResourceType.FUEL,
reason="Burning fuel for energy and heat",
)
# Consider drilling for oil (specialized or random chance)
oil_count = self.agent.get_resource_count(ResourceType.OIL)
# Oil is valuable - check if worth drilling
should_drill = False
# Mountaineers have oil bonus
if self.diplomacy.faction == FactionType.MOUNTAINEER:
should_drill = oil_count < 3 and random.random() < 0.3
# Already an oil worker
elif self.agent.actions_performed.get("drill_oil", 0) > 5:
should_drill = oil_count < 5 and random.random() < 0.4
# Random chance for anyone
elif random.random() < 0.05:
should_drill = True
if should_drill and self.agent.inventory_space() >= 2:
drill_config = ACTION_CONFIG[ActionType.DRILL_OIL]
if stats.can_work(abs(drill_config.energy_cost)):
return AIDecision(
action=ActionType.DRILL_OIL,
target_resource=ResourceType.OIL,
reason="Drilling for oil",
)
return None
def _check_critical_needs(self) -> Optional[AIDecision]:
"""Check if any vital stat is critical."""
stats = self.agent.stats
if stats.thirst < stats.MAX_THIRST * self.CRITICAL_THRESHOLD:
return self._address_thirst(critical=True)
if stats.hunger < stats.MAX_HUNGER * self.CRITICAL_THRESHOLD:
return self._address_hunger(critical=True)
if stats.heat < stats.MAX_HEAT * self.CRITICAL_THRESHOLD:
return self._address_heat(critical=True)
return None
def _check_proactive_needs(self) -> Optional[AIDecision]:
"""Proactively address needs before critical."""
stats = self.agent.stats
if stats.thirst < stats.MAX_THIRST * self.LOW_THRESHOLD:
return self._address_thirst(critical=False)
if stats.hunger < stats.MAX_HUNGER * self.LOW_THRESHOLD:
return self._address_hunger(critical=False)
if stats.heat < stats.MAX_HEAT * self.HEAT_PROACTIVE_THRESHOLD:
decision = self._address_heat(critical=False)
if decision:
return decision
return None
def _do_trader_behavior(self) -> Optional[AIDecision]:
"""Trader-specific behavior: arbitrage and market operations."""
if self.agent.money < 20:
decision = self._try_to_sell(urgent=True)
if decision:
return decision
return None
# Look for arbitrage opportunities
best_deal = None
best_margin = 0
# Include oil and fuel in trading
trade_resources = [
ResourceType.MEAT, ResourceType.BERRIES, ResourceType.WATER,
ResourceType.WOOD, ResourceType.OIL, ResourceType.FUEL
]
for resource_type in trade_resources:
order = self.market.get_cheapest_order(resource_type)
if not order or order.seller_id == self.agent.id:
continue
fair_value = self._get_resource_fair_value(resource_type)
buy_price = order.price_per_unit
sell_modifier = get_trade_price_modifier(self.skills.trading, is_buying=False)
potential_sell_price = int(fair_value * sell_modifier * 1.1)
margin = potential_sell_price - buy_price
if margin > best_margin and self.agent.money >= buy_price:
best_margin = margin
best_deal = (resource_type, order, buy_price, potential_sell_price)
if best_deal and best_margin >= 2:
resource_type, order, buy_price, sell_price = best_deal
safe_price = max(1, buy_price)
quantity = min(3, self.agent.inventory_space(), order.quantity,
self.agent.money // safe_price)
if quantity > 0:
return AIDecision(
action=ActionType.TRADE,
target_resource=resource_type,
order_id=order.id,
quantity=quantity,
price=buy_price,
reason=f"Trader: buying {resource_type.value} @ {buy_price}c",
)
decision = self._try_trader_sell()
if decision:
return decision
decision = self._check_price_adjustments()
if decision:
return decision
return None
def _try_trader_sell(self) -> Optional[AIDecision]:
"""Trader sells inventory at markup."""
for resource in self.agent.inventory:
if resource.type == ResourceType.CLOTHES:
continue
if resource.quantity <= 1:
continue
fair_value = self._get_resource_fair_value(resource.type)
sell_modifier = get_trade_price_modifier(self.skills.trading, is_buying=False)
signal = self.market.get_market_signal(resource.type)
if signal == "sell":
price = int(fair_value * sell_modifier * 1.4)
else:
price = int(fair_value * sell_modifier * 1.15)
my_orders = [o for o in self.market.get_orders_by_seller(self.agent.id)
if o.resource_type == resource.type]
if my_orders:
continue
quantity = resource.quantity - 1
return AIDecision(
action=ActionType.TRADE,
target_resource=resource.type,
quantity=quantity,
price=price,
reason=f"Trader: selling {resource.type.value} @ {price}c",
)
return None
def _check_price_adjustments(self) -> Optional[AIDecision]:
"""Check if we should adjust prices on orders."""
my_orders = self.market.get_orders_by_seller(self.agent.id)
if not my_orders:
return None
for order in my_orders:
resource_type = order.resource_type
signal = self.market.get_market_signal(resource_type)
current_price = order.price_per_unit
fair_value = self._get_resource_fair_value(resource_type)
if signal == "sell" and order.can_raise_price(self.current_turn, min_turns=3):
new_price = min(
int(current_price * 1.25),
int(fair_value * self.MAX_PRICE_MARKUP)
)
if new_price > current_price:
return AIDecision(
action=ActionType.TRADE,
target_resource=resource_type,
adjust_order_id=order.id,
new_price=new_price,
reason=f"Scarcity: raising {resource_type.value} price to {new_price}c",
)
if order.turns_without_sale >= 5:
lowest_order = self.market.get_cheapest_order(resource_type)
if lowest_order and lowest_order.id != order.id:
new_price = max(
lowest_order.price_per_unit - 1,
int(fair_value * self.MIN_PRICE_DISCOUNT)
)
else:
new_price = max(
int(current_price * 0.85),
int(fair_value * self.MIN_PRICE_DISCOUNT)
)
if new_price < current_price:
return AIDecision(
action=ActionType.TRADE,
target_resource=resource_type,
adjust_order_id=order.id,
new_price=new_price,
reason=f"Stale: lowering {resource_type.value} to {new_price}c",
)
return None
def _check_market_opportunities(self) -> Optional[AIDecision]:
"""Look for good buying opportunities."""
if self.agent.money < 10:
return None
shopping_list = []
for resource_type in [ResourceType.WATER, ResourceType.BERRIES, ResourceType.MEAT,
ResourceType.WOOD, ResourceType.OIL, ResourceType.FUEL]:
order = self.market.get_cheapest_order(resource_type)
if not order or order.seller_id == self.agent.id:
continue
if order.price_per_unit <= 0:
continue
if self.agent.money < order.price_per_unit:
continue
fair_value = self._get_resource_fair_value(resource_type)
is_good_deal = self._is_good_buy(resource_type, order.price_per_unit)
current_stock = self.agent.get_resource_count(resource_type)
need_score = 0
if resource_type == ResourceType.WATER:
need_score = max(0, self.MIN_WATER_STOCK - current_stock) * 3
elif resource_type in [ResourceType.BERRIES, ResourceType.MEAT]:
food_stock = (self.agent.get_resource_count(ResourceType.BERRIES) +
self.agent.get_resource_count(ResourceType.MEAT))
need_score = max(0, self.MIN_FOOD_STOCK - food_stock) * 2
elif resource_type == ResourceType.WOOD:
need_score = max(0, self.MIN_WOOD_STOCK - current_stock) * 1
elif resource_type in [ResourceType.OIL, ResourceType.FUEL]:
# Oil and fuel are valuable trading commodities
need_score = 1 if is_good_deal else 0
if is_good_deal:
price = max(1, order.price_per_unit)
efficiency_score = fair_value / price
total_score = need_score + efficiency_score * 2
shopping_list.append((resource_type, order, total_score))
elif need_score > 0 and self._is_wealthy():
total_score = need_score * 0.5
shopping_list.append((resource_type, order, total_score))
if not shopping_list:
return None
shopping_list.sort(key=lambda x: x[2], reverse=True)
resource_type, order, score = shopping_list[0]
if score < 1:
return None
price = max(1, order.price_per_unit)
can_afford = self.agent.money // price
space = self.agent.inventory_space()
want_quantity = min(2, can_afford, space, order.quantity)
if want_quantity <= 0:
return None
return AIDecision(
action=ActionType.TRADE,
target_resource=resource_type,
order_id=order.id,
quantity=want_quantity,
price=order.price_per_unit,
reason=f"Good deal: {resource_type.value} @ {order.price_per_unit}c",
)
def _check_clothes_crafting(self) -> Optional[AIDecision]:
"""Check if we should craft clothes."""
if self.agent.has_clothes():
return None
if not self.agent.has_resource(ResourceType.HIDE):
return None
weave_config = ACTION_CONFIG[ActionType.WEAVE]
if not self.agent.stats.can_work(abs(weave_config.energy_cost)):
return None
stats = self.agent.stats
if (stats.thirst < stats.MAX_THIRST * self.LOW_THRESHOLD or
stats.hunger < stats.MAX_HUNGER * self.LOW_THRESHOLD):
return None
return AIDecision(
action=ActionType.WEAVE,
target_resource=ResourceType.CLOTHES,
reason="Crafting clothes for heat protection",
)
def _address_thirst(self, critical: bool = False) -> AIDecision:
"""Address thirst."""
prefix = "Critical" if critical else "Low"
if self.agent.has_resource(ResourceType.WATER):
return AIDecision(
action=ActionType.CONSUME,
target_resource=ResourceType.WATER,
reason=f"{prefix} thirst: consuming water",
)
water_order = self.market.get_cheapest_order(ResourceType.WATER)
if water_order and water_order.seller_id != self.agent.id:
if self.agent.money >= water_order.price_per_unit:
fair_value = self._get_resource_fair_value(ResourceType.WATER)
should_buy = (
self._is_good_buy(ResourceType.WATER, water_order.price_per_unit) or
critical or
(self._is_wealthy() and water_order.price_per_unit <= fair_value * 1.5)
)
if should_buy:
return AIDecision(
action=ActionType.TRADE,
target_resource=ResourceType.WATER,
order_id=water_order.id,
quantity=1,
price=water_order.price_per_unit,
reason=f"{prefix} thirst: buying water @ {water_order.price_per_unit}c",
)
water_config = ACTION_CONFIG[ActionType.GET_WATER]
if self.agent.stats.can_work(abs(water_config.energy_cost)):
return AIDecision(
action=ActionType.GET_WATER,
target_resource=ResourceType.WATER,
reason=f"{prefix} thirst: getting water",
)
if self.agent.has_resource(ResourceType.BERRIES):
return AIDecision(
action=ActionType.CONSUME,
target_resource=ResourceType.BERRIES,
reason=f"{prefix} thirst: consuming berries (emergency)",
)
return AIDecision(
action=ActionType.REST,
reason=f"{prefix} thirst: too tired, resting",
)
def _address_hunger(self, critical: bool = False) -> AIDecision:
"""Address hunger."""
prefix = "Critical" if critical else "Low"
if self.agent.has_resource(ResourceType.MEAT):
return AIDecision(
action=ActionType.CONSUME,
target_resource=ResourceType.MEAT,
reason=f"{prefix} hunger: consuming meat",
)
if self.agent.has_resource(ResourceType.BERRIES):
return AIDecision(
action=ActionType.CONSUME,
target_resource=ResourceType.BERRIES,
reason=f"{prefix} hunger: consuming berries",
)
for resource_type in [ResourceType.MEAT, ResourceType.BERRIES]:
order = self.market.get_cheapest_order(resource_type)
if order and order.seller_id != self.agent.id and self.agent.money >= order.price_per_unit:
fair_value = self._get_resource_fair_value(resource_type)
should_buy = (
self._is_good_buy(resource_type, order.price_per_unit) or
critical or
(self._is_wealthy() and order.price_per_unit <= fair_value * 1.5)
)
if should_buy:
return AIDecision(
action=ActionType.TRADE,
target_resource=resource_type,
order_id=order.id,
quantity=1,
price=order.price_per_unit,
reason=f"{prefix} hunger: buying {resource_type.value}",
)
gather_config = ACTION_CONFIG[ActionType.GATHER]
if self.agent.stats.can_work(abs(gather_config.energy_cost)):
return AIDecision(
action=ActionType.GATHER,
target_resource=ResourceType.BERRIES,
reason=f"{prefix} hunger: gathering berries",
)
return AIDecision(
action=ActionType.REST,
reason=f"{prefix} hunger: too tired, resting",
)
def _address_heat(self, critical: bool = False) -> Optional[AIDecision]:
"""Address heat."""
prefix = "Critical" if critical else "Low"
# Fuel is better than wood for heat
if self.agent.has_resource(ResourceType.FUEL):
burn_config = ACTION_CONFIG[ActionType.BURN_FUEL]
if self.agent.stats.can_work(abs(burn_config.energy_cost)):
return AIDecision(
action=ActionType.BURN_FUEL,
target_resource=ResourceType.FUEL,
reason=f"{prefix} heat: burning fuel",
)
if self.agent.has_resource(ResourceType.WOOD):
fire_config = ACTION_CONFIG[ActionType.BUILD_FIRE]
if self.agent.stats.can_work(abs(fire_config.energy_cost)):
return AIDecision(
action=ActionType.BUILD_FIRE,
target_resource=ResourceType.WOOD,
reason=f"{prefix} heat: building fire",
)
# Try to buy fuel or wood
for resource_type in [ResourceType.FUEL, ResourceType.WOOD]:
cheapest = self.market.get_cheapest_order(resource_type)
if cheapest and cheapest.seller_id != self.agent.id:
if self.agent.money >= cheapest.price_per_unit:
fair_value = self._get_resource_fair_value(resource_type)
should_buy = (
self._is_good_buy(resource_type, cheapest.price_per_unit) or
critical or
(self._is_wealthy() and cheapest.price_per_unit <= fair_value * 1.5)
)
if should_buy:
return AIDecision(
action=ActionType.TRADE,
target_resource=resource_type,
order_id=cheapest.id,
quantity=1,
price=cheapest.price_per_unit,
reason=f"{prefix} heat: buying {resource_type.value}",
)
chop_config = ACTION_CONFIG[ActionType.CHOP_WOOD]
if self.agent.stats.can_work(abs(chop_config.energy_cost)):
return AIDecision(
action=ActionType.CHOP_WOOD,
target_resource=ResourceType.WOOD,
reason=f"{prefix} heat: chopping wood",
)
if not critical:
return None
return AIDecision(
action=ActionType.REST,
reason=f"{prefix} heat: too tired, resting",
)
def _check_energy(self) -> Optional[AIDecision]:
"""Check if energy management is needed."""
stats = self.agent.stats
if stats.energy < self.LOW_ENERGY_THRESHOLD:
return AIDecision(
action=ActionType.REST,
reason=f"Energy critically low ({stats.energy}), must rest",
)
if self.is_evening and stats.energy < self.REST_ENERGY_THRESHOLD:
has_supplies = (
self.agent.get_resource_count(ResourceType.WATER) >= 1 and
(self.agent.get_resource_count(ResourceType.MEAT) >= 1 or
self.agent.get_resource_count(ResourceType.BERRIES) >= 2)
)
if has_supplies:
return AIDecision(
action=ActionType.REST,
reason="Evening: resting for night",
)
return None
def _check_economic(self) -> Optional[AIDecision]:
"""Economic activities."""
decision = self._try_proactive_sell()
if decision:
return decision
if self.agent.inventory_space() <= 2:
decision = self._try_to_sell(urgent=True)
if decision:
return decision
return None
def _try_proactive_sell(self) -> Optional[AIDecision]:
"""Proactively sell when market conditions are good."""
if self._is_wealthy() and self.agent.inventory_space() > 3:
return None
if random.random() < self.p.hoarding_rate * 0.7:
return None
base_min = {
ResourceType.WATER: 2,
ResourceType.MEAT: 1,
ResourceType.BERRIES: 2,
ResourceType.WOOD: 2,
ResourceType.HIDE: 0,
ResourceType.OIL: 0,
ResourceType.FUEL: 1,
}
hoarding_mult = 0.5 + self.p.hoarding_rate
survival_minimums = {k: int(v * hoarding_mult) for k, v in base_min.items()}
best_opportunity = None
best_score = 0
for resource in self.agent.inventory:
if resource.type == ResourceType.CLOTHES:
continue
min_keep = survival_minimums.get(resource.type, 1)
excess = resource.quantity - min_keep
if excess <= 0:
continue
signal = self.market.get_market_signal(resource.type)
fair_value = self._get_resource_fair_value(resource.type)
sell_modifier = get_trade_price_modifier(self.skills.trading, is_buying=False)
if signal == "sell":
price = int(fair_value * 1.3 * sell_modifier)
score = 3 + excess
elif signal == "hold":
price = int(fair_value * sell_modifier)
score = 1 + excess * 0.5
else:
cheapest = self.market.get_cheapest_order(resource.type)
if cheapest and cheapest.seller_id != self.agent.id:
price = max(1, cheapest.price_per_unit - 1)
else:
price = int(fair_value * 0.8 * sell_modifier)
score = 0.5
# Oil and fuel have higher sell priority
if resource.type in [ResourceType.OIL, ResourceType.FUEL]:
score *= 1.5
score *= (0.7 + self.p.wealth_desire * 0.6)
if score > best_score:
best_score = score
best_opportunity = (resource.type, min(excess, 3), price)
if best_opportunity and best_score >= 1:
resource_type, quantity, price = best_opportunity
return AIDecision(
action=ActionType.TRADE,
target_resource=resource_type,
quantity=quantity,
price=price,
reason=f"Selling {resource_type.value} @ {price}c",
)
return None
def _try_to_sell(self, urgent: bool = False) -> Optional[AIDecision]:
"""Sell excess resources."""
survival_minimums = {
ResourceType.WATER: 2 if urgent else 3,
ResourceType.MEAT: 1 if urgent else 2,
ResourceType.BERRIES: 2 if urgent else 3,
ResourceType.WOOD: 1 if urgent else 2,
ResourceType.HIDE: 0,
ResourceType.OIL: 0,
ResourceType.FUEL: 0,
}
for resource in self.agent.inventory:
if resource.type == ResourceType.CLOTHES:
continue
min_keep = survival_minimums.get(resource.type, 1)
if resource.quantity > min_keep:
quantity_to_sell = resource.quantity - min_keep
price = self._calculate_sell_price(resource.type)
reason = "Urgent: clearing inventory" if urgent else f"Selling excess {resource.type.value}"
return AIDecision(
action=ActionType.TRADE,
target_resource=resource.type,
quantity=quantity_to_sell,
price=price,
reason=reason,
)
return None
def _calculate_sell_price(self, resource_type: ResourceType) -> int:
"""Calculate sell price."""
fair_value = self._get_resource_fair_value(resource_type)
suggested = self.market.get_suggested_price(resource_type, fair_value)
cheapest = self.market.get_cheapest_order(resource_type)
if cheapest and cheapest.seller_id != self.agent.id:
signal = self.market.get_market_signal(resource_type)
if signal != "sell":
suggested = min(suggested, cheapest.price_per_unit)
return max(1, suggested)
def _do_survival_work(self) -> AIDecision:
"""Perform work based on survival needs and personality."""
stats = self.agent.stats
water_count = self.agent.get_resource_count(ResourceType.WATER)
meat_count = self.agent.get_resource_count(ResourceType.MEAT)
berry_count = self.agent.get_resource_count(ResourceType.BERRIES)
wood_count = self.agent.get_resource_count(ResourceType.WOOD)
food_count = meat_count + berry_count
heat_urgency = 1 - (stats.heat / stats.MAX_HEAT)
def get_resource_decision(resource_type: ResourceType, gather_action: ActionType, reason: str) -> AIDecision:
if random.random() < self.p.market_affinity:
order = self.market.get_cheapest_order(resource_type)
if order and order.seller_id != self.agent.id and self.agent.money >= order.price_per_unit:
buy_modifier = get_trade_price_modifier(self.skills.trading, is_buying=True)
effective_price = order.price_per_unit
if self._is_good_buy(resource_type, effective_price):
return AIDecision(
action=ActionType.TRADE,
target_resource=resource_type,
order_id=order.id,
quantity=1,
price=order.price_per_unit,
reason=f"{reason} (buying)",
)
config = ACTION_CONFIG[gather_action]
if self.agent.stats.can_work(abs(config.energy_cost)):
# Apply religion bonus
religion_bonus = get_religion_action_bonus(
self.religion.religion, gather_action.value
)
return AIDecision(
action=gather_action,
target_resource=resource_type,
reason=f"{reason} (gathering)",
)
return None
if water_count < self.MIN_WATER_STOCK:
decision = get_resource_decision(
ResourceType.WATER,
ActionType.GET_WATER,
f"Stocking water ({water_count} < {self.MIN_WATER_STOCK})"
)
if decision:
return decision
if wood_count < self.MIN_WOOD_STOCK and heat_urgency > 0.3:
decision = get_resource_decision(
ResourceType.WOOD,
ActionType.CHOP_WOOD,
f"Stocking wood ({wood_count} < {self.MIN_WOOD_STOCK})"
)
if decision:
return decision
if food_count < self.MIN_FOOD_STOCK:
hunt_config = ACTION_CONFIG[ActionType.HUNT]
can_hunt = stats.energy >= abs(hunt_config.energy_cost) + 5
hunt_score = self.p.hunt_preference * self.p.risk_tolerance
gather_score = self.p.gather_preference * (1.5 - self.p.risk_tolerance)
total = hunt_score + gather_score
hunt_prob = hunt_score / total if total > 0 else 0.3
if meat_count == 0:
hunt_prob = min(0.8, hunt_prob + 0.3)
prefer_hunt = can_hunt and random.random() < hunt_prob
if prefer_hunt:
decision = get_resource_decision(
ResourceType.MEAT,
ActionType.HUNT,
f"Hunting ({food_count} < {self.MIN_FOOD_STOCK})"
)
if decision:
return decision
decision = get_resource_decision(
ResourceType.BERRIES,
ActionType.GATHER,
f"Stocking food ({food_count} < {self.MIN_FOOD_STOCK})"
)
if decision:
return decision
# Evening preparation
if self.is_late_day:
if water_count < self.MIN_WATER_STOCK + 1:
decision = get_resource_decision(ResourceType.WATER, ActionType.GET_WATER, "Evening: stocking water")
if decision:
return decision
if food_count < self.MIN_FOOD_STOCK + 1:
decision = get_resource_decision(ResourceType.BERRIES, ActionType.GATHER, "Evening: stocking food")
if decision:
return decision
if wood_count < self.MIN_WOOD_STOCK + 1:
decision = get_resource_decision(ResourceType.WOOD, ActionType.CHOP_WOOD, "Evening: stocking wood")
if decision:
return decision
# Default work
needs = []
if water_count < self.MIN_WATER_STOCK + 2:
needs.append((ResourceType.WATER, ActionType.GET_WATER, "Getting water", 2.0))
if food_count < self.MIN_FOOD_STOCK + 2:
needs.append((ResourceType.BERRIES, ActionType.GATHER, "Gathering berries",
2.0 * self.p.gather_preference))
hunt_config = ACTION_CONFIG[ActionType.HUNT]
if stats.energy >= abs(hunt_config.energy_cost) + 3:
hunt_weight = 2.0 * self.p.hunt_preference * self.p.risk_tolerance
needs.append((ResourceType.MEAT, ActionType.HUNT, "Hunting", hunt_weight))
if wood_count < self.MIN_WOOD_STOCK + 2:
needs.append((ResourceType.WOOD, ActionType.CHOP_WOOD, "Chopping wood",
1.0 * self.p.woodcut_preference))
if not needs:
if self.agent.inventory_space() <= 4:
decision = self._try_proactive_sell()
if decision:
return decision
if self.p.hunt_preference > self.p.gather_preference and stats.energy >= 10:
return AIDecision(
action=ActionType.HUNT,
target_resource=ResourceType.MEAT,
reason="Default: hunting",
)
return AIDecision(
action=ActionType.GATHER,
target_resource=ResourceType.BERRIES,
reason="Default: gathering",
)
if random.random() < self.p.market_affinity:
for resource_type, action, reason, weight in needs:
order = self.market.get_cheapest_order(resource_type)
if order and order.seller_id != self.agent.id and self.agent.money >= order.price_per_unit:
if self._is_good_buy(resource_type, order.price_per_unit):
return AIDecision(
action=ActionType.TRADE,
target_resource=resource_type,
order_id=order.id,
quantity=1,
price=order.price_per_unit,
reason=f"{reason} (buying cheap)",
)
total_weight = sum(weight for _, _, _, weight in needs)
r = random.random() * total_weight
cumulative = 0
for resource, action, reason, weight in needs:
cumulative += weight
if r <= cumulative:
return AIDecision(
action=action,
target_resource=resource,
reason=reason,
)
resource, action, reason, _ = needs[0]
return AIDecision(
action=action,
target_resource=resource,
reason=reason,
)
def get_ai_decision(agent: Agent, market: "OrderBook", step_in_day: int = 1,
day_steps: int = 10, current_turn: int = 0, world = None) -> AIDecision:
"""Convenience function to get an AI decision for an agent."""
ai = AgentAI(agent, market, step_in_day, day_steps, current_turn, world)
return ai.decide()