"""AI decision system for agents in the Village Simulation. Major rework to create market-driven economy: - Agents understand that BUYING saves energy (trading is smart!) - Wealth accumulation as a goal (money = safety buffer) - Dynamic pricing based on supply/demand signals - Proactive trading - buy low, sell high - Market participation is now central to survival strategy Key insight: An agent with money can survive without working. The market is not a last resort - it's the optimal strategy when prices are good. """ 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 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 market-driven economy behavior. Core philosophy: Trading is SMART, not a last resort. The agent now understands: 1. Buying is often more efficient than gathering (saves energy!) 2. Money is power - wealth means safety and flexibility 3. Selling at good prices builds wealth 4. Adjusting prices responds to supply/demand 5. The market is a tool for survival, not just emergency trades Economic behaviors: - Calculate "fair value" of resources based on energy cost - Buy when market price < energy cost to gather - Sell when market price > production cost - Adjust prices based on market conditions (supply/demand) - Accumulate wealth as a safety buffer """ # 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 MIN_WATER_STOCK = 3 MIN_FOOD_STOCK = 4 MIN_WOOD_STOCK = 3 # Heat thresholds HEAT_PROACTIVE_THRESHOLD = 0.50 # ECONOMY SETTINGS - These make agents trade more ENERGY_TO_MONEY_RATIO = 1.5 # 1 energy = ~1.5 coins in perceived value WEALTH_DESIRE = 0.3 # How much agents want to accumulate wealth (0-1) BUY_EFFICIENCY_THRESHOLD = 0.7 # Buy if market price < 70% of gather cost MIN_WEALTH_TARGET = 50 # Agents want at least this much money 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 # Load thresholds from config config = _get_ai_config() self.CRITICAL_THRESHOLD = config.critical_threshold self.LOW_ENERGY_THRESHOLD = config.low_energy_threshold # Try to load economy config economy = _get_economy_config() if economy: self.ENERGY_TO_MONEY_RATIO = getattr(economy, 'energy_to_money_ratio', self.ENERGY_TO_MONEY_RATIO) self.WEALTH_DESIRE = getattr(economy, 'wealth_desire', self.WEALTH_DESIRE) self.BUY_EFFICIENCY_THRESHOLD = getattr(economy, 'buy_efficiency_threshold', self.BUY_EFFICIENCY_THRESHOLD) self.MIN_WEALTH_TARGET = getattr(economy, 'min_wealth_target', self.MIN_WEALTH_TARGET) @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 AND economic optimization. Key insight: Trading is often BETTER than gathering because: 1. Trade uses only 1 energy vs 4-8 for gathering 2. If market price < energy cost, buying is pure profit 3. Money = stored energy = safety buffer """ # 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: Price adjustment - respond to market conditions decision = self._check_price_adjustments() if decision: return decision # Priority 4: Smart shopping - buy good deals on the market! decision = self._check_market_opportunities() if decision: return decision # Priority 5: Craft clothes if we have hide decision = self._check_clothes_crafting() if decision: return decision # Priority 6: Energy management decision = self._check_energy() if decision: return decision # Priority 7: Economic activities (sell excess, build wealth) decision = self._check_economic() if decision: return decision # Priority 8: Routine survival work (gather resources we need) return self._do_survival_work() 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 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 efficiency_score = fair_value / order.price_per_unit # 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 can_afford = self.agent.money // order.price_per_unit 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. Sell when: - Market signal says 'sell' (scarcity) - We have more than minimum stock - We could use more money (not wealthy) """ if self._is_wealthy() and self.agent.inventory_space() > 3: # Already rich and have space, no rush to sell return None survival_minimums = { ResourceType.WATER: 2, ResourceType.MEAT: 1, ResourceType.BERRIES: 2, ResourceType.WOOD: 2, ResourceType.HIDE: 0, } # 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) # Calculate optimal price if signal == "sell": # Scarcity - we can charge more price = int(fair_value * 1.3) # 30% markup score = 3 + excess elif signal == "hold": # Normal market price = fair_value 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) score = 0.5 # Not a great time to sell 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"Market opportunity: 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 what we need most for survival. KEY CHANGE: Always consider buying as an alternative! If there's a good deal on the market, BUY instead of gathering. This is the core economic behavior we want. """ 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 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 @ {order.price_per_unit}c - good deal!)", ) # 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) 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: # Decide between hunting and gathering based on conditions # Meat is more valuable (more hunger restored), but hunting costs more energy hunt_config = ACTION_CONFIG[ActionType.HUNT] gather_config = ACTION_CONFIG[ActionType.GATHER] # Prefer hunting if: # - We have enough energy for hunt # - AND (we have no meat OR random chance favors hunting for diversity) can_hunt = stats.energy >= abs(hunt_config.energy_cost) + 5 prefer_hunt = (meat_count == 0) or (can_hunt and random.random() < 0.4) # 40% hunt chance if prefer_hunt and can_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 (with buy checks) needs = [] if water_count < self.MIN_WATER_STOCK + 2: needs.append((ResourceType.WATER, ActionType.GET_WATER, "Getting water", 2)) if food_count < self.MIN_FOOD_STOCK + 2: # Both berries and hunting are valid options needs.append((ResourceType.BERRIES, ActionType.GATHER, "Gathering berries", 2)) # Add hunting with good weight if we have energy hunt_config = ACTION_CONFIG[ActionType.HUNT] if stats.energy >= abs(hunt_config.energy_cost) + 3: needs.append((ResourceType.MEAT, ActionType.HUNT, "Hunting for meat", 2)) # Same weight as berries if wood_count < self.MIN_WOOD_STOCK + 2: needs.append((ResourceType.WOOD, ActionType.CHOP_WOOD, "Chopping wood", 1)) 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: maintain food supply return AIDecision( action=ActionType.GATHER, target_resource=ResourceType.BERRIES, reason="Maintaining supplies", ) # For each need, check if we can buy cheaply 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()