400 lines
13 KiB
Python
400 lines
13 KiB
Python
"""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
|
|
|