villsim/backend/domain/action.py

276 lines
9.6 KiB
Python

"""Action definitions for the Village Simulation.
Action configurations are loaded dynamically from the global config.
"""
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, TYPE_CHECKING
from .resources import ResourceType
if TYPE_CHECKING:
from backend.config import SimulationConfig
class ActionType(Enum):
"""Types of actions an agent can perform."""
# Basic survival actions
SLEEP = "sleep" # Night action - restores energy
REST = "rest" # Day action - restores some energy
HUNT = "hunt" # Produces meat and hide
GATHER = "gather" # Produces berries
CHOP_WOOD = "chop_wood" # Produces wood
GET_WATER = "get_water" # Produces water
WEAVE = "weave" # Produces clothes from hide
BUILD_FIRE = "build_fire" # Consumes wood, provides heat
TRADE = "trade" # Market interaction
CONSUME = "consume" # Consume resource from inventory
# NEW: Oil industry actions
DRILL_OIL = "drill_oil" # Extract oil from oil fields
REFINE = "refine" # Convert oil to fuel
BURN_FUEL = "burn_fuel" # Use fuel for heat/energy
# NEW: Religious actions
PRAY = "pray" # Increase faith, slight energy cost
PREACH = "preach" # Spread religion, convert others
# NEW: Diplomatic actions
NEGOTIATE = "negotiate" # Improve relations with another faction
DECLARE_WAR = "declare_war" # Declare war on another faction
MAKE_PEACE = "make_peace" # Propose peace treaty
@dataclass
class ActionConfig:
"""Configuration for an action type."""
energy_cost: int # Negative = cost, positive = restoration
success_chance: float = 1.0 # 0.0 to 1.0
min_output: int = 0
max_output: int = 0
output_resource: Optional[ResourceType] = None
secondary_output: Optional[ResourceType] = None
secondary_min: int = 0
secondary_max: int = 0
requires_resource: Optional[ResourceType] = None
requires_quantity: int = 0
# NEW: Faith effects
faith_gain: int = 0
faith_spread: int = 0
def get_action_config() -> dict[ActionType, ActionConfig]:
"""Get action configurations from the global config."""
from backend.config import get_config
config = get_config()
actions = config.actions
return {
ActionType.SLEEP: ActionConfig(
energy_cost=actions.sleep_energy,
),
ActionType.REST: ActionConfig(
energy_cost=actions.rest_energy,
),
ActionType.HUNT: ActionConfig(
energy_cost=actions.hunt_energy,
success_chance=actions.hunt_success,
min_output=actions.hunt_meat_min,
max_output=actions.hunt_meat_max,
output_resource=ResourceType.MEAT,
secondary_output=ResourceType.HIDE,
secondary_min=actions.hunt_hide_min,
secondary_max=actions.hunt_hide_max,
),
ActionType.GATHER: ActionConfig(
energy_cost=actions.gather_energy,
success_chance=1.0,
min_output=actions.gather_min,
max_output=actions.gather_max,
output_resource=ResourceType.BERRIES,
),
ActionType.CHOP_WOOD: ActionConfig(
energy_cost=actions.chop_wood_energy,
success_chance=actions.chop_wood_success,
min_output=actions.chop_wood_min,
max_output=actions.chop_wood_max,
output_resource=ResourceType.WOOD,
),
ActionType.GET_WATER: ActionConfig(
energy_cost=actions.get_water_energy,
success_chance=1.0,
min_output=1,
max_output=1,
output_resource=ResourceType.WATER,
),
ActionType.WEAVE: ActionConfig(
energy_cost=actions.weave_energy,
success_chance=1.0,
min_output=1,
max_output=1,
output_resource=ResourceType.CLOTHES,
requires_resource=ResourceType.HIDE,
requires_quantity=1,
),
ActionType.BUILD_FIRE: ActionConfig(
energy_cost=actions.build_fire_energy,
success_chance=1.0,
requires_resource=ResourceType.WOOD,
requires_quantity=1,
),
ActionType.TRADE: ActionConfig(
energy_cost=actions.trade_energy,
),
ActionType.CONSUME: ActionConfig(
energy_cost=0,
),
# NEW: Oil industry actions
ActionType.DRILL_OIL: ActionConfig(
energy_cost=actions.drill_oil_energy,
success_chance=actions.drill_oil_success,
min_output=actions.drill_oil_min,
max_output=actions.drill_oil_max,
output_resource=ResourceType.OIL,
),
ActionType.REFINE: ActionConfig(
energy_cost=actions.refine_energy,
success_chance=1.0,
min_output=1,
max_output=1,
output_resource=ResourceType.FUEL,
requires_resource=ResourceType.OIL,
requires_quantity=2, # 2 oil -> 1 fuel
),
ActionType.BURN_FUEL: ActionConfig(
energy_cost=-1, # Minimal effort to burn fuel
success_chance=1.0,
requires_resource=ResourceType.FUEL,
requires_quantity=1,
),
# NEW: Religious actions
ActionType.PRAY: ActionConfig(
energy_cost=actions.pray_energy,
success_chance=1.0,
faith_gain=actions.pray_faith_gain,
),
ActionType.PREACH: ActionConfig(
energy_cost=actions.preach_energy,
success_chance=actions.preach_convert_chance,
faith_spread=actions.preach_faith_spread,
),
# NEW: Diplomatic actions
ActionType.NEGOTIATE: ActionConfig(
energy_cost=actions.negotiate_energy,
success_chance=0.7, # Not always successful
),
ActionType.DECLARE_WAR: ActionConfig(
energy_cost=actions.declare_war_energy,
success_chance=1.0, # Always succeeds (but has consequences)
),
ActionType.MAKE_PEACE: ActionConfig(
energy_cost=actions.make_peace_energy,
success_chance=0.5, # Harder to make peace than war
),
}
# Lazy-loaded action config cache
_action_config_cache: Optional[dict[ActionType, ActionConfig]] = None
def get_cached_action_config() -> dict[ActionType, ActionConfig]:
"""Get cached action config (rebuilds if config changes)."""
global _action_config_cache
if _action_config_cache is None:
_action_config_cache = get_action_config()
return _action_config_cache
def reset_action_config_cache() -> None:
"""Reset the action config cache (call when config changes)."""
global _action_config_cache
_action_config_cache = None
class _ActionConfigAccessor:
"""Accessor class that provides dict-like access to action configs."""
def __getitem__(self, action_type: ActionType) -> ActionConfig:
return get_cached_action_config()[action_type]
def get(self, action_type: ActionType, default=None) -> Optional[ActionConfig]:
return get_cached_action_config().get(action_type, default)
def __contains__(self, action_type: ActionType) -> bool:
return action_type in get_cached_action_config()
def items(self):
return get_cached_action_config().items()
def keys(self):
return get_cached_action_config().keys()
def values(self):
return get_cached_action_config().values()
# This provides the same interface as before but loads from config
ACTION_CONFIG = _ActionConfigAccessor()
# Action categories for AI decision making
SURVIVAL_ACTIONS = {
ActionType.HUNT, ActionType.GATHER, ActionType.CHOP_WOOD,
ActionType.GET_WATER, ActionType.BUILD_FIRE, ActionType.CONSUME
}
PRODUCTION_ACTIONS = {
ActionType.HUNT, ActionType.GATHER, ActionType.CHOP_WOOD,
ActionType.GET_WATER, ActionType.DRILL_OIL
}
CRAFTING_ACTIONS = {ActionType.WEAVE, ActionType.REFINE}
RELIGIOUS_ACTIONS = {ActionType.PRAY, ActionType.PREACH}
DIPLOMATIC_ACTIONS = {ActionType.NEGOTIATE, ActionType.DECLARE_WAR, ActionType.MAKE_PEACE}
ECONOMIC_ACTIONS = {ActionType.TRADE}
@dataclass
class ActionResult:
"""Result of executing an action."""
action_type: ActionType
success: bool
energy_spent: int = 0
resources_gained: list = field(default_factory=list)
resources_consumed: list = field(default_factory=list)
heat_gained: int = 0
faith_gained: int = 0 # NEW
relation_change: int = 0 # NEW
message: str = ""
# NEW: Diplomatic effects
target_faction: Optional[str] = None
diplomatic_effect: Optional[str] = None # "war", "peace", "improved", "degraded"
def to_dict(self) -> dict:
"""Convert to dictionary for API serialization."""
return {
"action_type": self.action_type.value,
"success": self.success,
"energy_spent": self.energy_spent,
"resources_gained": [
{"type": r.type.value, "quantity": r.quantity}
for r in self.resources_gained
],
"resources_consumed": [
{"type": r.type.value, "quantity": r.quantity}
for r in self.resources_consumed
],
"heat_gained": self.heat_gained,
"faith_gained": self.faith_gained,
"relation_change": self.relation_change,
"target_faction": self.target_faction,
"diplomatic_effect": self.diplomatic_effect,
"message": self.message,
}