1253 lines
49 KiB
Python
1253 lines
49 KiB
Python
"""AI decision system for agents in the Village Simulation.
|
|
|
|
This module provides two AI systems:
|
|
1. GOAP (Goal-Oriented Action Planning) - The default, modern approach
|
|
2. Legacy priority-based system - Kept for comparison/fallback
|
|
|
|
GOAP Benefits:
|
|
- Agents plan multi-step sequences to achieve goals
|
|
- Goals are dynamically prioritized based on state
|
|
- More emergent and adaptive behavior
|
|
- Easier to extend with new goals and actions
|
|
|
|
Major features:
|
|
- 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
|
|
"""
|
|
|
|
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 (loaded from config, modified by personality)
|
|
# These are default fallbacks; actual values come from config
|
|
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
|
|
|
|
# Load economy config
|
|
economy = _get_economy_config()
|
|
|
|
# Energy to money ratio (how much 1 energy is worth in coins)
|
|
self.ENERGY_TO_MONEY_RATIO = getattr(economy, 'energy_to_money_ratio', 150) if economy else 150
|
|
|
|
# Minimum price floor
|
|
self.MIN_PRICE = getattr(economy, 'min_price', 100) if economy else 100
|
|
|
|
# Buy efficiency threshold adjusted by price sensitivity
|
|
# High sensitivity = only buy very good deals
|
|
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', 5000) if economy else 5000
|
|
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(self.MIN_PRICE, 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(self.MIN_PRICE, 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(self.MIN_PRICE, 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,
|
|
use_goap: bool = True,
|
|
is_night: bool = False,
|
|
) -> AIDecision:
|
|
"""Get an AI decision for an agent.
|
|
|
|
By default, uses the GOAP (Goal-Oriented Action Planning) system.
|
|
Set use_goap=False to use the legacy priority-based system.
|
|
|
|
Args:
|
|
agent: The agent to make a decision for
|
|
market: The market order book
|
|
step_in_day: Current step within the day
|
|
day_steps: Total steps per day
|
|
current_turn: Current simulation turn
|
|
use_goap: Whether to use GOAP (default True) or legacy system
|
|
is_night: Whether it's currently night time
|
|
|
|
Returns:
|
|
AIDecision with the chosen action and parameters
|
|
"""
|
|
if use_goap:
|
|
from backend.core.goap.goap_ai import get_goap_decision
|
|
return get_goap_decision(
|
|
agent=agent,
|
|
market=market,
|
|
step_in_day=step_in_day,
|
|
day_steps=day_steps,
|
|
current_turn=current_turn,
|
|
is_night=is_night,
|
|
)
|
|
else:
|
|
# Legacy priority-based system
|
|
ai = AgentAI(agent, market, step_in_day, day_steps, current_turn)
|
|
return ai.decide()
|
|
|
|
|
|
def get_legacy_ai_decision(
|
|
agent: Agent,
|
|
market: "OrderBook",
|
|
step_in_day: int = 1,
|
|
day_steps: int = 10,
|
|
current_turn: int = 0,
|
|
) -> AIDecision:
|
|
"""Get an AI decision using the legacy priority-based system.
|
|
|
|
This is kept for comparison and testing purposes.
|
|
"""
|
|
ai = AgentAI(agent, market, step_in_day, day_steps, current_turn)
|
|
return ai.decide()
|