1272 lines
51 KiB
Python
1272 lines
51 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, Oil Workers, Priests
|
|
- Class inequality through varied strategies and skills
|
|
- Traders focus on arbitrage (buy low, sell high)
|
|
- Personality affects: risk tolerance, hoarding, market participation
|
|
|
|
NEW: Religion and diplomacy integration:
|
|
- Agents with high faith perform religious actions (pray, preach)
|
|
- Agents consider faction relations in trading decisions
|
|
- Diplomatic agents negotiate, declare war, or make peace
|
|
- Oil workers focus on drilling and refining
|
|
"""
|
|
|
|
import random
|
|
from dataclasses import dataclass, field
|
|
from typing import Optional, TYPE_CHECKING
|
|
|
|
from backend.domain.agent import Agent
|
|
from backend.domain.action import ActionType, ACTION_CONFIG
|
|
from backend.domain.resources import ResourceType
|
|
from backend.domain.personality import get_trade_price_modifier
|
|
from backend.domain.religion import get_religion_action_bonus
|
|
from backend.domain.diplomacy import (
|
|
FactionType, get_faction_relations, DiplomaticStatus
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from backend.core.market import OrderBook
|
|
|
|
|
|
@dataclass
|
|
class TradeItem:
|
|
"""A single item to buy/sell in a trade."""
|
|
order_id: str
|
|
resource_type: ResourceType
|
|
quantity: int
|
|
price_per_unit: int
|
|
|
|
|
|
@dataclass
|
|
class AIDecision:
|
|
"""A decision made by the AI for an agent."""
|
|
action: ActionType
|
|
target_resource: Optional[ResourceType] = None
|
|
order_id: Optional[str] = None
|
|
quantity: int = 1
|
|
price: int = 0
|
|
reason: str = ""
|
|
# For multi-item trades
|
|
trade_items: list[TradeItem] = field(default_factory=list)
|
|
# For price adjustments
|
|
adjust_order_id: Optional[str] = None
|
|
new_price: Optional[int] = None
|
|
# NEW: For diplomatic/religious actions
|
|
target_agent_id: Optional[str] = None
|
|
target_faction: Optional[FactionType] = None
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"action": self.action.value,
|
|
"target_resource": self.target_resource.value if self.target_resource else None,
|
|
"order_id": self.order_id,
|
|
"quantity": self.quantity,
|
|
"price": self.price,
|
|
"reason": self.reason,
|
|
"trade_items": [
|
|
{
|
|
"order_id": t.order_id,
|
|
"resource_type": t.resource_type.value,
|
|
"quantity": t.quantity,
|
|
"price_per_unit": t.price_per_unit,
|
|
}
|
|
for t in self.trade_items
|
|
],
|
|
"adjust_order_id": self.adjust_order_id,
|
|
"new_price": self.new_price,
|
|
"target_agent_id": self.target_agent_id,
|
|
"target_faction": self.target_faction.value if self.target_faction else None,
|
|
}
|
|
|
|
|
|
# Resource to action for gathering
|
|
RESOURCE_ACTIONS: dict[ResourceType, ActionType] = {
|
|
ResourceType.MEAT: ActionType.HUNT,
|
|
ResourceType.BERRIES: ActionType.GATHER,
|
|
ResourceType.WATER: ActionType.GET_WATER,
|
|
ResourceType.WOOD: ActionType.CHOP_WOOD,
|
|
ResourceType.HIDE: ActionType.HUNT,
|
|
ResourceType.CLOTHES: ActionType.WEAVE,
|
|
ResourceType.OIL: ActionType.DRILL_OIL,
|
|
ResourceType.FUEL: ActionType.REFINE,
|
|
}
|
|
|
|
|
|
def get_energy_cost(resource_type: ResourceType) -> int:
|
|
"""Get the energy cost to produce one unit of a resource."""
|
|
action = RESOURCE_ACTIONS.get(resource_type)
|
|
if not action:
|
|
return 10
|
|
config = ACTION_CONFIG.get(action)
|
|
if not config:
|
|
return 10
|
|
energy_cost = abs(config.energy_cost)
|
|
avg_output = max(1, (config.min_output + config.max_output) / 2) if config.output_resource else 1
|
|
return int(energy_cost / avg_output)
|
|
|
|
|
|
def _get_ai_config():
|
|
"""Get AI-relevant configuration values."""
|
|
from backend.config import get_config
|
|
config = get_config()
|
|
return config.agent_stats
|
|
|
|
|
|
def _get_economy_config():
|
|
"""Get economy/market configuration values."""
|
|
from backend.config import get_config
|
|
config = get_config()
|
|
return getattr(config, 'economy', None)
|
|
|
|
|
|
def _get_religion_config():
|
|
"""Get religion configuration values."""
|
|
from backend.config import get_config
|
|
config = get_config()
|
|
return getattr(config, 'religion', None)
|
|
|
|
|
|
def _get_diplomacy_config():
|
|
"""Get diplomacy configuration values."""
|
|
from backend.config import get_config
|
|
config = get_config()
|
|
return getattr(config, 'diplomacy', None)
|
|
|
|
|
|
class AgentAI:
|
|
"""AI decision maker with personality-driven economy behavior.
|
|
|
|
Now includes religion and diplomacy considerations.
|
|
"""
|
|
|
|
# Thresholds for stat management
|
|
LOW_THRESHOLD = 0.55 # Increased from 0.45 to be more proactive about survival
|
|
COMFORT_THRESHOLD = 0.70 # Increased from 0.60
|
|
REST_ENERGY_THRESHOLD = 18
|
|
WORK_ENERGY_MINIMUM = 20
|
|
|
|
# Resource stockpile targets
|
|
BASE_WATER_STOCK = 2
|
|
BASE_FOOD_STOCK = 3
|
|
BASE_WOOD_STOCK = 2
|
|
|
|
# Heat thresholds
|
|
HEAT_PROACTIVE_THRESHOLD = 0.50
|
|
|
|
# Faith thresholds
|
|
LOW_FAITH_THRESHOLD = 0.30
|
|
HIGH_FAITH_THRESHOLD = 0.70
|
|
|
|
# Economy settings
|
|
ENERGY_TO_MONEY_RATIO = 1.5
|
|
MAX_PRICE_MARKUP = 2.0
|
|
MIN_PRICE_DISCOUNT = 0.5
|
|
|
|
def __init__(self, agent: Agent, market: "OrderBook", step_in_day: int = 1,
|
|
day_steps: int = 10, current_turn: int = 0, world = None):
|
|
self.agent = agent
|
|
self.market = market
|
|
self.step_in_day = step_in_day
|
|
self.day_steps = day_steps
|
|
self.current_turn = current_turn
|
|
self.world = world # NEW: Reference to world for nearby agents
|
|
|
|
self.p = agent.personality
|
|
self.skills = agent.skills
|
|
self.religion = agent.religion
|
|
self.diplomacy = agent.diplomacy
|
|
|
|
# Load thresholds from config
|
|
config = _get_ai_config()
|
|
self.CRITICAL_THRESHOLD = config.critical_threshold
|
|
self.LOW_ENERGY_THRESHOLD = config.low_energy_threshold
|
|
|
|
# Personality-adjusted values
|
|
self.WEALTH_DESIRE = self.p.wealth_desire
|
|
|
|
economy = _get_economy_config()
|
|
base_threshold = getattr(economy, 'buy_efficiency_threshold', 0.7) if economy else 0.7
|
|
self.BUY_EFFICIENCY_THRESHOLD = base_threshold / self.p.price_sensitivity
|
|
|
|
base_target = getattr(economy, 'min_wealth_target', 50) if economy else 50
|
|
self.MIN_WEALTH_TARGET = int(base_target * (0.5 + self.p.wealth_desire))
|
|
|
|
hoarding_mult = 0.5 + self.p.hoarding_rate
|
|
self.MIN_WATER_STOCK = max(1, int(self.BASE_WATER_STOCK * hoarding_mult))
|
|
self.MIN_FOOD_STOCK = max(2, int(self.BASE_FOOD_STOCK * hoarding_mult))
|
|
self.MIN_WOOD_STOCK = max(1, int(self.BASE_WOOD_STOCK * hoarding_mult))
|
|
|
|
self.is_trader = self.p.trade_preference > 1.3 and self.p.market_affinity > 0.5
|
|
|
|
# NEW: Check for special roles
|
|
self.is_religious = self.religion.is_religious and self.agent.stats.faith > 40
|
|
self.is_zealot = self.religion.is_zealot
|
|
self.is_diplomat = self.diplomacy.diplomacy_skill > 0.6
|
|
|
|
@property
|
|
def is_evening(self) -> bool:
|
|
return self.step_in_day >= self.day_steps - 1
|
|
|
|
@property
|
|
def is_late_day(self) -> bool:
|
|
return self.step_in_day >= self.day_steps // 2
|
|
|
|
def _get_resource_fair_value(self, resource_type: ResourceType) -> int:
|
|
"""Calculate the 'fair value' of a resource."""
|
|
energy_cost = get_energy_cost(resource_type)
|
|
base_value = max(1, int(energy_cost * self.ENERGY_TO_MONEY_RATIO))
|
|
|
|
# Oil and fuel have special pricing
|
|
economy = _get_economy_config()
|
|
if economy:
|
|
if resource_type == ResourceType.OIL:
|
|
return economy.oil_base_price
|
|
elif resource_type == ResourceType.FUEL:
|
|
return economy.fuel_base_price
|
|
|
|
return base_value
|
|
|
|
def _is_good_buy(self, resource_type: ResourceType, price: int) -> bool:
|
|
fair_value = self._get_resource_fair_value(resource_type)
|
|
return price <= fair_value * self.BUY_EFFICIENCY_THRESHOLD
|
|
|
|
def _is_wealthy(self) -> bool:
|
|
return self.agent.money >= self.MIN_WEALTH_TARGET
|
|
|
|
def decide(self) -> AIDecision:
|
|
"""Make a decision based on survival, personality, religion, and diplomacy."""
|
|
# Priority 1: Critical survival needs
|
|
decision = self._check_critical_needs()
|
|
if decision:
|
|
return decision
|
|
|
|
# Priority 2: Proactive survival
|
|
decision = self._check_proactive_needs()
|
|
if decision:
|
|
return decision
|
|
|
|
# Priority 3: Basic stockpiling - ensure we have food and water before other activities
|
|
decision = self._check_basic_stockpile()
|
|
if decision:
|
|
return decision
|
|
|
|
# Priority 4: Religious actions (for religious agents) - only if well-fed
|
|
if self.is_religious:
|
|
decision = self._check_religious_actions()
|
|
if decision:
|
|
return decision
|
|
|
|
# Priority 5: Diplomatic actions (all non-neutral faction members)
|
|
# War can be declared by any aggressive agent, diplomacy by skilled ones
|
|
if self.diplomacy.faction != FactionType.NEUTRAL:
|
|
decision = self._check_diplomatic_actions()
|
|
if decision:
|
|
return decision
|
|
|
|
# Priority 6: Trader behavior
|
|
if self.is_trader:
|
|
decision = self._do_trader_behavior()
|
|
if decision:
|
|
return decision
|
|
|
|
# Priority 7: Oil industry (if have oil skills or near oil field)
|
|
decision = self._check_oil_industry()
|
|
if decision:
|
|
return decision
|
|
|
|
# Priority 8: Price adjustment
|
|
decision = self._check_price_adjustments()
|
|
if decision:
|
|
return decision
|
|
|
|
# Priority 9: Smart shopping
|
|
if random.random() < self.p.market_affinity:
|
|
decision = self._check_market_opportunities()
|
|
if decision:
|
|
return decision
|
|
|
|
# Priority 10: Craft clothes
|
|
decision = self._check_clothes_crafting()
|
|
if decision:
|
|
return decision
|
|
|
|
# Priority 11: Energy management
|
|
decision = self._check_energy()
|
|
if decision:
|
|
return decision
|
|
|
|
# Priority 12: Economic activities
|
|
if random.random() > self.p.hoarding_rate * 0.5:
|
|
decision = self._check_economic()
|
|
if decision:
|
|
return decision
|
|
|
|
# Priority 13: Routine survival work
|
|
return self._do_survival_work()
|
|
|
|
def _check_basic_stockpile(self) -> Optional[AIDecision]:
|
|
"""Ensure basic food and water supplies before other activities."""
|
|
water_count = self.agent.get_resource_count(ResourceType.WATER)
|
|
meat_count = self.agent.get_resource_count(ResourceType.MEAT)
|
|
berry_count = self.agent.get_resource_count(ResourceType.BERRIES)
|
|
food_count = meat_count + berry_count
|
|
|
|
# Minimum stockpile requirements (more aggressive than hoarding-adjusted values)
|
|
MIN_WATER = 2
|
|
MIN_FOOD = 3
|
|
|
|
# Check if we're dangerously low on resources
|
|
needs_water = water_count < MIN_WATER
|
|
needs_food = food_count < MIN_FOOD
|
|
|
|
if not needs_water and not needs_food:
|
|
return None
|
|
|
|
# Prioritize water since thirst is more urgent
|
|
if needs_water:
|
|
water_config = ACTION_CONFIG[ActionType.GET_WATER]
|
|
if self.agent.stats.can_work(abs(water_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.GET_WATER,
|
|
target_resource=ResourceType.WATER,
|
|
reason=f"Stockpiling water ({water_count} < {MIN_WATER})",
|
|
)
|
|
|
|
# Then food
|
|
if needs_food:
|
|
# Try to gather berries first (cheaper energy cost)
|
|
gather_config = ACTION_CONFIG[ActionType.GATHER]
|
|
if self.agent.stats.can_work(abs(gather_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.GATHER,
|
|
target_resource=ResourceType.BERRIES,
|
|
reason=f"Stockpiling food ({food_count} < {MIN_FOOD})",
|
|
)
|
|
|
|
# If can afford hunting and have energy
|
|
hunt_config = ACTION_CONFIG[ActionType.HUNT]
|
|
if self.agent.stats.can_work(abs(hunt_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.HUNT,
|
|
target_resource=ResourceType.MEAT,
|
|
reason=f"Stockpiling food by hunting ({food_count} < {MIN_FOOD})",
|
|
)
|
|
|
|
return None
|
|
|
|
def _check_religious_actions(self) -> Optional[AIDecision]:
|
|
"""Check if agent should perform religious actions."""
|
|
stats = self.agent.stats
|
|
|
|
# Low faith - should pray
|
|
if stats.faith < stats.MAX_FAITH * self.LOW_FAITH_THRESHOLD:
|
|
pray_config = ACTION_CONFIG[ActionType.PRAY]
|
|
if stats.can_work(abs(pray_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.PRAY,
|
|
reason=f"Low faith ({stats.faith}): praying at temple",
|
|
)
|
|
|
|
# Zealots want to preach and convert (but not too often to preserve energy)
|
|
if self.is_zealot:
|
|
if random.random() < 0.15: # Reduced from 30% to 15% to conserve energy
|
|
preach_config = ACTION_CONFIG[ActionType.PREACH]
|
|
if stats.can_work(abs(preach_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.PREACH,
|
|
reason=f"Zealot spreading the word of {self.religion.religion.value}",
|
|
)
|
|
|
|
# Religious agents pray occasionally to maintain faith
|
|
if stats.faith < stats.MAX_FAITH * self.HIGH_FAITH_THRESHOLD:
|
|
if random.random() < 0.15: # 15% chance
|
|
pray_config = ACTION_CONFIG[ActionType.PRAY]
|
|
if stats.can_work(abs(pray_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.PRAY,
|
|
reason="Maintaining faith through prayer",
|
|
)
|
|
|
|
return None
|
|
|
|
def _check_diplomatic_actions(self) -> Optional[AIDecision]:
|
|
"""Check if agent should perform diplomatic actions."""
|
|
faction_relations = get_faction_relations()
|
|
my_faction = self.diplomacy.faction
|
|
|
|
if my_faction == FactionType.NEUTRAL:
|
|
return None
|
|
|
|
# Check each other faction
|
|
for other_faction in FactionType:
|
|
if other_faction == my_faction or other_faction == FactionType.NEUTRAL:
|
|
continue
|
|
|
|
status = faction_relations.get_status(my_faction, other_faction)
|
|
|
|
# At war and exhausted - try to make peace
|
|
if status == DiplomaticStatus.WAR:
|
|
exhaustion = faction_relations.war_exhaustion.get(my_faction, 0)
|
|
if exhaustion > 30 and random.random() < 0.2:
|
|
peace_config = ACTION_CONFIG[ActionType.MAKE_PEACE]
|
|
if self.agent.stats.can_work(abs(peace_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.MAKE_PEACE,
|
|
target_faction=other_faction,
|
|
reason=f"War exhaustion ({exhaustion}): seeking peace with {other_faction.value}",
|
|
)
|
|
|
|
# Very hostile and aggressive - might declare war (but less frequently)
|
|
elif status == DiplomaticStatus.HOSTILE:
|
|
# War probability scales with aggression (reduced to promote stability)
|
|
# Base 2% chance + up to 8% from aggression
|
|
war_prob = 0.08 + (self.diplomacy.aggression * 0.08)
|
|
if self.diplomacy.aggression > 0.25 and random.random() < war_prob:
|
|
war_cfg = ACTION_CONFIG[ActionType.DECLARE_WAR]
|
|
if self.agent.stats.can_work(abs(war_cfg.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.DECLARE_WAR,
|
|
target_faction=other_faction,
|
|
reason=f"Tensions: declaring war on {other_faction.value}",
|
|
)
|
|
|
|
# Cold relations - try to improve
|
|
elif status in (DiplomaticStatus.COLD, DiplomaticStatus.NEUTRAL):
|
|
if self.diplomacy.diplomacy_skill > 0.6 and random.random() < 0.1:
|
|
neg_cfg = ACTION_CONFIG[ActionType.NEGOTIATE]
|
|
if self.agent.stats.can_work(abs(neg_cfg.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.NEGOTIATE,
|
|
target_faction=other_faction,
|
|
reason=f"Improving relations: {other_faction.value}",
|
|
)
|
|
|
|
return None
|
|
|
|
def _check_oil_industry(self) -> Optional[AIDecision]:
|
|
"""Check if agent should work in oil industry."""
|
|
stats = self.agent.stats
|
|
|
|
# Check if we have oil to refine
|
|
if self.agent.has_resource(ResourceType.OIL, 2):
|
|
refine_config = ACTION_CONFIG[ActionType.REFINE]
|
|
if stats.can_work(abs(refine_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.REFINE,
|
|
target_resource=ResourceType.FUEL,
|
|
reason="Refining oil into fuel",
|
|
)
|
|
|
|
# Check if we should burn fuel for heat/energy
|
|
if self.agent.has_resource(ResourceType.FUEL):
|
|
if stats.heat < stats.MAX_HEAT * 0.5 or stats.energy < 20:
|
|
burn_config = ACTION_CONFIG[ActionType.BURN_FUEL]
|
|
if stats.can_work(abs(burn_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.BURN_FUEL,
|
|
target_resource=ResourceType.FUEL,
|
|
reason="Burning fuel for energy and heat",
|
|
)
|
|
|
|
# Consider drilling for oil (specialized or random chance)
|
|
oil_count = self.agent.get_resource_count(ResourceType.OIL)
|
|
|
|
# Oil is valuable - check if worth drilling
|
|
should_drill = False
|
|
|
|
# Mountaineers have oil bonus
|
|
if self.diplomacy.faction == FactionType.MOUNTAINEER:
|
|
should_drill = oil_count < 3 and random.random() < 0.3
|
|
# Already an oil worker
|
|
elif self.agent.actions_performed.get("drill_oil", 0) > 5:
|
|
should_drill = oil_count < 5 and random.random() < 0.4
|
|
# Random chance for anyone
|
|
elif random.random() < 0.05:
|
|
should_drill = True
|
|
|
|
if should_drill and self.agent.inventory_space() >= 2:
|
|
drill_config = ACTION_CONFIG[ActionType.DRILL_OIL]
|
|
if stats.can_work(abs(drill_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.DRILL_OIL,
|
|
target_resource=ResourceType.OIL,
|
|
reason="Drilling for oil",
|
|
)
|
|
|
|
return None
|
|
|
|
def _check_critical_needs(self) -> Optional[AIDecision]:
|
|
"""Check if any vital stat is critical."""
|
|
stats = self.agent.stats
|
|
|
|
if stats.thirst < stats.MAX_THIRST * self.CRITICAL_THRESHOLD:
|
|
return self._address_thirst(critical=True)
|
|
|
|
if stats.hunger < stats.MAX_HUNGER * self.CRITICAL_THRESHOLD:
|
|
return self._address_hunger(critical=True)
|
|
|
|
if stats.heat < stats.MAX_HEAT * self.CRITICAL_THRESHOLD:
|
|
return self._address_heat(critical=True)
|
|
|
|
return None
|
|
|
|
def _check_proactive_needs(self) -> Optional[AIDecision]:
|
|
"""Proactively address needs before critical."""
|
|
stats = self.agent.stats
|
|
|
|
if stats.thirst < stats.MAX_THIRST * self.LOW_THRESHOLD:
|
|
return self._address_thirst(critical=False)
|
|
|
|
if stats.hunger < stats.MAX_HUNGER * self.LOW_THRESHOLD:
|
|
return self._address_hunger(critical=False)
|
|
|
|
if stats.heat < stats.MAX_HEAT * self.HEAT_PROACTIVE_THRESHOLD:
|
|
decision = self._address_heat(critical=False)
|
|
if decision:
|
|
return decision
|
|
|
|
return None
|
|
|
|
def _do_trader_behavior(self) -> Optional[AIDecision]:
|
|
"""Trader-specific behavior: arbitrage and market operations."""
|
|
if self.agent.money < 20:
|
|
decision = self._try_to_sell(urgent=True)
|
|
if decision:
|
|
return decision
|
|
return None
|
|
|
|
# Look for arbitrage opportunities
|
|
best_deal = None
|
|
best_margin = 0
|
|
|
|
# Include oil and fuel in trading
|
|
trade_resources = [
|
|
ResourceType.MEAT, ResourceType.BERRIES, ResourceType.WATER,
|
|
ResourceType.WOOD, ResourceType.OIL, ResourceType.FUEL
|
|
]
|
|
|
|
for resource_type in trade_resources:
|
|
order = self.market.get_cheapest_order(resource_type)
|
|
if not order or order.seller_id == self.agent.id:
|
|
continue
|
|
|
|
fair_value = self._get_resource_fair_value(resource_type)
|
|
buy_price = order.price_per_unit
|
|
sell_modifier = get_trade_price_modifier(self.skills.trading, is_buying=False)
|
|
potential_sell_price = int(fair_value * sell_modifier * 1.1)
|
|
|
|
margin = potential_sell_price - buy_price
|
|
if margin > best_margin and self.agent.money >= buy_price:
|
|
best_margin = margin
|
|
best_deal = (resource_type, order, buy_price, potential_sell_price)
|
|
|
|
if best_deal and best_margin >= 2:
|
|
resource_type, order, buy_price, sell_price = best_deal
|
|
safe_price = max(1, buy_price)
|
|
quantity = min(3, self.agent.inventory_space(), order.quantity,
|
|
self.agent.money // safe_price)
|
|
|
|
if quantity > 0:
|
|
return AIDecision(
|
|
action=ActionType.TRADE,
|
|
target_resource=resource_type,
|
|
order_id=order.id,
|
|
quantity=quantity,
|
|
price=buy_price,
|
|
reason=f"Trader: buying {resource_type.value} @ {buy_price}c",
|
|
)
|
|
|
|
decision = self._try_trader_sell()
|
|
if decision:
|
|
return decision
|
|
|
|
decision = self._check_price_adjustments()
|
|
if decision:
|
|
return decision
|
|
|
|
return None
|
|
|
|
def _try_trader_sell(self) -> Optional[AIDecision]:
|
|
"""Trader sells inventory at markup."""
|
|
for resource in self.agent.inventory:
|
|
if resource.type == ResourceType.CLOTHES:
|
|
continue
|
|
|
|
if resource.quantity <= 1:
|
|
continue
|
|
|
|
fair_value = self._get_resource_fair_value(resource.type)
|
|
sell_modifier = get_trade_price_modifier(self.skills.trading, is_buying=False)
|
|
|
|
signal = self.market.get_market_signal(resource.type)
|
|
if signal == "sell":
|
|
price = int(fair_value * sell_modifier * 1.4)
|
|
else:
|
|
price = int(fair_value * sell_modifier * 1.15)
|
|
|
|
my_orders = [o for o in self.market.get_orders_by_seller(self.agent.id)
|
|
if o.resource_type == resource.type]
|
|
if my_orders:
|
|
continue
|
|
|
|
quantity = resource.quantity - 1
|
|
|
|
return AIDecision(
|
|
action=ActionType.TRADE,
|
|
target_resource=resource.type,
|
|
quantity=quantity,
|
|
price=price,
|
|
reason=f"Trader: selling {resource.type.value} @ {price}c",
|
|
)
|
|
|
|
return None
|
|
|
|
def _check_price_adjustments(self) -> Optional[AIDecision]:
|
|
"""Check if we should adjust prices on orders."""
|
|
my_orders = self.market.get_orders_by_seller(self.agent.id)
|
|
if not my_orders:
|
|
return None
|
|
|
|
for order in my_orders:
|
|
resource_type = order.resource_type
|
|
signal = self.market.get_market_signal(resource_type)
|
|
current_price = order.price_per_unit
|
|
fair_value = self._get_resource_fair_value(resource_type)
|
|
|
|
if signal == "sell" and order.can_raise_price(self.current_turn, min_turns=3):
|
|
new_price = min(
|
|
int(current_price * 1.25),
|
|
int(fair_value * self.MAX_PRICE_MARKUP)
|
|
)
|
|
if new_price > current_price:
|
|
return AIDecision(
|
|
action=ActionType.TRADE,
|
|
target_resource=resource_type,
|
|
adjust_order_id=order.id,
|
|
new_price=new_price,
|
|
reason=f"Scarcity: raising {resource_type.value} price to {new_price}c",
|
|
)
|
|
|
|
if order.turns_without_sale >= 5:
|
|
lowest_order = self.market.get_cheapest_order(resource_type)
|
|
if lowest_order and lowest_order.id != order.id:
|
|
new_price = max(
|
|
lowest_order.price_per_unit - 1,
|
|
int(fair_value * self.MIN_PRICE_DISCOUNT)
|
|
)
|
|
else:
|
|
new_price = max(
|
|
int(current_price * 0.85),
|
|
int(fair_value * self.MIN_PRICE_DISCOUNT)
|
|
)
|
|
|
|
if new_price < current_price:
|
|
return AIDecision(
|
|
action=ActionType.TRADE,
|
|
target_resource=resource_type,
|
|
adjust_order_id=order.id,
|
|
new_price=new_price,
|
|
reason=f"Stale: lowering {resource_type.value} to {new_price}c",
|
|
)
|
|
|
|
return None
|
|
|
|
def _check_market_opportunities(self) -> Optional[AIDecision]:
|
|
"""Look for good buying opportunities."""
|
|
if self.agent.money < 10:
|
|
return None
|
|
|
|
shopping_list = []
|
|
|
|
for resource_type in [ResourceType.WATER, ResourceType.BERRIES, ResourceType.MEAT,
|
|
ResourceType.WOOD, ResourceType.OIL, ResourceType.FUEL]:
|
|
order = self.market.get_cheapest_order(resource_type)
|
|
if not order or order.seller_id == self.agent.id:
|
|
continue
|
|
|
|
if order.price_per_unit <= 0:
|
|
continue
|
|
|
|
if self.agent.money < order.price_per_unit:
|
|
continue
|
|
|
|
fair_value = self._get_resource_fair_value(resource_type)
|
|
is_good_deal = self._is_good_buy(resource_type, order.price_per_unit)
|
|
|
|
current_stock = self.agent.get_resource_count(resource_type)
|
|
need_score = 0
|
|
|
|
if resource_type == ResourceType.WATER:
|
|
need_score = max(0, self.MIN_WATER_STOCK - current_stock) * 3
|
|
elif resource_type in [ResourceType.BERRIES, ResourceType.MEAT]:
|
|
food_stock = (self.agent.get_resource_count(ResourceType.BERRIES) +
|
|
self.agent.get_resource_count(ResourceType.MEAT))
|
|
need_score = max(0, self.MIN_FOOD_STOCK - food_stock) * 2
|
|
elif resource_type == ResourceType.WOOD:
|
|
need_score = max(0, self.MIN_WOOD_STOCK - current_stock) * 1
|
|
elif resource_type in [ResourceType.OIL, ResourceType.FUEL]:
|
|
# Oil and fuel are valuable trading commodities
|
|
need_score = 1 if is_good_deal else 0
|
|
|
|
if is_good_deal:
|
|
price = max(1, order.price_per_unit)
|
|
efficiency_score = fair_value / price
|
|
total_score = need_score + efficiency_score * 2
|
|
shopping_list.append((resource_type, order, total_score))
|
|
elif need_score > 0 and self._is_wealthy():
|
|
total_score = need_score * 0.5
|
|
shopping_list.append((resource_type, order, total_score))
|
|
|
|
if not shopping_list:
|
|
return None
|
|
|
|
shopping_list.sort(key=lambda x: x[2], reverse=True)
|
|
resource_type, order, score = shopping_list[0]
|
|
|
|
if score < 1:
|
|
return None
|
|
|
|
price = max(1, order.price_per_unit)
|
|
can_afford = self.agent.money // price
|
|
space = self.agent.inventory_space()
|
|
want_quantity = min(2, can_afford, space, order.quantity)
|
|
|
|
if want_quantity <= 0:
|
|
return None
|
|
|
|
return AIDecision(
|
|
action=ActionType.TRADE,
|
|
target_resource=resource_type,
|
|
order_id=order.id,
|
|
quantity=want_quantity,
|
|
price=order.price_per_unit,
|
|
reason=f"Good deal: {resource_type.value} @ {order.price_per_unit}c",
|
|
)
|
|
|
|
def _check_clothes_crafting(self) -> Optional[AIDecision]:
|
|
"""Check if we should craft clothes."""
|
|
if self.agent.has_clothes():
|
|
return None
|
|
|
|
if not self.agent.has_resource(ResourceType.HIDE):
|
|
return None
|
|
|
|
weave_config = ACTION_CONFIG[ActionType.WEAVE]
|
|
if not self.agent.stats.can_work(abs(weave_config.energy_cost)):
|
|
return None
|
|
|
|
stats = self.agent.stats
|
|
if (stats.thirst < stats.MAX_THIRST * self.LOW_THRESHOLD or
|
|
stats.hunger < stats.MAX_HUNGER * self.LOW_THRESHOLD):
|
|
return None
|
|
|
|
return AIDecision(
|
|
action=ActionType.WEAVE,
|
|
target_resource=ResourceType.CLOTHES,
|
|
reason="Crafting clothes for heat protection",
|
|
)
|
|
|
|
def _address_thirst(self, critical: bool = False) -> AIDecision:
|
|
"""Address thirst."""
|
|
prefix = "Critical" if critical else "Low"
|
|
|
|
if self.agent.has_resource(ResourceType.WATER):
|
|
return AIDecision(
|
|
action=ActionType.CONSUME,
|
|
target_resource=ResourceType.WATER,
|
|
reason=f"{prefix} thirst: consuming water",
|
|
)
|
|
|
|
water_order = self.market.get_cheapest_order(ResourceType.WATER)
|
|
if water_order and water_order.seller_id != self.agent.id:
|
|
if self.agent.money >= water_order.price_per_unit:
|
|
fair_value = self._get_resource_fair_value(ResourceType.WATER)
|
|
should_buy = (
|
|
self._is_good_buy(ResourceType.WATER, water_order.price_per_unit) or
|
|
critical or
|
|
(self._is_wealthy() and water_order.price_per_unit <= fair_value * 1.5)
|
|
)
|
|
|
|
if should_buy:
|
|
return AIDecision(
|
|
action=ActionType.TRADE,
|
|
target_resource=ResourceType.WATER,
|
|
order_id=water_order.id,
|
|
quantity=1,
|
|
price=water_order.price_per_unit,
|
|
reason=f"{prefix} thirst: buying water @ {water_order.price_per_unit}c",
|
|
)
|
|
|
|
water_config = ACTION_CONFIG[ActionType.GET_WATER]
|
|
if self.agent.stats.can_work(abs(water_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.GET_WATER,
|
|
target_resource=ResourceType.WATER,
|
|
reason=f"{prefix} thirst: getting water",
|
|
)
|
|
|
|
if self.agent.has_resource(ResourceType.BERRIES):
|
|
return AIDecision(
|
|
action=ActionType.CONSUME,
|
|
target_resource=ResourceType.BERRIES,
|
|
reason=f"{prefix} thirst: consuming berries (emergency)",
|
|
)
|
|
|
|
return AIDecision(
|
|
action=ActionType.REST,
|
|
reason=f"{prefix} thirst: too tired, resting",
|
|
)
|
|
|
|
def _address_hunger(self, critical: bool = False) -> AIDecision:
|
|
"""Address hunger."""
|
|
prefix = "Critical" if critical else "Low"
|
|
|
|
if self.agent.has_resource(ResourceType.MEAT):
|
|
return AIDecision(
|
|
action=ActionType.CONSUME,
|
|
target_resource=ResourceType.MEAT,
|
|
reason=f"{prefix} hunger: consuming meat",
|
|
)
|
|
|
|
if self.agent.has_resource(ResourceType.BERRIES):
|
|
return AIDecision(
|
|
action=ActionType.CONSUME,
|
|
target_resource=ResourceType.BERRIES,
|
|
reason=f"{prefix} hunger: consuming berries",
|
|
)
|
|
|
|
for resource_type in [ResourceType.MEAT, ResourceType.BERRIES]:
|
|
order = self.market.get_cheapest_order(resource_type)
|
|
if order and order.seller_id != self.agent.id and self.agent.money >= order.price_per_unit:
|
|
fair_value = self._get_resource_fair_value(resource_type)
|
|
should_buy = (
|
|
self._is_good_buy(resource_type, order.price_per_unit) or
|
|
critical or
|
|
(self._is_wealthy() and order.price_per_unit <= fair_value * 1.5)
|
|
)
|
|
|
|
if should_buy:
|
|
return AIDecision(
|
|
action=ActionType.TRADE,
|
|
target_resource=resource_type,
|
|
order_id=order.id,
|
|
quantity=1,
|
|
price=order.price_per_unit,
|
|
reason=f"{prefix} hunger: buying {resource_type.value}",
|
|
)
|
|
|
|
gather_config = ACTION_CONFIG[ActionType.GATHER]
|
|
if self.agent.stats.can_work(abs(gather_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.GATHER,
|
|
target_resource=ResourceType.BERRIES,
|
|
reason=f"{prefix} hunger: gathering berries",
|
|
)
|
|
|
|
return AIDecision(
|
|
action=ActionType.REST,
|
|
reason=f"{prefix} hunger: too tired, resting",
|
|
)
|
|
|
|
def _address_heat(self, critical: bool = False) -> Optional[AIDecision]:
|
|
"""Address heat."""
|
|
prefix = "Critical" if critical else "Low"
|
|
|
|
# Fuel is better than wood for heat
|
|
if self.agent.has_resource(ResourceType.FUEL):
|
|
burn_config = ACTION_CONFIG[ActionType.BURN_FUEL]
|
|
if self.agent.stats.can_work(abs(burn_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.BURN_FUEL,
|
|
target_resource=ResourceType.FUEL,
|
|
reason=f"{prefix} heat: burning fuel",
|
|
)
|
|
|
|
if self.agent.has_resource(ResourceType.WOOD):
|
|
fire_config = ACTION_CONFIG[ActionType.BUILD_FIRE]
|
|
if self.agent.stats.can_work(abs(fire_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.BUILD_FIRE,
|
|
target_resource=ResourceType.WOOD,
|
|
reason=f"{prefix} heat: building fire",
|
|
)
|
|
|
|
# Try to buy fuel or wood
|
|
for resource_type in [ResourceType.FUEL, ResourceType.WOOD]:
|
|
cheapest = self.market.get_cheapest_order(resource_type)
|
|
if cheapest and cheapest.seller_id != self.agent.id:
|
|
if self.agent.money >= cheapest.price_per_unit:
|
|
fair_value = self._get_resource_fair_value(resource_type)
|
|
should_buy = (
|
|
self._is_good_buy(resource_type, cheapest.price_per_unit) or
|
|
critical or
|
|
(self._is_wealthy() and cheapest.price_per_unit <= fair_value * 1.5)
|
|
)
|
|
|
|
if should_buy:
|
|
return AIDecision(
|
|
action=ActionType.TRADE,
|
|
target_resource=resource_type,
|
|
order_id=cheapest.id,
|
|
quantity=1,
|
|
price=cheapest.price_per_unit,
|
|
reason=f"{prefix} heat: buying {resource_type.value}",
|
|
)
|
|
|
|
chop_config = ACTION_CONFIG[ActionType.CHOP_WOOD]
|
|
if self.agent.stats.can_work(abs(chop_config.energy_cost)):
|
|
return AIDecision(
|
|
action=ActionType.CHOP_WOOD,
|
|
target_resource=ResourceType.WOOD,
|
|
reason=f"{prefix} heat: chopping wood",
|
|
)
|
|
|
|
if not critical:
|
|
return None
|
|
|
|
return AIDecision(
|
|
action=ActionType.REST,
|
|
reason=f"{prefix} heat: too tired, resting",
|
|
)
|
|
|
|
def _check_energy(self) -> Optional[AIDecision]:
|
|
"""Check if energy management is needed."""
|
|
stats = self.agent.stats
|
|
|
|
if stats.energy < self.LOW_ENERGY_THRESHOLD:
|
|
return AIDecision(
|
|
action=ActionType.REST,
|
|
reason=f"Energy critically low ({stats.energy}), must rest",
|
|
)
|
|
|
|
if self.is_evening and stats.energy < self.REST_ENERGY_THRESHOLD:
|
|
has_supplies = (
|
|
self.agent.get_resource_count(ResourceType.WATER) >= 1 and
|
|
(self.agent.get_resource_count(ResourceType.MEAT) >= 1 or
|
|
self.agent.get_resource_count(ResourceType.BERRIES) >= 2)
|
|
)
|
|
if has_supplies:
|
|
return AIDecision(
|
|
action=ActionType.REST,
|
|
reason="Evening: resting for night",
|
|
)
|
|
|
|
return None
|
|
|
|
def _check_economic(self) -> Optional[AIDecision]:
|
|
"""Economic activities."""
|
|
decision = self._try_proactive_sell()
|
|
if decision:
|
|
return decision
|
|
|
|
if self.agent.inventory_space() <= 2:
|
|
decision = self._try_to_sell(urgent=True)
|
|
if decision:
|
|
return decision
|
|
|
|
return None
|
|
|
|
def _try_proactive_sell(self) -> Optional[AIDecision]:
|
|
"""Proactively sell when market conditions are good."""
|
|
if self._is_wealthy() and self.agent.inventory_space() > 3:
|
|
return None
|
|
|
|
if random.random() < self.p.hoarding_rate * 0.7:
|
|
return None
|
|
|
|
base_min = {
|
|
ResourceType.WATER: 2,
|
|
ResourceType.MEAT: 1,
|
|
ResourceType.BERRIES: 2,
|
|
ResourceType.WOOD: 2,
|
|
ResourceType.HIDE: 0,
|
|
ResourceType.OIL: 0,
|
|
ResourceType.FUEL: 1,
|
|
}
|
|
|
|
hoarding_mult = 0.5 + self.p.hoarding_rate
|
|
survival_minimums = {k: int(v * hoarding_mult) for k, v in base_min.items()}
|
|
|
|
best_opportunity = None
|
|
best_score = 0
|
|
|
|
for resource in self.agent.inventory:
|
|
if resource.type == ResourceType.CLOTHES:
|
|
continue
|
|
|
|
min_keep = survival_minimums.get(resource.type, 1)
|
|
excess = resource.quantity - min_keep
|
|
|
|
if excess <= 0:
|
|
continue
|
|
|
|
signal = self.market.get_market_signal(resource.type)
|
|
fair_value = self._get_resource_fair_value(resource.type)
|
|
sell_modifier = get_trade_price_modifier(self.skills.trading, is_buying=False)
|
|
|
|
if signal == "sell":
|
|
price = int(fair_value * 1.3 * sell_modifier)
|
|
score = 3 + excess
|
|
elif signal == "hold":
|
|
price = int(fair_value * sell_modifier)
|
|
score = 1 + excess * 0.5
|
|
else:
|
|
cheapest = self.market.get_cheapest_order(resource.type)
|
|
if cheapest and cheapest.seller_id != self.agent.id:
|
|
price = max(1, cheapest.price_per_unit - 1)
|
|
else:
|
|
price = int(fair_value * 0.8 * sell_modifier)
|
|
score = 0.5
|
|
|
|
# Oil and fuel have higher sell priority
|
|
if resource.type in [ResourceType.OIL, ResourceType.FUEL]:
|
|
score *= 1.5
|
|
|
|
score *= (0.7 + self.p.wealth_desire * 0.6)
|
|
|
|
if score > best_score:
|
|
best_score = score
|
|
best_opportunity = (resource.type, min(excess, 3), price)
|
|
|
|
if best_opportunity and best_score >= 1:
|
|
resource_type, quantity, price = best_opportunity
|
|
return AIDecision(
|
|
action=ActionType.TRADE,
|
|
target_resource=resource_type,
|
|
quantity=quantity,
|
|
price=price,
|
|
reason=f"Selling {resource_type.value} @ {price}c",
|
|
)
|
|
|
|
return None
|
|
|
|
def _try_to_sell(self, urgent: bool = False) -> Optional[AIDecision]:
|
|
"""Sell excess resources."""
|
|
survival_minimums = {
|
|
ResourceType.WATER: 2 if urgent else 3,
|
|
ResourceType.MEAT: 1 if urgent else 2,
|
|
ResourceType.BERRIES: 2 if urgent else 3,
|
|
ResourceType.WOOD: 1 if urgent else 2,
|
|
ResourceType.HIDE: 0,
|
|
ResourceType.OIL: 0,
|
|
ResourceType.FUEL: 0,
|
|
}
|
|
|
|
for resource in self.agent.inventory:
|
|
if resource.type == ResourceType.CLOTHES:
|
|
continue
|
|
|
|
min_keep = survival_minimums.get(resource.type, 1)
|
|
if resource.quantity > min_keep:
|
|
quantity_to_sell = resource.quantity - min_keep
|
|
price = self._calculate_sell_price(resource.type)
|
|
reason = "Urgent: clearing inventory" if urgent else f"Selling excess {resource.type.value}"
|
|
return AIDecision(
|
|
action=ActionType.TRADE,
|
|
target_resource=resource.type,
|
|
quantity=quantity_to_sell,
|
|
price=price,
|
|
reason=reason,
|
|
)
|
|
return None
|
|
|
|
def _calculate_sell_price(self, resource_type: ResourceType) -> int:
|
|
"""Calculate sell price."""
|
|
fair_value = self._get_resource_fair_value(resource_type)
|
|
suggested = self.market.get_suggested_price(resource_type, fair_value)
|
|
|
|
cheapest = self.market.get_cheapest_order(resource_type)
|
|
if cheapest and cheapest.seller_id != self.agent.id:
|
|
signal = self.market.get_market_signal(resource_type)
|
|
if signal != "sell":
|
|
suggested = min(suggested, cheapest.price_per_unit)
|
|
|
|
return max(1, suggested)
|
|
|
|
def _do_survival_work(self) -> AIDecision:
|
|
"""Perform work based on survival needs and personality."""
|
|
stats = self.agent.stats
|
|
|
|
water_count = self.agent.get_resource_count(ResourceType.WATER)
|
|
meat_count = self.agent.get_resource_count(ResourceType.MEAT)
|
|
berry_count = self.agent.get_resource_count(ResourceType.BERRIES)
|
|
wood_count = self.agent.get_resource_count(ResourceType.WOOD)
|
|
food_count = meat_count + berry_count
|
|
|
|
heat_urgency = 1 - (stats.heat / stats.MAX_HEAT)
|
|
|
|
def get_resource_decision(resource_type: ResourceType, gather_action: ActionType, reason: str) -> AIDecision:
|
|
if random.random() < self.p.market_affinity:
|
|
order = self.market.get_cheapest_order(resource_type)
|
|
if order and order.seller_id != self.agent.id and self.agent.money >= order.price_per_unit:
|
|
buy_modifier = get_trade_price_modifier(self.skills.trading, is_buying=True)
|
|
effective_price = order.price_per_unit
|
|
|
|
if self._is_good_buy(resource_type, effective_price):
|
|
return AIDecision(
|
|
action=ActionType.TRADE,
|
|
target_resource=resource_type,
|
|
order_id=order.id,
|
|
quantity=1,
|
|
price=order.price_per_unit,
|
|
reason=f"{reason} (buying)",
|
|
)
|
|
|
|
config = ACTION_CONFIG[gather_action]
|
|
if self.agent.stats.can_work(abs(config.energy_cost)):
|
|
# Apply religion bonus
|
|
religion_bonus = get_religion_action_bonus(
|
|
self.religion.religion, gather_action.value
|
|
)
|
|
return AIDecision(
|
|
action=gather_action,
|
|
target_resource=resource_type,
|
|
reason=f"{reason} (gathering)",
|
|
)
|
|
return None
|
|
|
|
if water_count < self.MIN_WATER_STOCK:
|
|
decision = get_resource_decision(
|
|
ResourceType.WATER,
|
|
ActionType.GET_WATER,
|
|
f"Stocking water ({water_count} < {self.MIN_WATER_STOCK})"
|
|
)
|
|
if decision:
|
|
return decision
|
|
|
|
if wood_count < self.MIN_WOOD_STOCK and heat_urgency > 0.3:
|
|
decision = get_resource_decision(
|
|
ResourceType.WOOD,
|
|
ActionType.CHOP_WOOD,
|
|
f"Stocking wood ({wood_count} < {self.MIN_WOOD_STOCK})"
|
|
)
|
|
if decision:
|
|
return decision
|
|
|
|
if food_count < self.MIN_FOOD_STOCK:
|
|
hunt_config = ACTION_CONFIG[ActionType.HUNT]
|
|
can_hunt = stats.energy >= abs(hunt_config.energy_cost) + 5
|
|
|
|
hunt_score = self.p.hunt_preference * self.p.risk_tolerance
|
|
gather_score = self.p.gather_preference * (1.5 - self.p.risk_tolerance)
|
|
|
|
total = hunt_score + gather_score
|
|
hunt_prob = hunt_score / total if total > 0 else 0.3
|
|
|
|
if meat_count == 0:
|
|
hunt_prob = min(0.8, hunt_prob + 0.3)
|
|
|
|
prefer_hunt = can_hunt and random.random() < hunt_prob
|
|
|
|
if prefer_hunt:
|
|
decision = get_resource_decision(
|
|
ResourceType.MEAT,
|
|
ActionType.HUNT,
|
|
f"Hunting ({food_count} < {self.MIN_FOOD_STOCK})"
|
|
)
|
|
if decision:
|
|
return decision
|
|
|
|
decision = get_resource_decision(
|
|
ResourceType.BERRIES,
|
|
ActionType.GATHER,
|
|
f"Stocking food ({food_count} < {self.MIN_FOOD_STOCK})"
|
|
)
|
|
if decision:
|
|
return decision
|
|
|
|
# Evening preparation
|
|
if self.is_late_day:
|
|
if water_count < self.MIN_WATER_STOCK + 1:
|
|
decision = get_resource_decision(ResourceType.WATER, ActionType.GET_WATER, "Evening: stocking water")
|
|
if decision:
|
|
return decision
|
|
if food_count < self.MIN_FOOD_STOCK + 1:
|
|
decision = get_resource_decision(ResourceType.BERRIES, ActionType.GATHER, "Evening: stocking food")
|
|
if decision:
|
|
return decision
|
|
if wood_count < self.MIN_WOOD_STOCK + 1:
|
|
decision = get_resource_decision(ResourceType.WOOD, ActionType.CHOP_WOOD, "Evening: stocking wood")
|
|
if decision:
|
|
return decision
|
|
|
|
# Default work
|
|
needs = []
|
|
|
|
if water_count < self.MIN_WATER_STOCK + 2:
|
|
needs.append((ResourceType.WATER, ActionType.GET_WATER, "Getting water", 2.0))
|
|
|
|
if food_count < self.MIN_FOOD_STOCK + 2:
|
|
needs.append((ResourceType.BERRIES, ActionType.GATHER, "Gathering berries",
|
|
2.0 * self.p.gather_preference))
|
|
|
|
hunt_config = ACTION_CONFIG[ActionType.HUNT]
|
|
if stats.energy >= abs(hunt_config.energy_cost) + 3:
|
|
hunt_weight = 2.0 * self.p.hunt_preference * self.p.risk_tolerance
|
|
needs.append((ResourceType.MEAT, ActionType.HUNT, "Hunting", hunt_weight))
|
|
|
|
if wood_count < self.MIN_WOOD_STOCK + 2:
|
|
needs.append((ResourceType.WOOD, ActionType.CHOP_WOOD, "Chopping wood",
|
|
1.0 * self.p.woodcut_preference))
|
|
|
|
if not needs:
|
|
if self.agent.inventory_space() <= 4:
|
|
decision = self._try_proactive_sell()
|
|
if decision:
|
|
return decision
|
|
|
|
if self.p.hunt_preference > self.p.gather_preference and stats.energy >= 10:
|
|
return AIDecision(
|
|
action=ActionType.HUNT,
|
|
target_resource=ResourceType.MEAT,
|
|
reason="Default: hunting",
|
|
)
|
|
return AIDecision(
|
|
action=ActionType.GATHER,
|
|
target_resource=ResourceType.BERRIES,
|
|
reason="Default: gathering",
|
|
)
|
|
|
|
if random.random() < self.p.market_affinity:
|
|
for resource_type, action, reason, weight in needs:
|
|
order = self.market.get_cheapest_order(resource_type)
|
|
if order and order.seller_id != self.agent.id and self.agent.money >= order.price_per_unit:
|
|
if self._is_good_buy(resource_type, order.price_per_unit):
|
|
return AIDecision(
|
|
action=ActionType.TRADE,
|
|
target_resource=resource_type,
|
|
order_id=order.id,
|
|
quantity=1,
|
|
price=order.price_per_unit,
|
|
reason=f"{reason} (buying cheap)",
|
|
)
|
|
|
|
total_weight = sum(weight for _, _, _, weight in needs)
|
|
r = random.random() * total_weight
|
|
cumulative = 0
|
|
for resource, action, reason, weight in needs:
|
|
cumulative += weight
|
|
if r <= cumulative:
|
|
return AIDecision(
|
|
action=action,
|
|
target_resource=resource,
|
|
reason=reason,
|
|
)
|
|
|
|
resource, action, reason, _ = needs[0]
|
|
return AIDecision(
|
|
action=action,
|
|
target_resource=resource,
|
|
reason=reason,
|
|
)
|
|
|
|
|
|
def get_ai_decision(agent: Agent, market: "OrderBook", step_in_day: int = 1,
|
|
day_steps: int = 10, current_turn: int = 0, world = None) -> AIDecision:
|
|
"""Convenience function to get an AI decision for an agent."""
|
|
ai = AgentAI(agent, market, step_in_day, day_steps, current_turn, world)
|
|
return ai.decide()
|