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