"""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 - Class inequality through varied strategies and skills - Traders focus on arbitrage (buy low, sell high) - Personality affects: risk tolerance, hoarding, market participation Key insight: Different personalities lead to different strategies. Traders don't gather - they profit from others' labor. Hunters take risks for bigger rewards. Gatherers play it safe. """ 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 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 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, } # 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, } # Energy cost to gather each resource (used for efficiency calculations) 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) class AgentAI: """AI decision maker with personality-driven economy behavior. Core philosophy: Each agent has a unique strategy based on personality. Personality effects: 1. wealth_desire: How aggressively to accumulate money 2. hoarding_rate: How much to keep vs. sell on market 3. risk_tolerance: Hunt (risky, high reward) vs. gather (safe) 4. market_affinity: How often to engage with market 5. trade_preference: High = trader profession (arbitrage focus) 6. price_sensitivity: How picky about deals Emergent professions: - Traders: High trade_preference + market_affinity = buy low, sell high - Hunters: High hunt_preference + risk_tolerance = meat production - Gatherers: High gather_preference, low risk = safe resource collection - Generalists: Balanced approach to all activities """ # Thresholds for stat management LOW_THRESHOLD = 0.45 # 45% - proactive action trigger COMFORT_THRESHOLD = 0.60 # 60% - aim for comfort # Energy thresholds REST_ENERGY_THRESHOLD = 18 # Rest when below this if no urgent needs WORK_ENERGY_MINIMUM = 20 # Prefer to have this much for work # Resource stockpile targets (modified by personality.hoarding_rate) BASE_WATER_STOCK = 2 BASE_FOOD_STOCK = 3 BASE_WOOD_STOCK = 2 # Heat thresholds HEAT_PROACTIVE_THRESHOLD = 0.50 # Base economy settings (modified by personality) ENERGY_TO_MONEY_RATIO = 1.5 # 1 energy = ~1.5 coins in perceived value MAX_PRICE_MARKUP = 2.0 # Maximum markup over base price MIN_PRICE_DISCOUNT = 0.5 # Minimum discount (50% of base price) def __init__(self, agent: Agent, market: "OrderBook", step_in_day: int = 1, day_steps: int = 10, current_turn: int = 0): self.agent = agent self.market = market self.step_in_day = step_in_day self.day_steps = day_steps self.current_turn = current_turn # Personality shortcuts self.p = agent.personality # Convenience shortcut self.skills = agent.skills # 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 # Wealth desire from personality (0.1 to 0.9) self.WEALTH_DESIRE = self.p.wealth_desire # Buy efficiency threshold adjusted by price sensitivity # High sensitivity = only buy very good deals 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 # Wealth target scaled by wealth desire 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)) # Resource stockpile targets modified by hoarding rate # High hoarders keep more in reserve hoarding_mult = 0.5 + self.p.hoarding_rate # 0.6 to 1.4 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)) # Trader mode: agents with high trade preference become market-focused self.is_trader = self.p.trade_preference > 1.3 and self.p.market_affinity > 0.5 @property def is_evening(self) -> bool: """Check if it's getting close to night (last 2 day steps).""" return self.step_in_day >= self.day_steps - 1 @property def is_late_day(self) -> bool: """Check if it's past midday (preparation time).""" 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 based on energy cost to produce. This is the theoretical minimum price an agent should sell for, and the maximum they should pay before just gathering themselves. """ energy_cost = get_energy_cost(resource_type) return max(1, int(energy_cost * self.ENERGY_TO_MONEY_RATIO)) def _is_good_buy(self, resource_type: ResourceType, price: int) -> bool: """Check if a market price is a good deal (cheaper than gathering).""" fair_value = self._get_resource_fair_value(resource_type) return price <= fair_value * self.BUY_EFFICIENCY_THRESHOLD def _is_wealthy(self) -> bool: """Check if agent has comfortable wealth.""" return self.agent.money >= self.MIN_WEALTH_TARGET def decide(self) -> AIDecision: """Make a decision based on survival, personality, and economic goals. Decision flow varies by personality: - Traders prioritize market operations (arbitrage) - Hunters prefer hunting when possible - Gatherers stick to safe resource collection - All agents prioritize survival when needed """ # Priority 1: Critical survival needs (immediate danger) decision = self._check_critical_needs() if decision: return decision # Priority 2: Proactive survival (prevent problems before they happen) decision = self._check_proactive_needs() if decision: return decision # Priority 3: Trader-specific behavior (high trade_preference agents) # Traders focus on market operations when survival is secured if self.is_trader: decision = self._do_trader_behavior() if decision: return decision # Priority 4: Price adjustment - respond to market conditions decision = self._check_price_adjustments() if decision: return decision # Priority 5: Smart shopping - buy good deals on the market! # Frequency affected by market_affinity if random.random() < self.p.market_affinity: decision = self._check_market_opportunities() if decision: return decision # Priority 6: Craft clothes if we have hide decision = self._check_clothes_crafting() if decision: return decision # Priority 7: Energy management decision = self._check_energy() if decision: return decision # Priority 8: Economic activities (sell excess, build wealth) # Frequency affected by hoarding_rate (low hoarding = sell more) if random.random() > self.p.hoarding_rate * 0.5: decision = self._check_economic() if decision: return decision # Priority 9: Routine survival work (gather resources we need) return self._do_survival_work() def _do_trader_behavior(self) -> Optional[AIDecision]: """Trader-specific behavior: focus on arbitrage and market operations. Traders don't gather much - they profit from buying low and selling high. Core trader strategy: 1. Look for arbitrage opportunities (price differences) 2. Buy underpriced goods 3. Sell at markup 4. Build wealth through trading margins """ # Traders need money to operate if self.agent.money < 20: # Low on capital - need to do some work or sell inventory decision = self._try_to_sell(urgent=True) if decision: return decision # If nothing to sell, do some quick gathering return None # Fall through to normal behavior # Look for arbitrage opportunities # Find resources being sold below fair value best_deal = None best_margin = 0 for resource_type in [ResourceType.MEAT, ResourceType.BERRIES, ResourceType.WATER, ResourceType.WOOD]: 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) # Check if we can profit by buying and reselling buy_price = order.price_per_unit # Apply trading skill to get better prices sell_modifier = get_trade_price_modifier(self.skills.trading, is_buying=False) potential_sell_price = int(fair_value * sell_modifier * 1.1) # 10% markup target 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) # Execute arbitrage if profitable if best_deal and best_margin >= 2: # At least 2 coins profit resource_type, order, buy_price, sell_price = best_deal safe_price = max(1, buy_price) # Prevent division by zero 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 (resell @ {sell_price}c)", ) # If holding inventory, try to sell at markup decision = self._try_trader_sell() if decision: return decision # Adjust prices on existing orders decision = self._check_price_adjustments() if decision: return decision return None def _try_trader_sell(self) -> Optional[AIDecision]: """Trader sells inventory at markup prices.""" for resource in self.agent.inventory: if resource.type == ResourceType.CLOTHES: continue # Traders sell everything except minimal survival reserves if resource.quantity <= 1: continue fair_value = self._get_resource_fair_value(resource.type) # Apply trading skill for better sell prices sell_modifier = get_trade_price_modifier(self.skills.trading, is_buying=False) # Check market conditions signal = self.market.get_market_signal(resource.type) if signal == "sell": # Scarcity - high markup price = int(fair_value * sell_modifier * 1.4) else: price = int(fair_value * sell_modifier * 1.15) # Don't undercut ourselves if we have active orders 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 # Wait for existing order to sell quantity = resource.quantity - 1 # Keep 1 for emergencies return AIDecision( action=ActionType.TRADE, target_resource=resource.type, quantity=quantity, price=price, reason=f"Trader: selling {resource.type.value} @ {price}c (markup)", ) return None def _check_critical_needs(self) -> Optional[AIDecision]: """Check if any vital stat is critical and act accordingly.""" stats = self.agent.stats # Check thirst first (depletes fastest and kills quickly) if stats.thirst < stats.MAX_THIRST * self.CRITICAL_THRESHOLD: return self._address_thirst(critical=True) # Check hunger if stats.hunger < stats.MAX_HUNGER * self.CRITICAL_THRESHOLD: return self._address_hunger(critical=True) # Check heat - critical level 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 they become critical. IMPORTANT CHANGE: Now considers buying as a first option, not last! """ stats = self.agent.stats # Proactive thirst management if stats.thirst < stats.MAX_THIRST * self.LOW_THRESHOLD: return self._address_thirst(critical=False) # Proactive hunger management if stats.hunger < stats.MAX_HUNGER * self.LOW_THRESHOLD: return self._address_hunger(critical=False) # Proactive heat management if stats.heat < stats.MAX_HEAT * self.HEAT_PROACTIVE_THRESHOLD: decision = self._address_heat(critical=False) if decision: return decision return None def _check_price_adjustments(self) -> Optional[AIDecision]: """Check if we should adjust prices on our market orders. Smart pricing strategy: - If order is stale (not selling), lower price - If demand is high (scarcity), raise price - Respond to market signals """ 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 demand is high and we've waited, consider raising price if signal == "sell" and order.can_raise_price(self.current_turn, min_turns=3): # Scarcity - raise price (but not too high) new_price = min( int(current_price * 1.25), # 25% increase 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 is getting stale (sitting too long), lower price if order.turns_without_sale >= 5: # Calculate competitive price lowest_order = self.market.get_cheapest_order(resource_type) if lowest_order and lowest_order.id != order.id: # Price just below the cheapest new_price = max( lowest_order.price_per_unit - 1, int(fair_value * self.MIN_PRICE_DISCOUNT) ) else: # We're the only seller - slight discount to attract buyers 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 order: lowering {resource_type.value} price to {new_price}c", ) return None def _check_market_opportunities(self) -> Optional[AIDecision]: """Look for good buying opportunities on the market. KEY INSIGHT: If market price < energy cost to gather, ALWAYS BUY! This is the core of smart trading behavior. Buying is smart because: - Trade costs only 1 energy - Gathering costs 4-8 energy - If price is low, we're getting resources for less than production cost """ # Don't shop if we're low on money and not wealthy if self.agent.money < 10: return None # Resources we might want to buy shopping_list = [] # Check each resource type for good deals for resource_type in [ResourceType.WATER, ResourceType.BERRIES, ResourceType.MEAT, ResourceType.WOOD]: order = self.market.get_cheapest_order(resource_type) if not order or order.seller_id == self.agent.id: continue # Skip invalid orders (price <= 0) if order.price_per_unit <= 0: continue if self.agent.money < order.price_per_unit: continue # Calculate if this is a good deal fair_value = self._get_resource_fair_value(resource_type) is_good_deal = self._is_good_buy(resource_type, order.price_per_unit) # Calculate our current need for this resource 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 # Score this opportunity if is_good_deal: # Good deal - definitely consider buying price = max(1, order.price_per_unit) # Prevent division by zero efficiency_score = fair_value / price # How much we're saving total_score = need_score + efficiency_score * 2 shopping_list.append((resource_type, order, total_score)) elif need_score > 0 and self._is_wealthy(): # Not a great deal, but we need it and have money total_score = need_score * 0.5 shopping_list.append((resource_type, order, total_score)) if not shopping_list: return None # Sort by score and pick the best opportunity shopping_list.sort(key=lambda x: x[2], reverse=True) resource_type, order, score = shopping_list[0] # Only act if the opportunity is worth it if score < 1: return None # Calculate how much to buy price = max(1, order.price_per_unit) # Prevent division by zero can_afford = self.agent.money // price space = self.agent.inventory_space() want_quantity = min(2, can_afford, space, order.quantity) # Buy up to 2 at a time 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: buying {resource_type.value} @ {order.price_per_unit}c (fair value: {self._get_resource_fair_value(resource_type)}c)", ) def _check_clothes_crafting(self) -> Optional[AIDecision]: """Check if we should craft clothes for heat efficiency.""" # Only craft if we don't already have clothes and have hide if self.agent.has_clothes(): return None # Need hide to craft if not self.agent.has_resource(ResourceType.HIDE): return None # Need energy to craft weave_config = ACTION_CONFIG[ActionType.WEAVE] if not self.agent.stats.can_work(abs(weave_config.energy_cost)): return None # Only craft if we're not in survival mode 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 - water is the primary solution. NEW PRIORITY: Try buying FIRST if it's efficient! Trading uses only 1 energy vs 3 for getting water. """ prefix = "Critical" if critical else "Low" # Step 1: Consume water from inventory (best - immediate, free) if self.agent.has_resource(ResourceType.WATER): return AIDecision( action=ActionType.CONSUME, target_resource=ResourceType.WATER, reason=f"{prefix} thirst: consuming water", ) # Step 2: Check if buying is more efficient than gathering # Trade = 1 energy, Get water = 3 energy. If price is reasonable, BUY! 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) # Buy if: good deal OR critical situation OR we're wealthy 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", ) # Step 3: Get water ourselves 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 from river", ) # Step 4: Emergency - consume berries (gives +3 thirst) if self.agent.has_resource(ResourceType.BERRIES): return AIDecision( action=ActionType.CONSUME, target_resource=ResourceType.BERRIES, reason=f"{prefix} thirst: consuming berries (emergency)", ) # No energy to get water - rest return AIDecision( action=ActionType.REST, reason=f"{prefix} thirst: too tired, resting", ) def _address_hunger(self, critical: bool = False) -> AIDecision: """Address hunger - meat is best, berries are backup. NEW PRIORITY: Consider buying FIRST if market has good prices! Trading = 1 energy vs 4-7 for gathering/hunting. """ prefix = "Critical" if critical else "Low" # Step 1: Consume meat from inventory (best for hunger - +40) if self.agent.has_resource(ResourceType.MEAT): return AIDecision( action=ActionType.CONSUME, target_resource=ResourceType.MEAT, reason=f"{prefix} hunger: consuming meat", ) # Step 2: Consume berries if we have them (+10 hunger) if self.agent.has_resource(ResourceType.BERRIES): return AIDecision( action=ActionType.CONSUME, target_resource=ResourceType.BERRIES, reason=f"{prefix} hunger: consuming berries", ) # Step 3: Check if buying food is more efficient than gathering 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) # Buy if: good deal OR critical OR wealthy 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} @ {order.price_per_unit}c", ) # Step 4: Gather berries ourselves (easy, 100% success) 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", ) # No energy - rest return AIDecision( action=ActionType.REST, reason=f"{prefix} hunger: too tired, resting", ) def _address_heat(self, critical: bool = False) -> Optional[AIDecision]: """Address heat by building fire or getting wood. NOW: Always considers buying wood if it's a good deal! Trade = 1 energy vs 8 energy for chopping. """ prefix = "Critical" if critical else "Low" # Step 1: Build fire if we have wood 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", ) # Step 2: Buy wood if available and efficient cheapest = self.market.get_cheapest_order(ResourceType.WOOD) if cheapest and cheapest.seller_id != self.agent.id and self.agent.money >= cheapest.price_per_unit: fair_value = self._get_resource_fair_value(ResourceType.WOOD) # Buy if: good deal OR critical OR wealthy should_buy = ( self._is_good_buy(ResourceType.WOOD, 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=ResourceType.WOOD, order_id=cheapest.id, quantity=1, price=cheapest.price_per_unit, reason=f"{prefix} heat: buying wood @ {cheapest.price_per_unit}c", ) # Step 3: Chop wood ourselves 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 for fire", ) # If not critical, return None to let other priorities take over if not critical: return None return AIDecision( action=ActionType.REST, reason=f"{prefix} heat: too tired to get wood, resting", ) def _check_energy(self) -> Optional[AIDecision]: """Check if energy management is needed. Improved logic: Don't rest at 13-14 energy just to rest. Instead, rest only if we truly can't do essential work. """ stats = self.agent.stats # Only rest if energy is very low if stats.energy < self.LOW_ENERGY_THRESHOLD: return AIDecision( action=ActionType.REST, reason=f"Energy critically low ({stats.energy}), must rest", ) # If it's evening and energy is moderate, rest to prepare for night if self.is_evening and stats.energy < self.REST_ENERGY_THRESHOLD: # Only if we have enough supplies 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=f"Evening: resting to prepare for night", ) return None def _check_economic(self) -> Optional[AIDecision]: """Economic activities: selling, wealth building, market participation. NEW PHILOSOPHY: Actively participate in the market! - Sell excess resources to build wealth - Price based on supply/demand, not just to clear inventory - Wealth = safety = survival """ # Proactive selling - not just when inventory is full # If we have excess and market is favorable, sell! decision = self._try_proactive_sell() if decision: return decision # If inventory is getting full, must sell 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. Affected by personality: - hoarding_rate: High hoarders keep more, sell less - wealth_desire: High wealth desire = more aggressive selling - trade_preference: High traders sell more frequently """ if self._is_wealthy() and self.agent.inventory_space() > 3: # Already rich and have space, no rush to sell return None # Hoarders are reluctant to sell if random.random() < self.p.hoarding_rate * 0.7: return None # Survival minimums scaled by hoarding rate base_min = { ResourceType.WATER: 2, ResourceType.MEAT: 1, ResourceType.BERRIES: 2, ResourceType.WOOD: 2, ResourceType.HIDE: 0, } # High hoarders keep more hoarding_mult = 0.5 + self.p.hoarding_rate survival_minimums = {k: int(v * hoarding_mult) for k, v in base_min.items()} # Look for profitable sales best_opportunity = None best_score = 0 for resource in self.agent.inventory: if resource.type == ResourceType.CLOTHES: continue # Don't sell clothes min_keep = survival_minimums.get(resource.type, 1) excess = resource.quantity - min_keep if excess <= 0: continue # Check market conditions signal = self.market.get_market_signal(resource.type) fair_value = self._get_resource_fair_value(resource.type) # Apply trading skill for better sell prices sell_modifier = get_trade_price_modifier(self.skills.trading, is_buying=False) # Calculate optimal price if signal == "sell": # Scarcity - we can charge more price = int(fair_value * 1.3 * sell_modifier) score = 3 + excess elif signal == "hold": # Normal market price = int(fair_value * sell_modifier) score = 1 + excess * 0.5 else: # Surplus - price competitively # Find cheapest competitor 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 # Not a great time to sell # Wealth desire increases sell motivation score *= (0.7 + self.p.wealth_desire * 0.6) if score > best_score: best_score = score best_opportunity = (resource.type, min(excess, 3), price) # Sell up to 3 at a time 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, keeping enough for survival.""" 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, } 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 based on fair value and market conditions.""" fair_value = self._get_resource_fair_value(resource_type) # Get market suggestion suggested = self.market.get_suggested_price(resource_type, fair_value) # Check competition cheapest = self.market.get_cheapest_order(resource_type) if cheapest and cheapest.seller_id != self.agent.id: # Don't price higher than cheapest competitor unless scarcity signal = self.market.get_market_signal(resource_type) if signal != "sell": # Match or undercut 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 preferences. Personality effects: - hunt_preference: Likelihood of choosing to hunt - gather_preference: Likelihood of choosing to gather - risk_tolerance: Affects hunt vs gather choice - market_affinity: Likelihood of buying vs gathering """ stats = self.agent.stats # Count current resources 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 # Urgency calculations heat_urgency = 1 - (stats.heat / stats.MAX_HEAT) # Helper to decide: buy or gather? def get_resource_decision(resource_type: ResourceType, gather_action: ActionType, reason: str) -> AIDecision: """Decide whether to buy or gather a resource.""" # Check if buying is efficient (affected by market_affinity) 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: # Apply trading skill for better buy prices buy_modifier = get_trade_price_modifier(self.skills.trading, is_buying=True) effective_price = order.price_per_unit # Skill affects perceived value 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 @ {order.price_per_unit}c)", ) # Gather it ourselves config = ACTION_CONFIG[gather_action] if self.agent.stats.can_work(abs(config.energy_cost)): return AIDecision( action=gather_action, target_resource=resource_type, reason=f"{reason} (gathering)", ) return None # Priority: Stock up on water if low 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 # Priority: Stock up on wood if low (for heat) # Affected by woodcut_preference 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 # Priority: Stock up on food if low if food_count < self.MIN_FOOD_STOCK: hunt_config = ACTION_CONFIG[ActionType.HUNT] # Personality-driven choice between hunting and gathering # risk_tolerance and hunt_preference affect this choice can_hunt = stats.energy >= abs(hunt_config.energy_cost) + 5 # Calculate hunt probability based on personality # High risk_tolerance + high hunt_preference = more hunting hunt_score = self.p.hunt_preference * self.p.risk_tolerance gather_score = self.p.gather_preference * (1.5 - self.p.risk_tolerance) # Normalize to probability total = hunt_score + gather_score hunt_prob = hunt_score / total if total > 0 else 0.3 # Also prefer hunting if we have no meat 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 for food ({food_count} < {self.MIN_FOOD_STOCK})" ) if decision: return decision # Otherwise try berries 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: varied work based on need AND personality preferences 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: # Weight by personality preferences needs.append((ResourceType.BERRIES, ActionType.GATHER, "Gathering berries", 2.0 * self.p.gather_preference)) # Add hunting weighted by personality 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 for meat", 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: # We have good reserves, maybe sell excess or rest if self.agent.inventory_space() <= 4: decision = self._try_proactive_sell() if decision: return decision # Default activity based on personality # High hunt_preference = hunt, else gather 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 (personality)", ) return AIDecision( action=ActionType.GATHER, target_resource=ResourceType.BERRIES, reason="Default: gathering (personality)", ) # For each need, check if we can buy cheaply (market_affinity affects this) 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!)", ) # Weighted random selection for gathering 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, ) # Fallback 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) -> AIDecision: """Convenience function to get an AI decision for an agent.""" ai = AgentAI(agent, market, step_in_day, day_steps, current_turn) return ai.decide()