# app.py Refactoring - Line-by-Line Extraction Map

This document provides a precise mapping of which lines from app.py go to which new modules.

## Line Number Reference

### Current app.py (1,291 lines)

| Lines | Section | Action | Destination |
|-------|---------|--------|-------------|
| 1-32 | Imports | MODIFY | Keep in app.py (updated imports) |
| 34-135 | register_all_event_handlers() | MOVE | server/event_registration.py |
| 138-263 | register_all_events() | MOVE | server/event_registration.py |
| 267-299 | UserRegistry class | MOVE | server/websocket_registry.py |
| 301-335 | BatchedUpdate class | MOVE | server/websocket_registry.py |
| 337-344 | USERS, playerRecords, server instances | SPLIT | Some to registry, some stay |
| 346-353 | sendToUser() | MOVE | server/websocket_messaging.py |
| 355-357 | sendEventMessage() | MOVE | server/websocket_messaging.py |
| 358-360 | sendUserInfo() | MOVE | server/websocket_messaging.py |
| 361-363 | sendDict() | MOVE | server/websocket_messaging.py |
| 364-366 | ComplexHandler() | MOVE | server/websocket_messaging.py |
| 367-371 | get_websocket_for_player() | MOVE | server/websocket_messaging.py |
| 372-385 | iterateGames() | MOVE | server/websocket_messaging.py |
| 387-577 | initLifeSim() | KEEP/OPTIMIZE | Stay in app.py (extract helpers) |
| 578-580 | producer() | MOVE | game_loop/producer_consumer.py |
| 582-583 | TARGET_FPS, FRAME_DURATION | MOVE | game_loop/producer_consumer.py |
| 585-628 | producer_handler() | MOVE | game_loop/producer_consumer.py |
| 630-644 | consumer_handler() | MOVE | game_loop/producer_consumer.py |
| 646-1121 | consumer() | REFACTOR | 60 lines stay, 416 to command_dispatcher |
| 1123-1168 | start() | MOVE | server/websocket_handlers.py |
| 1170-1181 | shutdown() | MOVE | server/websocket_handlers.py |
| 1183-1188 | error() | MOVE | server/websocket_handlers.py |
| 1189-1201 | every_minute() | MOVE | server/websocket_handlers.py |
| 1203-1230 | handler() | MOVE | server/websocket_handlers.py |
| 1232-1244 | initialize_dummy_user() | MOVE | server/websocket_handlers.py |
| 1246-1292 | main() | KEEP | Stay in app.py (updated imports) |

---

## Phase 1: Event Registration & WebSocket Infrastructure

### server/event_registration.py

```python
#!/usr/bin/env python
"""
Event registration systems for game events and handlers.
Called once at server startup.
"""

# ========================================
# FROM app.py LINES 34-135
# ========================================
def register_all_event_handlers():
    """Register all valid event handlers"""
    # Event handlers from events.py
    register_event_handler("actTest", actTest)
    register_event_handler("actTestTake", actTestTake)
    # ... (all 60+ handlers from lines 37-92)

    # Event handlers from dayEvents.py
    register_event_handler("christmas", dayEvents.christmas)
    # ... (all 27 handlers from lines 95-121)

    # Event handlers from conversationEvents.py
    register_event_handler("conversationInit", conversationEvents.conversationInit)

    # Event handlers from tutorial_events.py
    register_event_handler("firstConversation", tutorial_events.firstConversation)
    # ... (all tutorial handlers from lines 126-129)

    from event_handlers import _registry
    print(f"Registered {len(_registry.list_events())} event handlers")

# ========================================
# FROM app.py LINES 138-263
# ========================================
def register_all_events():
    """Register all game events with conditions for efficient filtering"""

    # Early childhood events (age 0-5)
    register_event("learnedWalk", learnedWalk, age_range=(0, 5))
    # ... (all events from lines 142-256)

    logger.info(f"Registered {event_count()} game events in event registry")
    print(f"Registered {event_count()} game events in event registry")

# ========================================
# NEW FUNCTION
# ========================================
def initialize_all_events():
    """Single function to call both registration functions"""
    register_all_event_handlers()
    register_all_events()
```

**Lines moved:** 230 lines (34-135, 138-263)

---

### server/websocket_registry.py

```python
#!/usr/bin/env python
"""
WebSocket connection registry and batched update system.
Provides O(1) user lookup and efficient state updates.
"""

# ========================================
# FROM app.py LINES 267-299
# ========================================
class UserRegistry:
    """
    Efficient O(1) user lookup registry.

    Replaces O(n) set iteration with O(1) dictionary lookup.
    """

    def __init__(self):
        self._users = {}  # userID -> websocket

    def add(self, websocket) -> None:
        """Add a websocket connection"""
        if hasattr(websocket, 'userID'):
            self._users[websocket.userID] = websocket

    def remove(self, websocket) -> None:
        """Remove a websocket connection"""
        if hasattr(websocket, 'userID') and websocket.userID in self._users:
            del self._users[websocket.userID]

    def get(self, user_id: str):
        """Get websocket by user ID (O(1))"""
        return self._users.get(user_id)

    def count(self) -> int:
        """Get number of connected users"""
        return len(self._users)

    def __contains__(self, websocket) -> bool:
        """Check if websocket is registered"""
        if hasattr(websocket, 'userID'):
            return websocket.userID in self._users
        return False

# ========================================
# FROM app.py LINES 301-335
# ========================================
class BatchedUpdate:
    """
    Accumulator for batched game state updates.

    Instead of sending 14+ separate messages per tick,
    batch all updates into a single message.
    """

    def __init__(self):
        self._updates = {}

    def add(self, key: str, value: any) -> None:
        """Add an update to the batch"""
        self._updates[key] = value

    def to_dict(self) -> dict:
        """Convert to dictionary for sending"""
        if not self._updates:
            return None

        return {
            'type': 'batch_update',
            'updates': self._updates
        }

    def to_json(self) -> str:
        """Convert to JSON string"""
        data = self.to_dict()
        if data is None:
            return None
        return json.dumps(data, default=lambda o: o.__dict__)

    def is_empty(self) -> bool:
        """Check if batch is empty"""
        return len(self._updates) == 0

# ========================================
# FROM app.py LINES 341-344
# ========================================
class ServerState:
    """Global server state"""
    def __init__(self):
        self.ticks = 0

# ========================================
# MODULE-LEVEL INSTANCES
# ========================================
_USERS = UserRegistry()
_server = ServerState()

def get_user_registry() -> UserRegistry:
    """Get the global user registry"""
    return _USERS

def get_server_state() -> ServerState:
    """Get the global server state"""
    return _server
```

**Lines moved:** 78 lines (267-344)

---

### server/websocket_messaging.py

```python
#!/usr/bin/env python
"""
WebSocket messaging utilities for sending data to clients.
Handles JSON serialization and error handling.
"""

import asyncio
import json
import logging
from server.websocket_registry import get_user_registry

logger = logging.getLogger(__name__)
USERS = get_user_registry()

# ========================================
# FROM app.py LINES 346-353
# ========================================
async def sendToUser(websocket, message):
    """Send message to user (O(1) lookup)"""
    user = USERS.get(websocket.userID)
    if user:
        try:
            await user.send(message)
        except Exception as e:
            print(f"Error sending to {websocket.userID}: {e}")

# ========================================
# FROM app.py LINES 355-357
# ========================================
async def sendEventMessage(websocket, m):
    if websocket:
        await sendToUser(websocket, json.dumps(m.__dict__, default=lambda o: o.__dict__))

# ========================================
# FROM app.py LINES 358-360
# ========================================
async def sendUserInfo(player, websocket):
    if websocket:
        await sendToUser(websocket, json.dumps(player.__dict__, default=lambda o: o.__dict__))

# ========================================
# FROM app.py LINES 361-363
# ========================================
async def sendDict(websocket, obj):
    if websocket:
        await sendToUser(websocket, json.dumps(obj, default=lambda o: o.__dict__))

# ========================================
# FROM app.py LINES 364-366
# ========================================
def ComplexHandler(Obj):
    if hasattr(Obj, 'jsonable'):
        return Obj.jsonable()

# ========================================
# FROM app.py LINES 367-371
# ========================================
def get_websocket_for_player(player_id):
    for ws in USERS:
        if hasattr(ws, 'userID') and ws.userID == player_id:
            return ws
    return None

# ========================================
# FROM app.py LINES 372-385
# ========================================
async def iterateGames():
    """Iterate through offline games and update them"""
    from functions import loadGameAsync, loadGames
    from player_cache import PlayerCache

    # Need to import these to avoid circular dependency
    from app import playerRecords, initLifeSim

    games = loadGames()
    if games and len(games) > 0:
        for game in games:
            cached_player = playerRecords.get(game[0])
            if cached_player is None or cached_player.connection == 'disconnected':
                foundGame = await loadGameAsync(game[0])
                if foundGame.c.status == "alive":
                    print('iterating game ' + foundGame.c.firstname + ' ' + foundGame.c.lastname + ' ' + str(foundGame.minuteOfHour) + 'min')
                    await initLifeSim(False, foundGame)
```

**Lines moved:** 40 lines (346-385)

**Note:** iterateGames has circular dependency with app.py's initLifeSim. Consider:
1. Keep iterateGames in app.py, or
2. Move initLifeSim to game_loop module, or
3. Pass initLifeSim as parameter to iterateGames

---

## Phase 2: Game Loop & WebSocket Handlers

### game_loop/producer_consumer.py

```python
#!/usr/bin/env python
"""
Producer/consumer pattern for game loop execution.
Handles FPS throttling and rate limiting.
"""

import asyncio
import time
import traceback
import websockets
from rate_limiter import RateLimiter
from config import config

# ========================================
# FROM app.py LINES 582-583
# ========================================
TARGET_FPS = 5000
FRAME_DURATION = 1.0 / TARGET_FPS

# ========================================
# FROM app.py LINES 578-580
# ========================================
async def producer(websocket):
    """Call game loop for this websocket's player"""
    # Import here to avoid circular dependency
    from app import initLifeSim
    await initLifeSim(websocket)
    return False

# ========================================
# FROM app.py LINES 585-628
# ========================================
async def producer_handler(websocket):
    """Main producer loop with FPS control"""
    from app import playerRecords

    start_time = time.time()
    frame_count = 0

    while True:
        try:
            player = playerRecords.get(websocket.userID)
            if player and player.connection == 'disconnected' and websocket.userID != 'DUMMY_USER_ID':
                break

            frame_start_time = time.time()
            await producer(websocket)
            frame_end_time = time.time()
            frame_duration = frame_end_time - frame_start_time

            frame_count += 1
            elapsed_time = frame_end_time - start_time

            sleep_duration = FRAME_DURATION - frame_duration
            if elapsed_time >= 1/60:
                await asyncio.sleep(0.001)
                start_time = frame_end_time
                frame_count = 0

            if elapsed_time >= 1.0:
                player = playerRecords.get(websocket.userID)
                if player:
                    player.fps = frame_count / elapsed_time
                    print("FPS: " + str(frame_count / elapsed_time) + ' ' + str(type(player)) + ' ' + str(player.c.firstname) + ' ' + str(player.c.lastname))

        except websockets.exceptions.ConnectionClosed as err:
            print("producer:Client disconnected.  Do cleanup" + str(err))
            from server.websocket_handlers import shutdown
            await shutdown(websocket)
            break
        except Exception as e:
            print("Error in producer_handler: " + traceback.format_exc())
            await websocket.close()
            from server.websocket_handlers import shutdown
            await shutdown(websocket)
            break

# ========================================
# FROM app.py LINES 630-644
# ========================================
async def consumer_handler(websocket):
    """Main consumer loop with rate limiting"""
    from app import consumer

    # Initialize rate limiter
    rate_limiter = RateLimiter(
        max_requests=config.WEBSOCKET_MAX_MESSAGES_PER_MINUTE,
        window_seconds=60
    )

    async for message in websocket:
        # Check rate limit
        if not rate_limiter.is_allowed(websocket.userID):
            print(f"Rate limit exceeded for {websocket.userID}")
            from server.websocket_handlers import error
            await error(websocket, "Rate limit exceeded. Please slow down.")
            continue

        await consumer(message, websocket)
```

**Lines moved:** 68 lines (578-644)

**Note:** Circular dependencies with app.py. Consider passing functions as parameters or moving initLifeSim and consumer to this module.

---

### game_loop/loop_manager.py

```python
#!/usr/bin/env python
"""
Game loop helper functions.
Main loop stays in app.py for performance.
"""

from server.websocket_registry import BatchedUpdate

# ========================================
# EXTRACTED FROM app.py LINES 414-432
# ========================================
def create_hourly_batch_update(player):
    """
    Create batched update object for hourly ticks.
    Called when minuteOfHour == 0
    """
    batch = BatchedUpdate()
    batch.add('date', player.date)
    batch.add('hourOfDay', player.hourOfDay)
    batch.add('minuteOfHour', player.minuteOfHour)
    batch.add('weekDayText', player.weekDayText)
    batch.add('energy', player.c.energy)
    batch.add('calcEnergy', player.c.calcEnergy)
    batch.add('money', player.c.money)
    batch.add('diamonds', player.c.diamonds)
    batch.add('prestige', player.c.prestige)
    batch.add('stress', player.c.stress)
    batch.add('happiness', player.c.happiness)
    batch.add('occupation', player.c.occupation)
    batch.add('location', player.c.location)
    batch.add('schedules', player.c.schedules)
    batch.add('intraDayMessage', player.c.intraDayMessage)
    batch.add('dailyPlan', player.c.dailyPlan)
    batch.add('gameSpeed', player.gameSpeed)
    return batch.to_dict()

# ========================================
# EXTRACTED FROM app.py LINES 442-489
# ========================================
async def process_daily_tick(player, websocket):
    """
    Process daily tick logic (hourOfDay == 24).
    Returns messageEvent if any.
    """
    from functions import (
        getPeakEnergy, get_dailyPlan, updateDeathChance,
        checkDayEvents, saveGameAsync
    )
    from server.websocket_messaging import sendUserInfo, sendEventMessage
    import random

    player.hourOfDay = 0
    if player.dayOfYear == 365:
        player.dayOfYear = 1
    else:
        player.dayOfYear += 1

    if player.dayOfWeek == 7:
        player.dayOfWeek = 1
    else:
        player.dayOfWeek += 1

    # Update date
    from datetime import date
    player.date = date.fromordinal(date(2022, 1, 1).toordinal() + player.dayOfYear - 1).strftime('%m-%d')
    player.monthOfYear = int(player.date.split('-')[0])
    from utils.helpers import get_season
    player.season = get_season(player.monthOfYear)
    player.date = str(player.date)
    player.time = str(player.hourOfDay) + ":00"
    player.weekDayText = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][player.dayOfWeek-1]

    # School calculations
    player.daysUntilSchoolEnds = (152 - player.dayOfYear)
    if player.dayOfYear > 244:
        player.daysUntilSchoolEnds = (365-244) + player.daysUntilSchoolEnds
    player.daysSinceSchoolStarted = (player.dayOfYear - 244)
    if player.dayOfYear < 244:
        player.daysSinceSchoolStarted = (121 + player.dayOfYear)

    # Summer vacation check
    if player.dayOfYear > 152 and player.dayOfYear < 244:
        player.summerVacation = True
    else:
        player.summerVacation = False

    # Send random character message
    from functions import sendRandomCharacterMessage
    await sendRandomCharacterMessage(player)

    # Reset daily event flag
    player.dayEvent = False

    # Restore energy
    getPeakEnergy(player.c)
    player.c.energy += 1 if player.c.energy < 100 else 0

    # Birthday message
    if player.c.ageDays == 1 and player.c.firstname:
        player.message = player.c.firstname.capitalize() + " is starting their life, full of opportunities."
        await sendUserInfo(player, websocket)

    # Age up
    if player.c.ageDays > 0 and player.c.ageDays % 365 == 0:
        player.c.ageYears += 1
        player.c.deathChance = updateDeathChance(player.c)
        player.message = player.c.firstname.capitalize() + " is " + str(player.c.ageYears) + " years old."
        await saveGameAsync(player)
        await sendUserInfo(player, websocket)

    # Death check
    if player.c.deathChance * 100 > (random.random() * 100) or player.c.ageYears > 120:
        player.c.status = "dead"
        player.message = player.c.firstname.capitalize() + " has died at the age of " + str(player.c.ageYears) + " years."
        await sendUserInfo(player, websocket)

    # Check day events
    result = checkDayEvents(player, 'check')
    if result:
        if result.type == 'messageEvent':
            player.events.append(result.id)
            await sendEventMessage(websocket, result)
        elif result.type == 'questionEvent':
            await sendEventMessage(websocket, result)

    # Generate daily plan
    player.c = get_dailyPlan(player, player.c)

    # Update relationships familiarity
    for i in range(len(player.r)):
        if player.r[i].status == "alive" and player.r[i].familiarity > 0:
            player.r[i].familiarity = player.r[i].familiarity - 3
        player.r[i] = get_dailyPlan(player, player.r[i])

# ========================================
# EXTRACTED FROM app.py LINES 471-489
# ========================================
async def process_weekly_tick(player, websocket):
    """
    Process weekly tick logic (dayOfWeek == 1 && hourOfDay == 0).
    """
    from functions import (
        handleFinances, handleMoods, handleEducation, handleJob,
        handleRelationships, set_avatar, handleHabitChanges,
        saveGameAsync
    )
    from server.websocket_messaging import sendUserInfo

    # Process all relationships
    for i in range(len(player.r)):
        handleFinances(player.r[i])
        handleMoods(player, player.r[i])
        handleEducation(player.r[i])
        handleJob(player, player.r[i])
        handleRelationships(player, player.r[i])
        player.r[i].image = set_avatar(player.r[i])

    # Process player character
    handleFinances(player.c)
    handleMoods(player, player.c)
    handleRelationships(player, player.c)
    handleJob(player, player.c)
    handleEducation(player.c)
    handleHabitChanges(player, player.c)
    player.c.image = set_avatar(player.c)

    # Save game
    await saveGameAsync(player)
    print('weekly tick')
    print(player.messageLog)
    await sendUserInfo(player, websocket)
```

**Lines moved:** ~250 lines (helper functions extracted from game loop)

---

### server/websocket_handlers.py

```python
#!/usr/bin/env python
"""
WebSocket connection lifecycle handlers.
Manages connection, disconnection, and background tasks.
"""

import asyncio
import json
import datetime
import websockets
from config import config
from server.websocket_registry import get_user_registry
from server.websocket_messaging import sendUserInfo, sendDict, iterateGames
from player_cache import PlayerCache

USERS = get_user_registry()

# These are imported from app to avoid circular dependency
# Will be set during module initialization
playerRecords = None

def set_player_records(records):
    """Set the playerRecords cache (called from app.py)"""
    global playerRecords
    playerRecords = records

# ========================================
# FROM app.py LINES 1183-1188
# ========================================
async def error(websocket, message):
    """Send error message to client"""
    event = {
        "type": "error",
        "message": message,
    }
    await websocket.send(json.dumps(event))

# ========================================
# FROM app.py LINES 1170-1181
# ========================================
async def shutdown(websocket):
    """Cleanup on disconnect"""
    from functions import saveGameAsync

    print(websockets.exceptions.ConnectionClosed)
    print("Client disconnected.  Do cleanup ")
    player = playerRecords.get(websocket.userID)
    if player:
        player.offlineStats.minutesOffline = 0
        player.connection = 'disconnected'
        player.controller = 'inactive'
        await saveGameAsync(player)

        USERS.remove(websocket)
        return True

# ========================================
# FROM app.py LINES 1189-1201
# ========================================
async def every_minute():
    """Background task for offline game iteration"""
    while True:
        print('every 5 seconds, checking for games')
        await iterateGames()

        # Log cache stats
        stats = playerRecords.get_stats()
        print(f"PlayerCache: {stats['size']}/{stats['max_size']} players "
              f"({stats['connected']} connected, {stats['disconnected']} disconnected), "
              f"{stats['memory_mb']:.1f}MB")

        await asyncio.sleep(5)

# ========================================
# FROM app.py LINES 1123-1168
# ========================================
async def start(websocket):
    """Initialize connection and start game loops"""
    from functions import loadGameAsync, insertGame, connect, getOccupations
    from core.models import playerClass
    from game_loop.producer_consumer import consumer_handler, producer_handler

    connected = {websocket}
    print('starting connection...')
    player = False

    cached_player = playerRecords.get(websocket.userID)
    if cached_player is None:
        print('attempting load for ' + websocket.userID)
        player = await loadGameAsync(websocket.userID)
        if player:
            if player.gameSpeed == config.SPEED_QUESTION_PAUSE:
                player.gameSpeed = config.SPEED_DEFAULT
            player.controller = 'active'
            player.connection = 'connected'
            connect(player)
            playerRecords.set(websocket.userID, player)
            print('loaded game')

        if not player:
            print('no loaded games, no saved game -- creating new game')
            player = playerClass()
            player.occupations = getOccupations()
            player.id = websocket.userID
            connect(player)
            insertGame(player)
            playerRecords.set(websocket.userID, player)
    else:
        player = cached_player
        print('reconnected ' + player.c.firstname + ' ' + player.c.lastname + ' age ' + str(player.c.ageYears))
        if player.gameSpeed == config.SPEED_QUESTION_PAUSE:
            player.gameSpeed = config.SPEED_DEFAULT
        player.controller = 'active'
        connect(player)

    await sendUserInfo(player, websocket)
    print(f"USERS: {USERS.count()} Players: {playerRecords.size()}")

    # Check daily login rewards on connect
    from retention.daily_rewards import handle_daily_login_check
    def send_to_client_wrapper(player_id, message):
        asyncio.create_task(sendDict(websocket, message))
    handle_daily_login_check(player.id, send_to_client_wrapper)

    await asyncio.gather(
        consumer_handler(websocket),
        producer_handler(websocket),
    )

# ========================================
# FROM app.py LINES 1203-1230
# ========================================
async def handler(websocket):
    """
    Handle a connection and dispatch it according to who is connecting.
    """
    try:
        print(datetime.datetime.now())
        message = await websocket.recv()
        event = json.loads(message)
        if event["type"] == "init":
            websocket.userID = event["userID"]
            print("Client connected with userID " + websocket.userID)
            USERS.add(websocket)
            await start(websocket)
        else:
            websocket.userID = event["userID"]
            print("Client connected with userID " + websocket.userID)
            USERS.add(websocket)
            await start(websocket)
            await error(websocket, "First message must be 'init'")
            await websocket.close()
            return False
        await websocket.close()
    except websockets.exceptions.ConnectionClosed as err:
        print("handler: Client disconnected.  Do cleanup" + str(err))
        await shutdown(websocket)
        return False

# ========================================
# FROM app.py LINES 1232-1244
# ========================================
async def initialize_dummy_user():
    """Initialize test connection"""
    uri = "ws://localhost:8001"
    async with websockets.connect(uri) as websocket:
        dummy_message = {"type": "test", "userID": "DUMMY_USER_ID"}
        try:
            await websocket.send(json.dumps(dummy_message))
            await websocket.close()
        except websockets.exceptions.ConnectionClosed as err:
            print("initialize_dummy_user: Client disconnected.  Do cleanup" + str(err))
            await shutdown(websocket)
    return False
```

**Lines moved:** 170 lines (1123-1244)

---

## Phase 3: Command Dispatcher (CRITICAL)

### server/command_dispatcher.py

This is the biggest and most complex extraction. The file will be ~500 lines and contain 40+ command handler classes.

**Structure:**

```python
#!/usr/bin/env python
"""
Command dispatch system for WebSocket messages.
Converts massive if/elif chain into table-driven routing.
"""

import asyncio
import json
import logging
from typing import Dict, Any

logger = logging.getLogger(__name__)

# ========================================
# BASE CLASS
# ========================================
class CommandHandler:
    """Base class for command handlers"""

    async def handle(self, player, event, websocket):
        """Handle the command - must be overridden"""
        raise NotImplementedError

# ========================================
# SIMPLE COMMAND HANDLERS
# FROM app.py LINES 691-706
# ========================================

class StopCommandHandler(CommandHandler):
    """Handle stop command (lines 691-694)"""
    async def handle(self, player, event, websocket):
        from server.websocket_messaging import sendUserInfo
        await sendUserInfo(player, websocket)
        print('stopped!')
        player.controller = 'inactive'

class StartCommandHandler(CommandHandler):
    """Handle start command (lines 695-697)"""
    async def handle(self, player, event, websocket):
        print('started!')
        player.controller = "active"

class RestartCommandHandler(CommandHandler):
    """Handle restart command (lines 698-706)"""
    async def handle(self, player, event, websocket):
        from core.models import playerClass
        from functions import getOccupations
        from server.websocket_messaging import sendUserInfo
        from app import playerRecords

        print('restart!')
        player = playerClass()
        player.occupations = getOccupations()
        player.id = websocket.userID
        playerRecords.set(websocket.userID, player)
        await sendUserInfo(player, websocket)
        player.updateClient = True

# ... (Continue for all 40+ command types) ...
# Each command from lines 707-1068 becomes a handler class

# ========================================
# COMMAND DISPATCHER
# ========================================
class CommandDispatcher:
    """
    Dispatch commands to appropriate handlers.
    Replaces 476-line if/elif chain with table lookup.
    """

    def __init__(self):
        self._handlers: Dict[str, CommandHandler] = {}
        self._register_handlers()

    def _register_handlers(self):
        """Register all command handlers"""
        # Simple commands (message field)
        self._handlers['stop'] = StopCommandHandler()
        self._handlers['start'] = StartCommandHandler()
        self._handlers['restart'] = RestartCommandHandler()

        # Type-based commands
        self._handlers['characterSetup'] = CharacterSetupCommandHandler()
        # ... (register all 40+ handlers)

    async def dispatch(self, event: Dict[str, Any], player, websocket):
        """
        Dispatch command to appropriate handler.
        Returns True if handled, False otherwise.
        """
        # Check 'message' field first
        if event.get('message') in self._handlers:
            handler = self._handlers[event.get('message')]
            await handler.handle(player, event, websocket)
            return True

        # Check 'type' field
        if event.get('type') in self._handlers:
            handler = self._handlers[event.get('type')]
            await handler.handle(player, event, websocket)
            return True

        # Fallback to event handler system (lines 1069-1121)
        return await self._handle_event_dispatch(event, player, websocket)

    async def _handle_event_dispatch(self, event, player, websocket):
        """Handle event-based dispatch (lines 1069-1121)"""
        # ... (event dispatch logic)

# Global dispatcher
_dispatcher = CommandDispatcher()

async def dispatch_command(event: Dict[str, Any], player, websocket) -> bool:
    """Dispatch a command to the appropriate handler"""
    return await _dispatcher.dispatch(event, player, websocket)
```

**Lines moved:** 416 lines (691-1068, 1070-1117)

**List of all command handlers to create:**

1. StopCommandHandler (691-694)
2. StartCommandHandler (695-697)
3. RestartCommandHandler (698-706)
4. CharacterSetupCommandHandler (707-711)
5. DeviceTokenCommandHandler (712-715)
6. GetExtraCurricularsCommandHandler (716-717)
7. ApplyForExtracurricularCommandHandler (718-722)
8. QuitExtracurricularCommandHandler (723-726)
9. RetrievePersonCommandHandler (727-735)
10. ConversationCommandHandler (736-774)
11. MarkConversationAsReadCommandHandler (775-777)
12. SpeedCommandHandler (778-838) - **COMPLEX**
13. FocusUpdateCommandHandler (839-842)
14. QuitHabitCommandHandler (843-849)
15. StopQuitHabitCommandHandler (843-849)
16. PurchaseItemCommandHandler (850-853)
17. PurchaseInAppItemCommandHandler (854-857)
18. PurchaseEnergyRefillCommandHandler (858-869)
19. PurchaseTimeSkipCommandHandler (870-881)
20. GetEnergyRefillTiersCommandHandler (882-885)
21. GetTimeSkipTiersCommandHandler (887-890)
22. GetAchievementsCommandHandler (892-895)
23. AcknowledgeAchievementCommandHandler (897-902)
24. GetDailyRewardsCommandHandler (904-908)
25. ClaimDailyRewardCommandHandler (910-915)
26. GetDailyQuestsCommandHandler (917-921)
27. ClaimQuestRewardCommandHandler (923-935)
28. GetPlayerStatisticsCommandHandler (937-940)
29. ExportDataCommandHandler (942-964)
30. DeleteAccountCommandHandler (966-997)
31. ApplyForJobCommandHandler (999-1003)
32. QuitJobCommandHandler (1004-1007)
33. RomanceCommandHandler (1008-1012)
34. DateNightCommandHandler (1013-1025)
35. BreakUpCommandHandler (1026-1029)
36. DivorceCommandHandler (1026-1029) - same as BreakUp
37. PartnerGiftCommandHandler (1030-1042)
38. ResetSpeedCommandHandler (1044-1045)
39. SwipeMatchCommandHandler (1046-1052)
40. GetSwipeCharacterCommandHandler (1053-1068)
41. EventDispatchHandler (1070-1117) - fallback for event system

---

## Updated app.py (Final)

After all extractions, app.py will look like:

```python
#!/usr/bin/env python
"""
BaoLife WebSocket Server - Main Entry Point

Lines reduced from 1,291 to ~350-450
"""

# ========================================
# IMPORTS (~40 lines)
# ========================================
import asyncio
import datetime
import time
import json
import os
import signal
import logging
import websockets

# Server infrastructure
from server.event_registration import initialize_all_events
from server.websocket_registry import get_user_registry, get_server_state, BatchedUpdate
from server.websocket_messaging import sendToUser, sendEventMessage, sendUserInfo, sendDict
from server.command_dispatcher import dispatch_command
from server.websocket_handlers import handler, every_minute, initialize_dummy_user, set_player_records

# Game loop
from game_loop.producer_consumer import producer_handler, consumer_handler
from game_loop.loop_manager import create_hourly_batch_update, process_daily_tick, process_weekly_tick

# Existing modules
from functions import *
from events import *
from intradayActivity import *
from player_cache import PlayerCache
from config import config
from event_registry import get_applicable_events

logger = logging.getLogger(__name__)

# ========================================
# INITIALIZATION (~10 lines)
# ========================================

# Register all events and handlers
initialize_all_events()
print('startup')

# Initialize registries
USERS = get_user_registry()
server = get_server_state()
playerRecords = PlayerCache(max_size=config.MAX_CONNECTIONS)
lastIteration = False

# Share playerRecords with other modules
set_player_records(playerRecords)

# ========================================
# GAME LOOP (~150 lines)
# PERFORMANCE CRITICAL - KEEP IN app.py
# ========================================

async def initLifeSim(websocket, oneTimePlayer=False):
    """
    Main game loop - runs every tick.

    PERFORMANCE CRITICAL: This is the hot path.
    Kept in app.py for minimal call overhead.
    """
    # Check for dummy user
    if websocket.userID == 'DUMMY_USER_ID':
        return False

    # Get player
    player = False
    if oneTimePlayer:
        player = oneTimePlayer
    else:
        player = playerRecords.get(websocket.userID)

    # Increment ticks
    player.ticks += 1

    # Check if time to update
    if player.ticks % player.gameSpeed == 0 or oneTimePlayer:
        if (player.controller == 'active' and player.status != "creating") or oneTimePlayer:
            # Check if player is dead
            if player.c.status == "dead":
                handleDeath(player)
                await saveGameAsync(player)
                return False

            # Increment time
            player.minuteOfHour += 1
            player.time = str(player.hourOfDay) + ":" + str(player.minuteOfHour)

            if player.minuteOfHour == 60:
                player.minuteOfHour = 0

            if player.minuteOfHour == 0:  # Hourly ticks
                # Create batched update (extracted to helper)
                updateObject = create_hourly_batch_update(player)
                player.message = False
                player.hourOfDay += 1

                # Update age
                result = updateAge(player)
                if result.type == 'messageEvent':
                    await sendEventMessage(websocket, result)

                # Daily tick
                if player.hourOfDay == 24:
                    await process_daily_tick(player, websocket)

                # Weekly tick
                if player.dayOfWeek == 1 and player.hourOfDay == 0:
                    await process_weekly_tick(player, websocket)

                # Hourly activities
                player.c = getIntradayActivity(player, player.c)
                for i in range(len(player.r)):
                    player.r[i] = getIntradayActivity(player, player.r[i])

                player = parseLocations(player)
                parseOneTimeEvents(player)
                player = updateBio(player)

                # Handle message queue
                if player.messageQueue and len(player.messageQueue) > 0:
                    player.message = player.messageQueue.pop(0)
                    player.messageLog.append(player.message)
                    await sendEventMessage(websocket, messageFunction('queue', player.message, player, True))
                    player.message = ""

                # Update client if needed
                if player.updateClient:
                    await sendUserInfo(player, websocket)
                    player.updateClient = False

                # Check events (not while paused)
                if player.gameSpeed < config.SPEED_QUESTION_PAUSE:
                    # Check tutorial events first
                    if result := checkTutorialEvents(player, 'check'):
                        if result.type == 'messageEvent':
                            player.events.append(result.id)
                            await sendEventMessage(websocket, result)
                        elif result.type == 'questionEvent':
                            await sendEventMessage(websocket, result)
                    # Then check regular events
                    else:
                        applicable_events = get_applicable_events(player)
                        for event_info in applicable_events:
                            try:
                                result = event_info['func'](player, 'check')
                                if result:
                                    if result.type == 'messageEvent':
                                        player.events.append(result.id)
                                        await sendEventMessage(websocket, result)
                                    elif result.type == 'questionEvent':
                                        await sendEventMessage(websocket, result)
                                    break
                            except Exception as e:
                                logger.error(f"Error in event {event_info['id']}: {e}")

                # Check dilemmas
                if result := checkDilemmas(player):
                    await sendEventMessage(websocket, result)

                # Send updates
                updateObject = handleUpdates(updateObject, player, websocket)
                if updateObject and updateObject != {}:
                    await sendDict(websocket, updateObject)

            elif player.gameSpeed > 10:
                # Fast mode - just send minute update
                await sendDict(websocket, {'type': 'u', 'minuteOfHour': player.minuteOfHour})

            # Handle offline player
            if oneTimePlayer:
                player.connection = 'disconnected'
                player.controller = 'inactive'
                player.offlineStats.minutesOffline += 1
                print("offline for " + str(player.offlineStats.minutesOffline) + " minutes, saving game")
                await saveGameAsync(player)

    return False

# ========================================
# CONSUMER (~60 lines)
# SIMPLIFIED - routing delegated to command_dispatcher
# ========================================

async def consumer(message, websocket):
    """
    Handle incoming WebSocket messages.
    Simplified from 476 lines - routing delegated to command_dispatcher.
    """
    player = playerRecords.get(websocket.userID)

    # Create update object
    updateObject = {
        'date': player.date,
        'hourOfDay': player.hourOfDay,
        'minuteOfHour': player.minuteOfHour,
        'weekDayText': player.weekDayText,
        'energy': player.c.energy,
        'calcEnergy': player.c.calcEnergy,
        'money': player.c.money,
        'diamonds': player.c.diamonds,
        'prestige': player.c.prestige,
        'stress': player.c.stress,
        'happiness': player.c.happiness,
        'occupation': player.c.occupation,
        'location': player.c.location,
        'schedules': player.c.schedules,
        'intraDayMessage': player.c.intraDayMessage,
        'dailyPlan': player.c.dailyPlan,
        'gameSpeed': player.gameSpeed,
    }

    if not message:
        print("no message")
        return False

    if isinstance(message, bytes):
        message = message.decode('utf-8')

    print('received' + message)
    event = json.loads(message)

    # Dispatch to command handler (all the if/elif logic moved to command_dispatcher)
    await dispatch_command(event, player, websocket)

    # Handle message queue
    if player.messageQueue and len(player.messageQueue) > 0:
        player.message = player.messageQueue.pop(0)
        player.messageLog.append(player.message)
        print('sending message from queue ' + player.message)
        await sendEventMessage(websocket, messageFunction('answerQueue', player.message, player, True))
        player.message = ""

    # Send updates
    updateObject = handleUpdates(updateObject, player, websocket)
    if updateObject and updateObject != {}:
        await sendDict(websocket, updateObject)

    return True

# ========================================
# MAIN ENTRY POINT (~50 lines)
# ========================================

async def main():
    """Main entry point"""
    loop = asyncio.get_running_loop()
    stop = loop.create_future()

    loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)

    # Initialize async database pool
    from database_async import initialize_pool, close_pool
    await initialize_pool(pool_size=config.MAX_CONNECTIONS)
    print(f"Database connection pool initialized: size={config.MAX_CONNECTIONS}")

    # Initialize retention systems
    print("Initializing retention systems...")
    from retention.daily_quests import initialize_quest_templates
    from retention.daily_rewards import initialize_daily_rewards
    initialize_quest_templates()
    initialize_daily_rewards()
    print("Retention systems initialized")

    port = int(os.environ.get("PORT", "8001"))
    print('running...')
    websocket_server = websockets.serve(handler, "", port)

    # Initialize tasks
    tasks = asyncio.gather(
        websocket_server,
        every_minute()
    )

    await asyncio.sleep(1)
    await initialize_dummy_user()
    await stop

    # Cleanup
    await close_pool()
    print("Database pool closed")

    tasks.cancel()
    await asyncio.gather(tasks, return_exceptions=True)

if __name__ == "__main__":
    asyncio.run(main())
```

**Final line count: ~350-450 lines** (down from 1,291)

---

## Summary of Extractions

| Module | Lines Moved | Lines Remaining | Files Created |
|--------|-------------|-----------------|---------------|
| Event Registration | 230 | 5 | server/event_registration.py |
| WebSocket Registry | 78 | 10 | server/websocket_registry.py |
| WebSocket Messaging | 40 | 4 | server/websocket_messaging.py |
| Producer/Consumer | 68 | 3 | game_loop/producer_consumer.py |
| Loop Helpers | 40 | 0 | game_loop/loop_manager.py |
| WebSocket Handlers | 170 | 50 | server/websocket_handlers.py |
| Command Dispatcher | 416 | 60 | server/command_dispatcher.py |
| Game Loop | 0 | 150 | (stays in app.py - optimized) |
| Main | 0 | 50 | (stays in app.py) |
| **TOTAL** | **~1,042** | **~332** | **7 new files** |

**Final app.py: ~350-450 lines** (including some overhead)

---

## Circular Dependency Resolution

Several circular dependencies need to be resolved:

1. **iterateGames → initLifeSim**
   - Solution: Pass initLifeSim as parameter or keep iterateGames in app.py

2. **producer → initLifeSim**
   - Solution: Import inside function or pass as parameter

3. **consumer_handler → consumer**
   - Solution: Import inside function

4. **Various handlers → playerRecords**
   - Solution: Use set_player_records() to share instance

**Recommended approach:** Import inside functions where needed to break circular dependencies.

---

## Next Steps

1. Review this extraction map
2. Create feature branch: `refactor/app-modularization`
3. Create directory structure:
   ```bash
   mkdir -p ws/server
   mkdir -p ws/game_loop
   touch ws/server/__init__.py
   touch ws/game_loop/__init__.py
   ```
4. Start with Phase 1 (low-risk extractions)
5. Test after each extraction
6. Commit frequently with clear messages
