# BaoLife Event System: Organization & Scaling Guide

**Last Updated**: November 13, 2025
**Status**: ✅ Optimized for 100-year full-life simulation

---

## Table of Contents
1. [Quick Reference](#quick-reference)
2. [Event Naming Conventions](#event-naming-conventions)
3. [File Organization Strategy](#file-organization-strategy)
4. [Event Types & Patterns](#event-types--patterns)
5. [Performance & Scalability](#performance--scalability)
6. [SQL vs Pickle Decision](#sql-vs-pickle-decision)
7. [Testing Strategy](#testing-strategy)
8. [Common Pitfalls](#common-pitfalls)

---

## Quick Reference

### Current Event System Stats
- **Total Events**: ~87 registered in event_registry
- **Performance**: O(1) lookups (set-based, 57x faster than lists)
- **Memory**: ~15 KB per player (100-year lifetime)
- **Check Frequency**: Every game hour (minuteOfHour == 0)
- **Event Types**: messageEvent, questionEvent, conversationEvent, dilemmaClass

### Event Checking Flow
```
Every game hour:
├─ checkTutorialEvents()       → ~10 events (age 0-5)
├─ get_applicable_events()     → ~87 events → ~20 filtered
├─ checkDayEvents()            → ~40 events (holidays, school)
└─ checkDilemmas()             → ~5 events (moral choices)
```

---

## Event Naming Conventions

### 1. One-Time Events (Happen Once Ever)
```python
fname = 'learnedWalk'           # Milestone
fname = 'firstKiss'             # Life event
fname = 'graduatedCollege'      # Achievement
```

**Pattern**: `camelCase`, no suffix, past tense for milestones

### 2. Annual Recurring Events
```python
fname = f'christmas_{player.c.ageYears}'      # Holiday
fname = f'birthday_{player.c.ageYears}'       # Annual celebration
fname = f'yearlyReview_{player.c.ageYears}'   # Yearly check-in
```

**Pattern**: `eventName_{age}`, allows recurrence each year

### 3. Per-NPC Events
```python
fname = f'firstDate_{partner.id}'                          # Relationship milestone
fname = f'argument_{player.c.id}_{npc.id}_{player.date}'  # Conflict (can repeat)
fname = f'funeral_{npc.firstname}{npc.lastname}'          # Death event
```

**Pattern**: Include `{npc.id}` or names for uniqueness

### 4. Age-Range Repeating Events
```python
fname = f'schoolFight_{player.c.ageYears}'   # Can happen each year of school
fname = f'carAccident_{player.date}'         # Can happen multiple times
fname = f'promotion_{player.c.job.id}_{player.c.ageYears}'
```

**Pattern**: Include year or date to allow repetition

### 5. Location-Based Events
```python
fname = f'visitedParis_{player.c.ageYears}'
fname = f'movedHouse_{player.c.location.id}'
```

**Pattern**: Include location identifier

---

## File Organization Strategy

### Current Structure (Good for ~100 events)
```
ws/
├── events.py              # General life events (70+ events)
├── dayEvents.py           # Holidays, school, birthdays (~40 events)
├── conversationEvents.py  # NPC conversations (~20 events)
├── tutorial_events.py     # Tutorial flow (~10 events)
└── server/
    └── event_registration.py  # Event registry setup
```

### Recommended Structure (Scale to 500+ events)
```
ws/
└── events/
    ├── __init__.py                  # Export all events
    ├── base.py                      # messageFunction, questionFunction, etc.
    ├── registry.py                  # Event registration (moved from server/)
    │
    ├── childhood/
    │   ├── __init__.py
    │   ├── early_years.py           # Age 0-5: walk, talk, potty
    │   ├── elementary.py            # Age 6-11: school, friends
    │   └── puberty.py               # Age 12-14: body changes
    │
    ├── adolescence/
    │   ├── __init__.py
    │   ├── high_school.py           # Age 15-18: grades, dating
    │   ├── college.py               # Age 18-22: parties, major
    │   └── first_job.py             # Age 16-25: employment
    │
    ├── adulthood/
    │   ├── __init__.py
    │   ├── career.py                # Age 22-65: promotions, layoffs
    │   ├── family.py                # Age 18-50: marriage, kids
    │   └── homeownership.py         # Age 25-40: buying house
    │
    ├── senior/
    │   ├── __init__.py
    │   ├── retirement.py            # Age 60-65: retire
    │   └── health_decline.py        # Age 70+: aging events
    │
    ├── holidays/
    │   ├── __init__.py
    │   ├── annual.py                # Christmas, New Year, etc.
    │   └── birthdays.py             # Birthday celebrations
    │
    ├── relationships/
    │   ├── __init__.py
    │   ├── romance.py               # Dating, breakups, marriage
    │   ├── family.py                # Parent, sibling interactions
    │   └── friendships.py           # Friend events
    │
    ├── random/
    │   ├── __init__.py
    │   ├── positive.py              # Found money, free concert
    │   ├── negative.py              # Car crash, injury
    │   └── neutral.py               # Neutral flavor events
    │
    └── tutorial/
        ├── __init__.py
        └── onboarding.py            # New player tutorial
```

### Migration Path
```python
# events/__init__.py
"""
Central export for all game events.
Import events from this module to maintain backward compatibility.
"""

# Import from categorized modules
from .childhood.early_years import *
from .childhood.elementary import *
from .adolescence.high_school import *
# ... etc

# Export all for backward compatibility
__all__ = [
    # Childhood
    'learnedWalk', 'learnedTalk', 'lostFirstTooth',
    # Adolescence
    'firstKiss', 'actTest', 'graduation',
    # ... etc
]
```

---

## Event Types & Patterns

### 1. Message Events (One-Way Information)

```python
def learnedWalk(player, type='message'):
    """
    Player learns to walk (one-time milestone).

    Triggers: Age 1 year, hasn't learned yet
    Category: Childhood milestone
    """
    fname = 'learnedWalk'
    check = (fname not in player.events and
             player.c.ageYears == 1 and
             random.random() < 0.5)  # 50% chance at age 1

    if check:
        return messageFunction(
            fname=fname,
            message='You took your first steps today!',
            player=player,
            check=check,
            energyCost=0,
            moneyCost=0
        )
```

**Template**:
```python
def {eventName}(player, type='message'):
    """[Event description, triggers, category]"""
    fname = '{eventName}'  # or f'{eventName}_{player.c.ageYears}' for annual
    check = (fname not in player.events and
             [AGE_CONDITION] and
             [PROBABILITY_CONDITION])

    if check:
        return messageFunction(fname, message, player, check)
```

### 2. Question Events (Player Choices)

```python
def birthday(player, type='message', message=False, response=False):
    """
    Annual birthday celebration with choice of how to spend it.

    Triggers: Every 365 days
    Category: Holiday/Annual
    """
    fname = f'birthday_{player.c.ageYears}'
    check = (fname not in player.askedQuestions and
             player.c.ageDays > 0 and
             player.c.ageDays % 365 == 0)

    if check and type != 'answer':
        from functions import get_random_friend, get_random_family
        answerOptions = []

        if get_random_family(player):
            answerOptions.append("Spend it with family.")
        if get_random_friend(player):
            answerOptions.append("Spend it with friends.")
        answerOptions.append("Spend it alone, doing what you enjoy.")

        return questionFunction(
            fname=fname,
            message="It's your birthday! How would you like to spend it?",
            player=player,
            check=True,
            answerOptions=answerOptions
        )

    elif type == 'answer':
        player.askedQuestions.add(fname)

        if response['option'] == 'Spend it with family.':
            player.c.happiness += 15
            for p in player.r:
                if p.familyLevel == 1:
                    p.affinity += 15

        # ... handle other options
```

**Template**:
```python
def {eventName}(player, type='message', message=False, response=False):
    """[Event description]"""
    fname = f'{eventName}_{player.c.ageYears}'  # If annual
    check = (fname not in player.askedQuestions and [CONDITIONS])

    if check and type != 'answer':
        answerOptions = [...]
        return questionFunction(fname, message, player, True, answerOptions)

    elif type == 'answer':
        player.askedQuestions.add(fname)
        # Handle response logic
```

### 3. Conversation Events (Multi-Turn Dialogues)

See `conversationEvents.py` for examples.

### 4. Dilemma Events (Moral Choices with Consequences)

See `events.py` for `braceletDilemma`, `bullyDilemma` examples.

---

## Performance & Scalability

### Current Performance (After Set Optimization)

```python
# Event lookup performance
'christmas_25' in player.events  # 0.04 μs (O(1))

# Per hour: ~87 events × 0.04 μs = 3.5 μs total
# Per 100-year life: ~876,000 hours × 3.5 μs = ~3 seconds total
```

### Memory Footprint (100-Year Lifetime)

```
Annual events:     600 events  (6 holidays × 100 years)
Birthdays:         100 events  (1 per year)
One-time events:   500 events  (throughout life)
───────────────────────────────────────────────
Total:           ~1,200 events

Memory:           ~15 KB pickled
RAM:              ~60 KB in-memory (set)
```

**Verdict**: ✅ Scales easily to 1,000+ events per player

### Event Registry Filtering

```python
# event_registry.py already optimizes this
class EventRegistry:
    _by_age: Dict[Tuple[int, int], List[str]]  # Pre-filtered by age

# Example: Age 25 player
# Before filtering: 87 events
# After age filter: ~20 events (only check age 18-40 range)
# After conditions:  ~5 events actually checked
```

---

## SQL vs Pickle Decision

### Current: Pickle-Based (Recommended ✅)

**Pros**:
- ✅ **Simple**: One `pickle.dumps()` saves entire player state
- ✅ **Fast**: Direct object serialization, no ORM overhead
- ✅ **Flexible**: Add new fields without schema migrations
- ✅ **Atomic**: Entire player state saved together
- ✅ **Tested**: Already working in production

**Cons**:
- ❌ Can't query event history directly (e.g., "How many players got married at 25?")
- ❌ No analytics without unpickling all players
- ❌ Schema changes require migration code

**Use Cases Supported**:
- ✅ Save/load player game state
- ✅ Offline game progression
- ✅ Event deduplication
- ✅ Player-specific event history

---

### Alternative: SQL-Based Events (Not Recommended for Now ⚠️)

**Schema**:
```sql
CREATE TABLE player_events (
    id INT AUTO_INCREMENT PRIMARY KEY,
    player_id VARCHAR(36),
    event_id VARCHAR(100),
    event_type ENUM('message', 'question', 'conversation'),
    triggered_at TIMESTAMP,
    age_years INT,
    date VARCHAR(10),
    response JSON,
    INDEX idx_player (player_id),
    INDEX idx_event (event_id),
    INDEX idx_age (age_years),
    UNIQUE KEY uk_player_event (player_id, event_id)
);
```

**Pros**:
- ✅ **Analytics**: Query across all players (e.g., "What % chose family for birthday?")
- ✅ **Debugging**: See event history without unpickling
- ✅ **Reporting**: Dashboard of popular events
- ✅ **Compliance**: GDPR deletion easier

**Cons**:
- ❌ **Complexity**: Dual persistence (player object + events table)
- ❌ **Performance**: Extra DB writes per event (~60 events per hour)
- ❌ **Consistency**: Must keep player.events set and SQL table in sync
- ❌ **Migration**: Need to populate existing players
- ❌ **Cost**: More DB storage (1,000 rows per player vs 15 KB blob)

---

### Hybrid Approach (Future Enhancement)

Keep pickle for player state, add SQL for analytics:

```python
async def saveGameAsync(player):
    # 1. Save player object as pickle (current)
    await save_player_pickle(player)

    # 2. Optionally log new events to SQL (async, non-blocking)
    if config.ENABLE_EVENT_ANALYTICS:
        asyncio.create_task(log_new_events_to_sql(player))
```

**When to use SQL**:
- ✅ When you need cross-player analytics
- ✅ When building admin dashboards
- ✅ When doing A/B testing on events
- ✅ When you have 100,000+ players

**When NOT to use SQL**:
- ❌ For event deduplication (pickle is faster)
- ❌ For single-player queries (pickle has everything)
- ❌ Before you have scale problems

**Current Recommendation**:
📋 **Keep using pickle.** Add SQL analytics only when you need it.

---

## Testing Strategy

### Unit Tests for Events

```python
# tests/unit/test_events.py
def test_learned_walk_triggers_at_age_1():
    """Test that learnedWalk event triggers correctly."""
    player = create_test_player(age_years=1)

    result = learnedWalk(player, 'check')

    assert result is not None
    assert result.type == 'messageEvent'
    assert result.id == 'learnedWalk'
    assert 'learnedWalk' not in player.events  # Not added until sent

def test_learned_walk_no_duplicate():
    """Test that learnedWalk doesn't trigger twice."""
    player = create_test_player(age_years=1)
    player.events.add('learnedWalk')

    result = learnedWalk(player, 'check')

    assert result is None  # Already triggered

def test_christmas_triggers_on_date():
    """Test that christmas triggers on Dec 25."""
    player = create_test_player(age_years=10)
    player.date = '12-25'

    result = christmas(player, 'check')

    assert result is not None
    assert result.id == 'christmas_10'

def test_christmas_repeats_annually():
    """Test that christmas repeats each year."""
    player = create_test_player(age_years=10)
    player.date = '12-25'
    player.events.add('christmas_9')  # Had it last year

    result = christmas(player, 'check')

    assert result is not None  # Should trigger again at age 10
```

### Integration Tests

```python
# tests/integration/test_event_flow.py
async def test_event_triggers_during_game_loop():
    """Test that events trigger during actual game loop."""
    player = create_test_player(age_years=1)
    websocket = create_mock_websocket(player.id)

    # Simulate one hour tick
    await initLifeSim(websocket)

    # Check if age-appropriate events were checked
    assert len(player.events) > 0
```

---

## Common Pitfalls

### ❌ 1. Forgetting Year Suffix for Annual Events

```python
# BAD: Will only trigger once ever
def christmas(player, type='message'):
    fname = 'christmas'
    check = fname not in player.events and player.date == '12-25'

# GOOD: Will trigger every year
def christmas(player, type='message'):
    fname = f'christmas_{player.c.ageYears}'
    check = fname not in player.events and player.date == '12-25'
```

### ❌ 2. Not Checking player.events

```python
# BAD: Will trigger every hour on birthday
def birthday(player, type='message'):
    check = player.c.ageDays % 365 == 0

# GOOD: Only triggers once per year
def birthday(player, type='message'):
    fname = f'birthday_{player.c.ageYears}'
    check = fname not in player.askedQuestions and player.c.ageDays % 365 == 0
```

### ❌ 3. Appending to player.events in Event Function

```python
# BAD: Event function shouldn't modify player.events
def event(player, type='message'):
    if check:
        player.events.add(fname)  # DON'T DO THIS
        return messageFunction(...)

# GOOD: Game loop adds to player.events after sending
def event(player, type='message'):
    if check:
        return messageFunction(...)
# game_loop/loop_manager.py line 253:
#   player.events.add(result.id)  ← Game loop does this
```

### ❌ 4. Using Timestamps in Event IDs

```python
# BAD: Creates infinite growth
fname = f'event_{datetime.now().timestamp()}'  # New ID every call!

# GOOD: Use stable identifiers
fname = f'event_{player.c.ageYears}'  # Same ID for same year
```

### ❌ 5. Not Testing Probability Events

```python
# BAD: Event might never trigger in testing
def rareEvent(player, type='message'):
    check = random.random() < 0.001  # 0.1% chance

# GOOD: Make probability testable
def rareEvent(player, type='message', _test_force=False):
    check = _test_force or random.random() < 0.001

# In tests:
result = rareEvent(player, 'check', _test_force=True)
```

---

## Event Documentation Template

When adding new events, use this template in docstring:

```python
def eventName(player, type='message', message=False, response=False):
    """
    Brief description of what happens.

    **Triggers**:
    - Age: 18-25 years
    - Condition: Has job
    - Probability: 5% per year
    - Frequency: Can repeat annually

    **Effects**:
    - Happiness: +10
    - Money: -50
    - Adds relationship with coworker

    **Category**: Career
    **Type**: questionEvent
    **Added**: 2025-11-13
    **Author**: Your Name

    **Related Events**:
    - firstJob (prerequisite)
    - promotion (follow-up)
    """
    fname = f'eventName_{player.c.ageYears}'
    # ... implementation
```

---

## Summary & Next Steps

### ✅ Current Status: Ready for Scale

Your event system is well-designed for a full-life simulation:
- ✅ O(1) lookup performance (sets)
- ✅ ~15 KB memory per 100-year life
- ✅ Event registry with age-based filtering
- ✅ Clear deduplication patterns
- ✅ Backward-compatible migrations

### 📋 Recommended Next Steps

**Now** (Priority: High):
1. ✅ Use this guide for all new events
2. ✅ Document existing events with category comments
3. ✅ Add unit tests for critical events

**Soon** (Priority: Medium):
1. ⏸️ Reorganize into category folders (when you hit 150+ events)
2. ⏸️ Add event analytics table in SQL (when you need dashboards)
3. ⏸️ Create event editor tool for game designers

**Later** (Priority: Low):
1. ⏸️ Event expiry system (when you have 10,000+ events)
2. ⏸️ Event chains/prerequisites system
3. ⏸️ Dynamic event generation (AI-based)

### 🚀 Scale Goals

- **Current**: ~87 events → Works great
- **Target**: 500-1,000 events → Organized, maintainable
- **Future**: 10,000+ events → May need event expiry, DB indexing

**You're ready to scale confidently! 🎉**
