"""Predefined GOAP actions for agents. Actions are organized by category: - Consume actions: Use resources from inventory - Gather actions: Produce resources - Trade actions: Buy/sell on market - Utility actions: Rest, sleep, build fire """ from typing import Optional, TYPE_CHECKING from backend.domain.action import ActionType, ACTION_CONFIG from backend.domain.resources import ResourceType from .world_state import WorldState from .action import ( GOAPAction, create_consume_action, create_gather_action, create_buy_action, create_rest_action, create_build_fire_action, create_sleep_action, ) if TYPE_CHECKING: from backend.domain.agent import Agent from backend.core.market import OrderBook def _get_action_configs(): """Get action configurations from config.""" return ACTION_CONFIG # ============================================================================= # CONSUME ACTIONS # ============================================================================= def _create_drink_water() -> GOAPAction: """Drink water to restore thirst.""" return create_consume_action( resource_type=ResourceType.WATER, stat_name="thirst", stat_increase=0.50, # 50 thirst out of 100 ) def _create_eat_meat() -> GOAPAction: """Eat meat to restore hunger (primary food source).""" return create_consume_action( resource_type=ResourceType.MEAT, stat_name="hunger", stat_increase=0.35, # 35 hunger secondary_stat="energy", secondary_increase=0.24, # 12 energy ) def _create_eat_berries() -> GOAPAction: """Eat berries to restore hunger and some thirst.""" return create_consume_action( resource_type=ResourceType.BERRIES, stat_name="hunger", stat_increase=0.10, # 10 hunger secondary_stat="thirst", secondary_increase=0.04, # 4 thirst ) CONSUME_ACTIONS = [ _create_drink_water(), _create_eat_meat(), _create_eat_berries(), ] # ============================================================================= # GATHER ACTIONS # ============================================================================= def _create_get_water() -> GOAPAction: """Get water from the river.""" config = _get_action_configs()[ActionType.GET_WATER] return create_gather_action( action_type=ActionType.GET_WATER, resource_type=ResourceType.WATER, energy_cost=config.energy_cost, expected_output=1, success_chance=1.0, ) def _create_gather_berries() -> GOAPAction: """Gather berries (safe, reliable).""" config = _get_action_configs()[ActionType.GATHER] expected = (config.min_output + config.max_output) // 2 return create_gather_action( action_type=ActionType.GATHER, resource_type=ResourceType.BERRIES, energy_cost=config.energy_cost, expected_output=expected, success_chance=1.0, ) def _create_hunt() -> GOAPAction: """Hunt for meat (risky, high reward). Hunt should be attractive because: - Meat gives much more hunger than berries (35 vs 10) - Meat also gives energy (12) - You also get hide for clothes Cost is balanced against gathering: - Hunt: -7 energy, 70% success, 2-5 meat + 0-2 hide - Gather: -3 energy, 100% success, 2-4 berries Effective food per energy: - Hunt: 3.5 meat avg * 0.7 = 2.45 meat = 2.45 * 35 hunger = 85.75 hunger for 7 energy = 12.25 hunger/energy - Gather: 3 berries avg * 1.0 = 3 berries = 3 * 10 hunger = 30 hunger for 3 energy = 10 hunger/energy So hunting is actually MORE efficient per energy for hunger! The cost should reflect this. """ config = _get_action_configs()[ActionType.HUNT] expected = (config.min_output + config.max_output) // 2 # Custom preconditions for hunting def preconditions(state: WorldState) -> bool: # Need more energy for hunting (but not excessively so) energy_needed = abs(config.energy_cost) / 50.0 + 0.05 return state.energy_pct >= energy_needed and state.inventory_space >= 2 def effects(state: WorldState) -> dict: # Account for success chance effective_meat = int(expected * config.success_chance) effective_hide = int(1 * config.success_chance) # Average hide energy_spent = abs(config.energy_cost) / 50.0 return { "energy_pct": max(0, state.energy_pct - energy_spent), "meat_count": state.meat_count + effective_meat, "food_count": state.food_count + effective_meat, "hide_count": state.hide_count + effective_hide, "inventory_space": max(0, state.inventory_space - effective_meat - effective_hide), } def cost(state: WorldState) -> float: # Hunt should be comparable to gather when considering value: # - Hunt gives 3.5 meat avg (35 hunger each) = 122.5 hunger value # - Gather gives 3 berries avg (10 hunger each) = 30 hunger value # Hunt is 4x more valuable for hunger! So cost can be higher but not 4x. # Base cost similar to gather base_cost = 0.6 # Success chance penalty (small) if config.success_chance < 1.0: base_cost *= 1.0 + (1.0 - config.success_chance) * 0.2 # STRONG profession specialization effect for hunting # Compare hunt_preference to other preferences other_prefs = (state.gather_preference + state.trade_preference) / 2 relative_strength = state.hunt_preference / max(0.1, other_prefs) # relative_strength > 1.0 means hunting is your specialty if relative_strength >= 1.0: # Specialist discount: up to 50% off preference_modifier = 1.0 / relative_strength else: # Non-specialist penalty: up to 3x cost preference_modifier = 1.0 + (1.0 - relative_strength) * 2.0 base_cost *= preference_modifier # Skill reduces cost further (experienced hunters are efficient) # skill 0: no bonus, skill 1.0: 40% discount skill_modifier = 1.0 - state.hunting_skill * 0.4 base_cost *= skill_modifier # Risk tolerance still has mild effect risk_modifier = 1.0 + (0.5 - state.risk_tolerance) * 0.15 base_cost *= risk_modifier # Big bonus if we have no meat - prioritize getting some if state.meat_count == 0: base_cost *= 0.6 # Bonus if low on food in general if state.food_count < 2: base_cost *= 0.8 return base_cost return GOAPAction( name="Hunt", action_type=ActionType.HUNT, target_resource=ResourceType.MEAT, preconditions=preconditions, effects=effects, cost=cost, ) def _create_chop_wood() -> GOAPAction: """Chop wood for fires.""" config = _get_action_configs()[ActionType.CHOP_WOOD] expected = (config.min_output + config.max_output) // 2 return create_gather_action( action_type=ActionType.CHOP_WOOD, resource_type=ResourceType.WOOD, energy_cost=config.energy_cost, expected_output=expected, success_chance=config.success_chance, ) def _create_weave_clothes() -> GOAPAction: """Craft clothes from hide.""" config = _get_action_configs()[ActionType.WEAVE] def preconditions(state: WorldState) -> bool: return ( state.hide_count >= 1 and not state.has_clothes and state.energy_pct >= abs(config.energy_cost) / 50.0 + 0.05 ) def effects(state: WorldState) -> dict: return { "has_clothes": True, "hide_count": state.hide_count - 1, "energy_pct": max(0, state.energy_pct - abs(config.energy_cost) / 50.0), } def cost(state: WorldState) -> float: return abs(config.energy_cost) / 3.0 return GOAPAction( name="Weave Clothes", action_type=ActionType.WEAVE, target_resource=ResourceType.CLOTHES, preconditions=preconditions, effects=effects, cost=cost, ) GATHER_ACTIONS = [ _create_get_water(), _create_gather_berries(), _create_hunt(), _create_chop_wood(), _create_weave_clothes(), ] # ============================================================================= # TRADE ACTIONS # ============================================================================= def _create_buy_water() -> GOAPAction: """Buy water from the market.""" return create_buy_action(ResourceType.WATER) def _create_buy_meat() -> GOAPAction: """Buy meat from the market.""" return create_buy_action(ResourceType.MEAT) def _create_buy_berries() -> GOAPAction: """Buy berries from the market.""" return create_buy_action(ResourceType.BERRIES) def _create_buy_wood() -> GOAPAction: """Buy wood from the market.""" return create_buy_action(ResourceType.WOOD) def _create_sell_action(resource_type: ResourceType, min_keep: int = 1) -> GOAPAction: """Factory for creating sell actions.""" count_name = f"{resource_type.value}_count" if resource_type == ResourceType.BERRIES: count_name = "berries_count" elif resource_type == ResourceType.MEAT: count_name = "meat_count" def preconditions(state: WorldState) -> bool: current = getattr(state, count_name) return current > min_keep and state.energy_pct >= 0.05 def effects(state: WorldState) -> dict: # Estimate we'll get a reasonable price (around min_price from config) # This is approximate - actual execution will get real prices estimated_price = 100 # Base estimate (min_price from config) current = getattr(state, count_name) sell_qty = min(3, current - min_keep) # Sell up to 3, keep minimum result = { "money": state.money + estimated_price * sell_qty, count_name: current - sell_qty, "inventory_space": state.inventory_space + sell_qty, "energy_pct": max(0, state.energy_pct - 0.02), } # Update food count if selling food if resource_type in [ResourceType.MEAT, ResourceType.BERRIES]: result["food_count"] = state.food_count - sell_qty return result def cost(state: WorldState) -> float: # Selling has low cost - everyone should be able to sell excess base_cost = 1.0 # MILD profession effect for selling (everyone should be able to trade) other_prefs = (state.hunt_preference + state.gather_preference) / 2 relative_strength = state.trade_preference / max(0.1, other_prefs) if relative_strength >= 1.0: # Specialist discount: up to 40% off for dedicated traders preference_modifier = max(0.6, 1.0 / relative_strength) else: # Mild non-specialist penalty: up to 50% cost increase preference_modifier = 1.0 + (1.0 - relative_strength) * 0.5 base_cost *= preference_modifier # Skill reduces cost (experienced traders know the market) # skill 0: no bonus, skill 1.0: 40% discount skill_modifier = 1.0 - state.trading_skill * 0.4 base_cost *= skill_modifier # Hoarders reluctant to sell (mild effect) base_cost *= (0.8 + state.hoarding_rate * 0.4) return base_cost return GOAPAction( name=f"Sell {resource_type.value}", action_type=ActionType.TRADE, target_resource=resource_type, preconditions=preconditions, effects=effects, cost=cost, ) TRADE_ACTIONS = [ _create_buy_water(), _create_buy_meat(), _create_buy_berries(), _create_buy_wood(), _create_sell_action(ResourceType.WATER, min_keep=2), _create_sell_action(ResourceType.MEAT, min_keep=1), _create_sell_action(ResourceType.BERRIES, min_keep=2), _create_sell_action(ResourceType.WOOD, min_keep=1), _create_sell_action(ResourceType.HIDE, min_keep=0), ] # ============================================================================= # UTILITY ACTIONS # ============================================================================= UTILITY_ACTIONS = [ create_rest_action(), create_build_fire_action(), create_sleep_action(), ] # ============================================================================= # ALL ACTIONS # ============================================================================= def get_all_actions() -> list[GOAPAction]: """Get all available GOAP actions.""" return CONSUME_ACTIONS + GATHER_ACTIONS + TRADE_ACTIONS + UTILITY_ACTIONS def get_action_by_type(action_type: ActionType) -> list[GOAPAction]: """Get all GOAP actions of a specific type.""" all_actions = get_all_actions() return [a for a in all_actions if a.action_type == action_type] def get_action_by_name(name: str) -> Optional[GOAPAction]: """Get a specific action by name.""" all_actions = get_all_actions() for action in all_actions: if action.name == name: return action return None