# Action Prompts System

## Overview

Action prompts are predefined story actions that players can select during gameplay. They appear as buttons in the story UI, with availability filtered by subscription tier and location type.

**Key Features:**
- YAML-driven configuration
- Tier-based availability (Free, Basic, Premium, Elite)
- Location-based context filtering
- Custom AI prompt templates per action

**Files:**
- **Model:** `app/models/action_prompt.py`
- **Loader:** `app/services/action_prompt_loader.py`
- **Data:** `app/data/action_prompts.yaml`

---

## Architecture

```
┌─────────────────────────┐
│   action_prompts.yaml   │  ← YAML configuration
├─────────────────────────┤
│  ActionPromptLoader     │  ← Singleton loader/cache
├─────────────────────────┤
│    ActionPrompt         │  ← Data model
├─────────────────────────┤
│   Story UI / API        │  ← Filtered action buttons
└─────────────────────────┘
```

---

## ActionPrompt Model

**File:** `app/models/action_prompt.py`

### Fields

```python
@dataclass
class ActionPrompt:
    prompt_id: str                    # Unique identifier
    category: ActionCategory          # Action category
    display_text: str                 # Button label
    description: str                  # Tooltip text
    tier_required: UserTier           # Minimum tier
    context_filter: List[LocationType] # Where available
    dm_prompt_template: str           # AI prompt template
    icon: Optional[str] = None        # Optional icon name
    cooldown_turns: int = 0           # Turns before reuse
```

### ActionCategory Enum

```python
class ActionCategory(str, Enum):
    ASK_QUESTION = "ask_question"   # Gather info from NPCs
    TRAVEL = "travel"               # Move to new location
    GATHER_INFO = "gather_info"     # Search/investigate
    REST = "rest"                   # Rest and recover
    INTERACT = "interact"           # Interact with objects
    EXPLORE = "explore"             # Explore the area
    SPECIAL = "special"             # Tier-specific special actions
```

### LocationType Enum

```python
class LocationType(str, Enum):
    TOWN = "town"              # Populated settlements
    TAVERN = "tavern"          # Taverns and inns
    WILDERNESS = "wilderness"  # Outdoor areas
    DUNGEON = "dungeon"        # Dungeons and caves
    SAFE_AREA = "safe_area"    # Protected zones
    LIBRARY = "library"        # Libraries and archives
    ANY = "any"                # Available everywhere
```

### Availability Methods

```python
from app.models.action_prompt import ActionPrompt, LocationType
from app.ai.model_selector import UserTier

# Check if available
if action.is_available(UserTier.FREE, LocationType.TOWN):
    # Show action to player
    pass

# Check if locked (tier too low)
if action.is_locked(UserTier.FREE):
    reason = action.get_lock_reason(UserTier.FREE)
    # "Requires Premium tier or higher"
```

### Serialization

```python
# To dictionary
data = action.to_dict()

# From dictionary
action = ActionPrompt.from_dict(data)
```

---

## ActionPromptLoader Service

**File:** `app/services/action_prompt_loader.py`

Singleton service that loads and caches action prompts from YAML.

### Basic Usage

```python
from app.services.action_prompt_loader import ActionPromptLoader
from app.models.action_prompt import LocationType
from app.ai.model_selector import UserTier

loader = ActionPromptLoader()

# Load from YAML (or auto-loads from default path)
loader.load_from_yaml("app/data/action_prompts.yaml")

# Get available actions for user at location
actions = loader.get_available_actions(
    user_tier=UserTier.FREE,
    location_type=LocationType.TOWN
)

for action in actions:
    print(f"{action.display_text} - {action.description}")
```

### Query Methods

```python
# Get specific action
action = loader.get_action_by_id("ask_locals")

# Get all actions
all_actions = loader.get_all_actions()

# Get actions by tier (ignoring location)
tier_actions = loader.get_actions_by_tier(UserTier.PREMIUM)

# Get actions by category
questions = loader.get_actions_by_category("ask_question")

# Get locked actions (for upgrade prompts)
locked = loader.get_locked_actions(UserTier.FREE, LocationType.TOWN)
```

### Utility Methods

```python
# Check if loaded
if loader.is_loaded():
    count = loader.get_prompt_count()

# Force reload
loader.reload("app/data/action_prompts.yaml")

# Reset singleton (for testing)
ActionPromptLoader.reset_instance()
```

### Error Handling

```python
from app.services.action_prompt_loader import (
    ActionPromptLoader,
    ActionPromptLoaderError,
    ActionPromptNotFoundError
)

try:
    action = loader.get_action_by_id("invalid_id")
except ActionPromptNotFoundError:
    # Action not found
    pass

try:
    loader.load_from_yaml("invalid_path.yaml")
except ActionPromptLoaderError as e:
    # File not found, invalid YAML, etc.
    pass
```

---

## YAML Configuration

**File:** `app/data/action_prompts.yaml`

### Structure

```yaml
action_prompts:
  - prompt_id: unique_id
    category: ask_question
    display_text: Button Label
    description: Tooltip description
    tier_required: free
    context_filter: [town, tavern]
    icon: chat
    cooldown_turns: 0
    dm_prompt_template: |
      Jinja2 template for AI prompt...
```

### Available Actions

#### Free Tier (4 actions)

| ID | Display Text | Category | Locations |
|----|-------------|----------|-----------|
| `ask_locals` | Ask locals for information | ask_question | town, tavern |
| `explore_area` | Explore the area | explore | wilderness, dungeon |
| `search_supplies` | Search for supplies | gather_info | any |
| `rest_recover` | Rest and recover | rest | town, tavern, safe_area |

#### Premium Tier (+3 actions)

| ID | Display Text | Category | Locations |
|----|-------------|----------|-----------|
| `investigate_suspicious` | Investigate suspicious activity | gather_info | any |
| `follow_lead` | Follow a lead | travel | any |
| `make_camp` | Make camp | rest | wilderness |

#### Elite Tier (+3 actions)

| ID | Display Text | Category | Locations |
|----|-------------|----------|-----------|
| `consult_texts` | Consult ancient texts | special | library, town |
| `commune_nature` | Commune with nature | special | wilderness |
| `seek_audience` | Seek audience with authorities | special | town |

### Total by Tier

- **Free:** 4 actions
- **Premium:** 7 actions (Free + 3)
- **Elite:** 10 actions (Premium + 3)

---

## DM Prompt Templates

Each action has a Jinja2 template for generating AI prompts.

### Available Variables

```jinja2
{{ character.name }}
{{ character.level }}
{{ character.player_class }}
{{ character.current_hp }}
{{ character.max_hp }}
{{ character.stats.strength }}
{{ character.stats.dexterity }}
{{ character.stats.constitution }}
{{ character.stats.intelligence }}
{{ character.stats.wisdom }}
{{ character.stats.charisma }}
{{ character.reputation }}

{{ game_state.current_location }}
{{ game_state.location_type }}
{{ game_state.active_quests }}
```

### Template Example

```yaml
dm_prompt_template: |
  The player explores the area around {{ game_state.current_location }}.

  Character: {{ character.name }}, Level {{ character.level }} {{ character.player_class }}
  Perception modifier: {{ character.stats.wisdom | default(10) }}

  Describe what the player discovers:
  - Environmental details and atmosphere
  - Points of interest (paths, structures, natural features)
  - Any items, tracks, or clues found
  - Potential dangers or opportunities

  Based on their Wisdom score, they may notice hidden details.
  End with options for where to go or what to investigate next.
```

### How Templates Flow to AI

The `dm_prompt_template` is passed through the system as follows:

1. **Sessions API** loads the action prompt and extracts `dm_prompt_template`
2. **AI task** receives it in the context as `dm_prompt_template`
3. **NarrativeGenerator** receives it as `action_instructions` parameter
4. **story_action.j2** injects it under "Action-Specific Instructions"

```python
# In ai_tasks.py
response = generator.generate_story_response(
    character=context['character'],
    action=context['action'],
    game_state=context['game_state'],
    user_tier=user_tier,
    action_instructions=context.get('dm_prompt_template')  # From action prompt
)
```

### Player Agency Rules

All action templates should follow these critical rules to maintain player agency:

```yaml
# Example with player agency enforcement
dm_prompt_template: |
  The player searches for supplies in {{ game_state.current_location }}.

  IMPORTANT - This is a SEARCH action, not a purchase action:
  - In towns/markets: Describe vendors and wares with PRICES. Ask what to buy.
  - In wilderness: Describe what they FIND. Ask if they want to gather.

  NEVER automatically:
  - Purchase items or spend gold
  - Add items to inventory without asking
  - Complete any transaction

  End with: "What would you like to do?"
```

The base `story_action.j2` template also enforces these rules globally:
- Never make decisions for the player
- Never complete transactions without consent
- Present choices and let the player decide

---

## Integration Examples

### API Endpoint

```python
@bp.route('/sessions/<session_id>/available-actions', methods=['GET'])
@require_auth
def get_available_actions(session_id):
    user = get_current_user()
    session = get_session(session_id)

    loader = ActionPromptLoader()

    # Get available actions
    available = loader.get_available_actions(
        user_tier=user.tier,
        location_type=session.game_state.location_type
    )

    # Get locked actions for upgrade prompts
    locked = loader.get_locked_actions(
        user_tier=user.tier,
        location_type=session.game_state.location_type
    )

    return api_response(
        status=200,
        result={
            "available_actions": [a.to_dict() for a in available],
            "locked_actions": [
                {
                    **a.to_dict(),
                    "lock_reason": a.get_lock_reason(user.tier)
                }
                for a in locked
            ]
        }
    )
```

### Story UI Template

```jinja2
{% for action in available_actions %}
<button
    class="action-btn"
    hx-post="/api/v1/sessions/{{ session_id }}/action"
    hx-vals='{"action_type": "button", "prompt_id": "{{ action.prompt_id }}"}'
    hx-target="#dm-response"
    title="{{ action.description }}">
    {% if action.icon %}<i class="icon-{{ action.icon }}"></i>{% endif %}
    {{ action.display_text }}
</button>
{% endfor %}

{% for action in locked_actions %}
<button
    class="action-btn locked"
    disabled
    title="{{ action.lock_reason }}">
    {{ action.display_text }}
    <span class="lock-icon">🔒</span>
</button>
{% endfor %}
```

### Processing Action in AI Task

```python
from app.services.action_prompt_loader import ActionPromptLoader
from app.ai.prompt_templates import render_prompt

def process_button_action(session_id: str, prompt_id: str, user_tier: UserTier):
    loader = ActionPromptLoader()
    session = get_session(session_id)
    character = get_character(session.character_id)

    # Get the action
    action = loader.get_action_by_id(prompt_id)

    # Verify availability
    if not action.is_available(user_tier, session.game_state.location_type):
        raise ValueError("Action not available")

    # Build the AI prompt using action's template
    prompt = render_prompt(
        action.dm_prompt_template,
        character=character.to_dict(),
        game_state=session.game_state.to_dict()
    )

    # Generate AI response
    response = narrative_generator.generate_story_response(...)

    return response
```

---

## Adding New Actions

### 1. Add to YAML

```yaml
- prompt_id: new_action_id
  category: explore          # Must match ActionCategory enum
  display_text: My New Action
  description: What this action does
  tier_required: premium     # free, basic, premium, elite
  context_filter: [town, wilderness]  # Or [any] for all
  icon: star                 # Optional icon name
  cooldown_turns: 2          # 0 for no cooldown
  dm_prompt_template: |
    The player {{ action description }}...

    Character: {{ character.name }}, Level {{ character.level }}

    Describe:
    - What happens
    - What they discover
    - Next steps

    End with a clear outcome.
```

### 2. Reload Actions

```python
loader = ActionPromptLoader()
loader.reload("app/data/action_prompts.yaml")
```

### 3. Test

```python
action = loader.get_action_by_id("new_action_id")
assert action.is_available(UserTier.PREMIUM, LocationType.TOWN)
```

---

## Tier Hierarchy

Actions are available to users at or above the required tier:

```
FREE (0) < BASIC (1) < PREMIUM (2) < ELITE (3)
```

- **FREE action:** Available to all tiers
- **PREMIUM action:** Available to Premium and Elite
- **ELITE action:** Available only to Elite

---

## Cooldown System

Actions with `cooldown_turns > 0` cannot be used again for that many turns.

```yaml
cooldown_turns: 3  # Cannot use for 3 turns after use
```

Cooldown tracking should be implemented in the session/game state.

---

## Testing

### Unit Tests

```python
def test_action_availability():
    action = ActionPrompt(
        prompt_id="test",
        category=ActionCategory.EXPLORE,
        display_text="Test",
        description="Test action",
        tier_required=UserTier.PREMIUM,
        context_filter=[LocationType.TOWN],
        dm_prompt_template="Test"
    )

    # Premium action available to Elite in Town
    assert action.is_available(UserTier.ELITE, LocationType.TOWN) == True

    # Premium action not available to Free
    assert action.is_available(UserTier.FREE, LocationType.TOWN) == False

    # Not available in wrong location
    assert action.is_available(UserTier.ELITE, LocationType.WILDERNESS) == False

def test_loader():
    loader = ActionPromptLoader()
    loader.load_from_yaml("app/data/action_prompts.yaml")

    # Free tier in town should see limited actions
    actions = loader.get_available_actions(UserTier.FREE, LocationType.TOWN)
    assert len(actions) == 2  # ask_locals, search_supplies

    # Elite tier sees all actions for location
    elite_actions = loader.get_available_actions(UserTier.ELITE, LocationType.TOWN)
    assert len(elite_actions) > len(actions)
```

### Manual Verification

```python
from app.services.action_prompt_loader import ActionPromptLoader
from app.models.action_prompt import LocationType
from app.ai.model_selector import UserTier

loader = ActionPromptLoader()
loader.load_from_yaml("app/data/action_prompts.yaml")

print(f"Total actions: {loader.get_prompt_count()}")

for tier in [UserTier.FREE, UserTier.PREMIUM, UserTier.ELITE]:
    actions = loader.get_actions_by_tier(tier)
    print(f"{tier.value}: {len(actions)} actions")
```
