1189 lines
49 KiB
Python

"""AI decision system for agents in the Village Simulation.
Major rework to create diverse, personality-driven economy:
- Each agent has unique personality traits affecting all decisions
- Emergent professions: Hunters, Gatherers, Traders, Generalists
- 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()