# Node.js Backend Rewrite Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Rewrite the BaoLife Python WebSocket backend to Node.js/TypeScript with full protocol compatibility.

**Architecture:** Table-driven command dispatcher with per-player game loops running at 10ms ticks. Event system mirrors Python structure with 13 event categories. All message types preserved for zero iOS changes.

**Tech Stack:** Node.js 20+, TypeScript (strict), ws, mysql2/promise, zod, OpenAI SDK, Vitest

**Reference:** See `docs/plans/2025-12-25-nodejs-backend-rewrite-design.md` for full architecture details.

**Python Source:** `../lichun/ws/` (reference for porting logic)

---

## Phase 1: Project Setup

### Task 1.1: Initialize Node.js Project

**Files:**
- Create: `server/package.json`
- Create: `server/tsconfig.json`
- Create: `server/.gitignore`
- Create: `server/.env.example`

**Step 1: Create server directory and initialize npm**

```bash
mkdir -p server
cd server
npm init -y
```

**Step 2: Install dependencies**

```bash
npm install ws mysql2 zod openai dotenv uuid
npm install -D typescript @types/node @types/ws vitest tsx @vitest/coverage-v8
```

**Step 3: Create tsconfig.json**

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}
```

**Step 4: Create package.json scripts**

Update package.json scripts section:
```json
{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "test": "vitest",
    "test:coverage": "vitest run --coverage"
  }
}
```

**Step 5: Create .gitignore**

```
node_modules/
dist/
.env
coverage/
*.log
```

**Step 6: Create .env.example**

```
PORT=8001
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=
DB_NAME=baolife
OPENAI_API_KEY=
DEBUG=false
```

**Step 7: Commit**

```bash
git add server/
git commit -m "feat(server): initialize Node.js/TypeScript project"
```

---

### Task 1.2: Create Directory Structure

**Files:**
- Create: `server/src/index.ts` (placeholder)
- Create: `server/src/config.ts`
- Create directory structure

**Step 1: Create all directories**

```bash
mkdir -p server/src/{server,game,models,events,handlers,services,monetization,retention,dating,database,utils}
mkdir -p server/src/events/{tutorial,childhood,adolescence,adulthood,education,health,activities,holidays,conversations,dilemmas,negative,random,school_year}
mkdir -p server/tests/{unit,integration,fixtures}
```

**Step 2: Create config.ts**

```typescript
// server/src/config.ts
import dotenv from 'dotenv';
dotenv.config();

export const config = {
  // Server
  PORT: parseInt(process.env.PORT ?? '8001', 10),

  // Database
  DB_HOST: process.env.DB_HOST ?? 'localhost',
  DB_PORT: parseInt(process.env.DB_PORT ?? '3306', 10),
  DB_USER: process.env.DB_USER ?? 'root',
  DB_PASSWORD: process.env.DB_PASSWORD ?? '',
  DB_NAME: process.env.DB_NAME ?? 'baolife',

  // AI
  OPENAI_API_KEY: process.env.OPENAI_API_KEY ?? '',
  CONVERSATION_MODEL: process.env.CONVERSATION_MODEL ?? 'gpt-4o-mini',

  // Game
  TICK_INTERVAL: 10, // ms (100 ticks/sec)
  SPEED_PAUSED: 10000,
  SPEED_DEFAULT: 10000,
  SPEED_BUTTON_VALUES: [10000, 1000, 500, 50, 20, 1] as const,

  // Limits
  MAX_CONNECTIONS: 20,
  RATE_LIMIT_PER_MINUTE: 30,

  // Debug
  DEBUG: process.env.DEBUG === 'true',
} as const;

export type Config = typeof config;
```

**Step 3: Create placeholder index.ts**

```typescript
// server/src/index.ts
import { config } from './config.js';

console.log(`BaoLife Server starting on port ${config.PORT}...`);
```

**Step 4: Verify TypeScript compiles**

```bash
cd server && npm run build
```

Expected: No errors, dist/ folder created

**Step 5: Commit**

```bash
git add server/
git commit -m "feat(server): add config and directory structure"
```

---

### Task 1.3: Setup Vitest

**Files:**
- Create: `server/vitest.config.ts`
- Create: `server/tests/unit/config.test.ts`

**Step 1: Create vitest.config.ts**

```typescript
// server/vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['tests/**/*.test.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      include: ['src/**/*.ts'],
      exclude: ['src/index.ts'],
    },
  },
});
```

**Step 2: Create first test**

```typescript
// server/tests/unit/config.test.ts
import { describe, it, expect } from 'vitest';
import { config } from '../../src/config.js';

describe('config', () => {
  it('should have default port 8001', () => {
    expect(config.PORT).toBe(8001);
  });

  it('should have tick interval of 10ms', () => {
    expect(config.TICK_INTERVAL).toBe(10);
  });

  it('should have 6 speed values', () => {
    expect(config.SPEED_BUTTON_VALUES).toHaveLength(6);
  });
});
```

**Step 3: Run tests**

```bash
cd server && npm test
```

Expected: 3 tests pass

**Step 4: Commit**

```bash
git add server/
git commit -m "feat(server): setup Vitest testing framework"
```

---

## Phase 2: Database Layer

### Task 2.1: Create Connection Pool

**Files:**
- Create: `server/src/database/pool.ts`
- Create: `server/tests/unit/database/pool.test.ts`

**Step 1: Write failing test**

```typescript
// server/tests/unit/database/pool.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';

// Mock mysql2/promise before importing pool
vi.mock('mysql2/promise', () => ({
  default: {
    createPool: vi.fn(() => ({
      execute: vi.fn(),
      end: vi.fn(),
    })),
  },
}));

describe('database pool', () => {
  it('should create pool with correct config', async () => {
    const mysql = await import('mysql2/promise');
    const { initializePool } = await import('../../../src/database/pool.js');

    initializePool();

    expect(mysql.default.createPool).toHaveBeenCalledWith(
      expect.objectContaining({
        waitForConnections: true,
        connectionLimit: 20,
      })
    );
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd server && npm test -- tests/unit/database/pool.test.ts
```

Expected: FAIL (module not found)

**Step 3: Create pool.ts**

```typescript
// server/src/database/pool.ts
import mysql, { Pool, PoolConnection, ResultSetHeader, RowDataPacket } from 'mysql2/promise';
import { config } from '../config.js';

let pool: Pool | null = null;

export function initializePool(): Pool {
  if (pool) return pool;

  pool = mysql.createPool({
    host: config.DB_HOST,
    port: config.DB_PORT,
    user: config.DB_USER,
    password: config.DB_PASSWORD,
    database: config.DB_NAME,
    waitForConnections: true,
    connectionLimit: config.MAX_CONNECTIONS,
    queueLimit: 0,
    enableKeepAlive: true,
    keepAliveInitialDelay: 10000,
  });

  return pool;
}

export function getPool(): Pool {
  if (!pool) {
    throw new Error('Database pool not initialized. Call initializePool() first.');
  }
  return pool;
}

export async function closePool(): Promise<void> {
  if (pool) {
    await pool.end();
    pool = null;
  }
}

export async function query<T extends RowDataPacket[]>(
  sql: string,
  params?: unknown[]
): Promise<T> {
  const [rows] = await getPool().execute<T>(sql, params);
  return rows;
}

export async function queryOne<T extends RowDataPacket>(
  sql: string,
  params?: unknown[]
): Promise<T | null> {
  const rows = await query<T[]>(sql, params);
  return rows[0] ?? null;
}

export async function execute(
  sql: string,
  params?: unknown[]
): Promise<ResultSetHeader> {
  const [result] = await getPool().execute<ResultSetHeader>(sql, params);
  return result;
}

export async function getConnection(): Promise<PoolConnection> {
  return getPool().getConnection();
}
```

**Step 4: Run test to verify it passes**

```bash
cd server && npm test -- tests/unit/database/pool.test.ts
```

Expected: PASS

**Step 5: Commit**

```bash
git add server/src/database/ server/tests/
git commit -m "feat(server): add database connection pool"
```

---

### Task 2.2: Create Transaction Helper

**Files:**
- Create: `server/src/database/transactions.ts`
- Create: `server/tests/unit/database/transactions.test.ts`

**Step 1: Write failing test**

```typescript
// server/tests/unit/database/transactions.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';

const mockConnection = {
  beginTransaction: vi.fn(),
  commit: vi.fn(),
  rollback: vi.fn(),
  release: vi.fn(),
  execute: vi.fn(),
};

vi.mock('../../../src/database/pool.js', () => ({
  getConnection: vi.fn(() => Promise.resolve(mockConnection)),
}));

describe('transaction', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should commit on success', async () => {
    const { transaction } = await import('../../../src/database/transactions.js');

    const result = await transaction(async (conn) => {
      return 'success';
    });

    expect(result).toBe('success');
    expect(mockConnection.beginTransaction).toHaveBeenCalled();
    expect(mockConnection.commit).toHaveBeenCalled();
    expect(mockConnection.release).toHaveBeenCalled();
  });

  it('should rollback on error', async () => {
    const { transaction } = await import('../../../src/database/transactions.js');

    await expect(transaction(async () => {
      throw new Error('test error');
    })).rejects.toThrow('test error');

    expect(mockConnection.rollback).toHaveBeenCalled();
    expect(mockConnection.release).toHaveBeenCalled();
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd server && npm test -- tests/unit/database/transactions.test.ts
```

Expected: FAIL

**Step 3: Create transactions.ts**

```typescript
// server/src/database/transactions.ts
import { PoolConnection } from 'mysql2/promise';
import { getConnection } from './pool.js';

export async function transaction<T>(
  fn: (conn: PoolConnection) => Promise<T>
): Promise<T> {
  const conn = await getConnection();

  try {
    await conn.beginTransaction();
    const result = await fn(conn);
    await conn.commit();
    return result;
  } catch (error) {
    await conn.rollback();
    throw error;
  } finally {
    conn.release();
  }
}
```

**Step 4: Run test to verify it passes**

```bash
cd server && npm test -- tests/unit/database/transactions.test.ts
```

Expected: PASS

**Step 5: Commit**

```bash
git add server/src/database/ server/tests/
git commit -m "feat(server): add transaction helper with rollback"
```

---

### Task 2.3: Create Database Index

**Files:**
- Create: `server/src/database/index.ts`

**Step 1: Create barrel export**

```typescript
// server/src/database/index.ts
export {
  initializePool,
  getPool,
  closePool,
  query,
  queryOne,
  execute,
  getConnection,
} from './pool.js';

export { transaction } from './transactions.js';
```

**Step 2: Verify imports work**

```bash
cd server && npx tsc --noEmit
```

Expected: No errors

**Step 3: Commit**

```bash
git add server/src/database/
git commit -m "feat(server): add database barrel export"
```

---

## Phase 3: Core Models

### Task 3.1: Create Base Types

**Files:**
- Create: `server/src/models/types.ts`

**Step 1: Create shared types**

```typescript
// server/src/models/types.ts
export type GameStatus = 'creating' | 'playing' | 'dead';
export type Controller = 'active' | 'inactive';
export type Connection = 'connected' | 'disconnected';
export type Sex = 'male' | 'female';
export type Season = 'spring' | 'summer' | 'autumn' | 'winter';
export type RelationshipType = 'family' | 'friend' | 'romantic' | 'acquaintance';

export interface ResourceCost {
  energyCost?: number;
  moneyCost?: number;
  diamondCost?: number;
}

export interface SimplePerson {
  id: string;
  name: string;
  image?: string;
}
```

**Step 2: Commit**

```bash
git add server/src/models/
git commit -m "feat(server): add base type definitions"
```

---

### Task 3.2: Create Person Model

**Files:**
- Create: `server/src/models/Person.ts`
- Create: `server/tests/unit/models/Person.test.ts`

**Step 1: Write failing test**

```typescript
// server/tests/unit/models/Person.test.ts
import { describe, it, expect } from 'vitest';
import { Person } from '../../../src/models/Person.js';

describe('Person', () => {
  it('should create a person with required fields', () => {
    const person = new Person({
      id: 'test-id',
      name: 'John',
      lastName: 'Doe',
      age: 25,
      sex: 'male',
    });

    expect(person.id).toBe('test-id');
    expect(person.fullName).toBe('John Doe');
    expect(person.age).toBe(25);
  });

  it('should calculate health percentage', () => {
    const person = new Person({
      id: 'test-id',
      name: 'John',
      lastName: 'Doe',
      age: 25,
      sex: 'male',
      health: 75,
    });

    expect(person.healthPercentage).toBe(75);
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd server && npm test -- tests/unit/models/Person.test.ts
```

Expected: FAIL

**Step 3: Create Person.ts**

```typescript
// server/src/models/Person.ts
import { Sex } from './types.js';

export interface PersonData {
  id: string;
  name: string;
  lastName: string;
  age: number;
  sex: Sex;
  image?: string;
  mood?: number;
  affinity?: number;
  money?: number;
  diamonds?: number;
  prestige?: number;
  happiness?: number;
  health?: number;
  intelligence?: number;
  bio?: string;
  interests?: string[];
  traits?: string[];
  compatibilityScore?: number;
  birthday?: string;
}

export class Person {
  id: string;
  name: string;
  lastName: string;
  age: number;
  sex: Sex;
  image: string;
  mood: number;
  affinity: number;
  money: number;
  diamonds: number;
  prestige: number;
  happiness: number;
  health: number;
  intelligence: number;
  bio: string;
  interests: string[];
  traits: string[];
  compatibilityScore: number;
  birthday: string;

  constructor(data: PersonData) {
    this.id = data.id;
    this.name = data.name;
    this.lastName = data.lastName;
    this.age = data.age;
    this.sex = data.sex;
    this.image = data.image ?? '';
    this.mood = data.mood ?? 50;
    this.affinity = data.affinity ?? 50;
    this.money = data.money ?? 0;
    this.diamonds = data.diamonds ?? 0;
    this.prestige = data.prestige ?? 0;
    this.happiness = data.happiness ?? 50;
    this.health = data.health ?? 100;
    this.intelligence = data.intelligence ?? 50;
    this.bio = data.bio ?? '';
    this.interests = data.interests ?? [];
    this.traits = data.traits ?? [];
    this.compatibilityScore = data.compatibilityScore ?? 0;
    this.birthday = data.birthday ?? '';
  }

  get fullName(): string {
    return `${this.name} ${this.lastName}`;
  }

  get healthPercentage(): number {
    return Math.max(0, Math.min(100, this.health));
  }

  toJSON(): PersonData {
    return {
      id: this.id,
      name: this.name,
      lastName: this.lastName,
      age: this.age,
      sex: this.sex,
      image: this.image,
      mood: this.mood,
      affinity: this.affinity,
      money: this.money,
      diamonds: this.diamonds,
      prestige: this.prestige,
      happiness: this.happiness,
      health: this.health,
      intelligence: this.intelligence,
      bio: this.bio,
      interests: this.interests,
      traits: this.traits,
      compatibilityScore: this.compatibilityScore,
      birthday: this.birthday,
    };
  }
}
```

**Step 4: Run test to verify it passes**

```bash
cd server && npm test -- tests/unit/models/Person.test.ts
```

Expected: PASS

**Step 5: Commit**

```bash
git add server/src/models/ server/tests/
git commit -m "feat(server): add Person model"
```

---

### Task 3.3: Create Player Model

**Files:**
- Create: `server/src/models/Player.ts`
- Create: `server/tests/unit/models/Player.test.ts`

**Step 1: Write failing test**

```typescript
// server/tests/unit/models/Player.test.ts
import { describe, it, expect } from 'vitest';
import { Player } from '../../../src/models/Player.js';
import { Person } from '../../../src/models/Person.js';

describe('Player', () => {
  it('should create a player with default values', () => {
    const character = new Person({
      id: 'char-1',
      name: 'Test',
      lastName: 'Player',
      age: 0,
      sex: 'male',
    });

    const player = new Player({
      userId: 'user-123',
      character,
    });

    expect(player.userId).toBe('user-123');
    expect(player.status).toBe('creating');
    expect(player.hourOfDay).toBe(8);
    expect(player.gameSpeed).toBe(10000);
  });

  it('should track events with O(1) lookup', () => {
    const character = new Person({
      id: 'char-1',
      name: 'Test',
      lastName: 'Player',
      age: 0,
      sex: 'male',
    });

    const player = new Player({ userId: 'user-123', character });

    player.events.add('event-1');
    expect(player.events.has('event-1')).toBe(true);
    expect(player.events.has('event-2')).toBe(false);
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd server && npm test -- tests/unit/models/Player.test.ts
```

Expected: FAIL

**Step 3: Create Player.ts**

```typescript
// server/src/models/Player.ts
import { Person } from './Person.js';
import { GameStatus, Controller, Connection, Season } from './types.js';

export interface PlayerData {
  userId: string;
  character: Person;
  status?: GameStatus;
  controller?: Controller;
  connection?: Connection;
  date?: string;
  hourOfDay?: number;
  minuteOfHour?: number;
  dayOfYear?: number;
  dayOfWeek?: number;
  monthOfYear?: number;
  season?: Season;
  gameSpeed?: number;
  previousGameSpeed?: number;
  energyLevel?: number;
  money?: number;
  diamonds?: number;
  relationships?: Person[];
  events?: Set<string>;
  askedQuestions?: Set<string>;
  deviceToken?: string;
}

export class Player {
  userId: string;
  character: Person;
  status: GameStatus;
  controller: Controller;
  connection: Connection;

  // Time
  date: string;
  hourOfDay: number;
  minuteOfHour: number;
  dayOfYear: number;
  dayOfWeek: number;
  monthOfYear: number;
  season: Season;

  // Speed
  gameSpeed: number;
  previousGameSpeed: number;
  private tickCounter: number = 0;

  // Resources
  energyLevel: number;
  money: number;
  diamonds: number;

  // Relationships
  relationships: Person[];

  // Event tracking (O(1) dedup)
  events: Set<string>;
  askedQuestions: Set<string>;

  // System
  deviceToken: string;

  constructor(data: PlayerData) {
    this.userId = data.userId;
    this.character = data.character;
    this.status = data.status ?? 'creating';
    this.controller = data.controller ?? 'inactive';
    this.connection = data.connection ?? 'connected';

    // Time defaults
    this.date = data.date ?? new Date().toISOString().split('T')[0];
    this.hourOfDay = data.hourOfDay ?? 8;
    this.minuteOfHour = data.minuteOfHour ?? 0;
    this.dayOfYear = data.dayOfYear ?? 1;
    this.dayOfWeek = data.dayOfWeek ?? 1;
    this.monthOfYear = data.monthOfYear ?? 1;
    this.season = data.season ?? 'spring';

    // Speed
    this.gameSpeed = data.gameSpeed ?? 10000;
    this.previousGameSpeed = data.previousGameSpeed ?? 10000;

    // Resources
    this.energyLevel = data.energyLevel ?? 100;
    this.money = data.money ?? 0;
    this.diamonds = data.diamonds ?? 0;

    // Relationships
    this.relationships = data.relationships ?? [];

    // Event tracking
    this.events = data.events ?? new Set();
    this.askedQuestions = data.askedQuestions ?? new Set();

    // System
    this.deviceToken = data.deviceToken ?? '';
  }

  get isActive(): boolean {
    return this.controller === 'active' && this.status === 'playing';
  }

  getTicksForSpeed(): number {
    // Higher gameSpeed = slower game (more ticks needed per minute)
    // At 10ms tick interval, 100 ticks = 1 second
    // gameSpeed 1 = fastest (1 tick per minute advance)
    // gameSpeed 10000 = paused (10000 ticks per minute advance)
    return this.gameSpeed;
  }

  incrementTick(): boolean {
    if (!this.isActive) return false;

    this.tickCounter++;
    const ticksNeeded = this.getTicksForSpeed();

    if (this.tickCounter >= ticksNeeded) {
      this.tickCounter = 0;
      return true; // Advance minute
    }

    return false;
  }

  toJSON(): Record<string, unknown> {
    return {
      userId: this.userId,
      c: this.character.toJSON(),
      status: this.status,
      controller: this.controller,
      connection: this.connection,
      date: this.date,
      hourOfDay: this.hourOfDay,
      minuteOfHour: this.minuteOfHour,
      dayOfYear: this.dayOfYear,
      dayOfWeek: this.dayOfWeek,
      monthOfYear: this.monthOfYear,
      season: this.season,
      gameSpeed: this.gameSpeed,
      energyLevel: this.energyLevel,
      money: this.money,
      diamonds: this.diamonds,
      r: this.relationships.map(r => r.toJSON()),
    };
  }
}
```

**Step 4: Run test to verify it passes**

```bash
cd server && npm test -- tests/unit/models/Player.test.ts
```

Expected: PASS

**Step 5: Commit**

```bash
git add server/src/models/ server/tests/
git commit -m "feat(server): add Player model with tick counter"
```

---

### Task 3.4: Create MessageEvent Model

**Files:**
- Create: `server/src/models/MessageEvent.ts`
- Create: `server/tests/unit/models/MessageEvent.test.ts`

**Step 1: Write failing test**

```typescript
// server/tests/unit/models/MessageEvent.test.ts
import { describe, it, expect } from 'vitest';
import { MessageEvent } from '../../../src/models/MessageEvent.js';

describe('MessageEvent', () => {
  it('should identify claimable events', () => {
    const event = new MessageEvent({
      id: 'event-1',
      message: 'You found $100!',
      type: 'reward',
      moneyCost: -100, // Negative cost = gain
    });

    expect(event.isClaimable).toBe(true);
  });

  it('should identify negative events', () => {
    const event = new MessageEvent({
      id: 'event-2',
      message: 'You lost $50',
      type: 'loss',
      moneyCost: 50, // Positive cost = loss
    });

    expect(event.isNegative).toBe(true);
    expect(event.isClaimable).toBe(false);
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd server && npm test -- tests/unit/models/MessageEvent.test.ts
```

Expected: FAIL

**Step 3: Create MessageEvent.ts**

```typescript
// server/src/models/MessageEvent.ts
import { SimplePerson } from './types.js';

export type EventCategory =
  | 'career'
  | 'social'
  | 'achievement'
  | 'education'
  | 'health'
  | 'finance'
  | 'random'
  | 'neutral'
  | 'negative';

export interface MessageEventData {
  id: string;
  message: string;
  type: string;
  date?: string;
  hour?: number;
  energyCost?: number;
  diamondCost?: number;
  moneyCost?: number;
  affinityChange?: number;
  characters?: SimplePerson[];
  claimed?: boolean;
  claimedAt?: Date;
  category?: EventCategory;
}

export class MessageEvent {
  id: string;
  message: string;
  type: string;
  date: string;
  hour: number;
  energyCost: number;
  diamondCost: number;
  moneyCost: number;
  affinityChange: number;
  characters: SimplePerson[];
  claimed: boolean;
  claimedAt: Date | null;
  category: EventCategory;

  constructor(data: MessageEventData) {
    this.id = data.id;
    this.message = data.message;
    this.type = data.type;
    this.date = data.date ?? new Date().toISOString().split('T')[0];
    this.hour = data.hour ?? 12;
    this.energyCost = data.energyCost ?? 0;
    this.diamondCost = data.diamondCost ?? 0;
    this.moneyCost = data.moneyCost ?? 0;
    this.affinityChange = data.affinityChange ?? 0;
    this.characters = data.characters ?? [];
    this.claimed = data.claimed ?? false;
    this.claimedAt = data.claimedAt ?? null;
    this.category = data.category ?? this.inferCategory();
  }

  get isClaimable(): boolean {
    // Has positive rewards (negative costs mean gains)
    return (
      this.moneyCost < 0 ||
      this.energyCost < 0 ||
      this.diamondCost < 0 ||
      this.affinityChange > 0
    );
  }

  get isNegative(): boolean {
    // Has negative effects (positive costs)
    return (
      this.moneyCost > 0 ||
      this.energyCost > 0 ||
      this.diamondCost > 0 ||
      this.affinityChange < 0
    );
  }

  private inferCategory(): EventCategory {
    const msg = this.message.toLowerCase();

    if (msg.includes('job') || msg.includes('work') || msg.includes('career')) {
      return 'career';
    }
    if (msg.includes('friend') || msg.includes('relationship')) {
      return 'social';
    }
    if (msg.includes('achieve') || msg.includes('unlock')) {
      return 'achievement';
    }
    if (msg.includes('school') || msg.includes('learn') || msg.includes('study')) {
      return 'education';
    }
    if (msg.includes('health') || msg.includes('sick') || msg.includes('hospital')) {
      return 'health';
    }
    if (msg.includes('money') || msg.includes('$') || msg.includes('pay')) {
      return 'finance';
    }
    if (this.isNegative) {
      return 'negative';
    }

    return 'random';
  }

  toJSON(): MessageEventData {
    return {
      id: this.id,
      message: this.message,
      type: this.type,
      date: this.date,
      hour: this.hour,
      energyCost: this.energyCost,
      diamondCost: this.diamondCost,
      moneyCost: this.moneyCost,
      affinityChange: this.affinityChange,
      characters: this.characters,
      claimed: this.claimed,
      claimedAt: this.claimedAt ?? undefined,
      category: this.category,
    };
  }
}
```

**Step 4: Run test to verify it passes**

```bash
cd server && npm test -- tests/unit/models/MessageEvent.test.ts
```

Expected: PASS

**Step 5: Commit**

```bash
git add server/src/models/ server/tests/
git commit -m "feat(server): add MessageEvent model with claimable logic"
```

---

### Task 3.5: Create Question Model

**Files:**
- Create: `server/src/models/Question.ts`

**Step 1: Create Question.ts**

```typescript
// server/src/models/Question.ts
import { SimplePerson, ResourceCost } from './types.js';

export interface AnswerOption extends ResourceCost {
  key: string;
  text: string;
}

export interface QuestionData {
  id: string;
  message: string;
  type: string;
  answers: AnswerOption[];
  characters?: SimplePerson[];
  image?: string;
}

export class Question {
  id: string;
  message: string;
  type: string;
  answers: AnswerOption[];
  characters: SimplePerson[];
  image: string;

  constructor(data: QuestionData) {
    this.id = data.id;
    this.message = data.message;
    this.type = data.type;
    this.answers = data.answers;
    this.characters = data.characters ?? [];
    this.image = data.image ?? '';
  }

  getAnswer(key: string): AnswerOption | undefined {
    return this.answers.find(a => a.key === key);
  }

  toJSON(): QuestionData {
    return {
      id: this.id,
      message: this.message,
      type: this.type,
      answers: this.answers,
      characters: this.characters,
      image: this.image,
    };
  }
}
```

**Step 2: Commit**

```bash
git add server/src/models/
git commit -m "feat(server): add Question model"
```

---

### Task 3.6: Create Models Index

**Files:**
- Create: `server/src/models/index.ts`

**Step 1: Create barrel export**

```typescript
// server/src/models/index.ts
export * from './types.js';
export { Person, type PersonData } from './Person.js';
export { Player, type PlayerData } from './Player.js';
export { MessageEvent, type MessageEventData, type EventCategory } from './MessageEvent.js';
export { Question, type QuestionData, type AnswerOption } from './Question.js';
```

**Step 2: Verify imports**

```bash
cd server && npx tsc --noEmit
```

Expected: No errors

**Step 3: Commit**

```bash
git add server/src/models/
git commit -m "feat(server): add models barrel export"
```

---

## Phase 4: Server Infrastructure

### Task 4.1: Create Connection Registry

**Files:**
- Create: `server/src/server/ConnectionRegistry.ts`
- Create: `server/tests/unit/server/ConnectionRegistry.test.ts`

**Step 1: Write failing test**

```typescript
// server/tests/unit/server/ConnectionRegistry.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { ConnectionRegistry } from '../../../src/server/ConnectionRegistry.js';

describe('ConnectionRegistry', () => {
  let registry: ConnectionRegistry;

  beforeEach(() => {
    registry = new ConnectionRegistry();
  });

  it('should register and retrieve sessions by userId', () => {
    const mockSession = { userId: 'user-1' } as any;

    registry.register('user-1', mockSession);

    expect(registry.get('user-1')).toBe(mockSession);
    expect(registry.size).toBe(1);
  });

  it('should unregister sessions', () => {
    const mockSession = { userId: 'user-1' } as any;

    registry.register('user-1', mockSession);
    registry.unregister('user-1');

    expect(registry.get('user-1')).toBeUndefined();
    expect(registry.size).toBe(0);
  });

  it('should iterate over all sessions', () => {
    registry.register('user-1', { userId: 'user-1' } as any);
    registry.register('user-2', { userId: 'user-2' } as any);

    const userIds: string[] = [];
    for (const session of registry.values()) {
      userIds.push(session.userId);
    }

    expect(userIds).toContain('user-1');
    expect(userIds).toContain('user-2');
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd server && npm test -- tests/unit/server/ConnectionRegistry.test.ts
```

Expected: FAIL

**Step 3: Create ConnectionRegistry.ts**

```typescript
// server/src/server/ConnectionRegistry.ts
import type { PlayerSession } from '../game/PlayerSession.js';

export class ConnectionRegistry {
  private sessions: Map<string, PlayerSession> = new Map();

  register(userId: string, session: PlayerSession): void {
    // Disconnect existing session if present
    const existing = this.sessions.get(userId);
    if (existing) {
      existing.disconnect();
    }
    this.sessions.set(userId, session);
  }

  unregister(userId: string): void {
    this.sessions.delete(userId);
  }

  get(userId: string): PlayerSession | undefined {
    return this.sessions.get(userId);
  }

  has(userId: string): boolean {
    return this.sessions.has(userId);
  }

  get size(): number {
    return this.sessions.size;
  }

  values(): IterableIterator<PlayerSession> {
    return this.sessions.values();
  }

  async saveAll(): Promise<void> {
    const savePromises: Promise<void>[] = [];
    for (const session of this.sessions.values()) {
      savePromises.push(session.savePlayer());
    }
    await Promise.all(savePromises);
  }
}

export const connectionRegistry = new ConnectionRegistry();
```

**Step 4: Create placeholder PlayerSession for type checking**

```typescript
// server/src/game/PlayerSession.ts (placeholder)
export interface PlayerSession {
  userId: string;
  disconnect(): void;
  savePlayer(): Promise<void>;
}
```

**Step 5: Run test to verify it passes**

```bash
cd server && npm test -- tests/unit/server/ConnectionRegistry.test.ts
```

Expected: PASS

**Step 6: Commit**

```bash
git add server/src/server/ server/src/game/ server/tests/
git commit -m "feat(server): add ConnectionRegistry with O(1) lookup"
```

---

### Task 4.2: Create Message Dispatcher

**Files:**
- Create: `server/src/server/MessageDispatcher.ts`
- Create: `server/tests/unit/server/MessageDispatcher.test.ts`

**Step 1: Write failing test**

```typescript
// server/tests/unit/server/MessageDispatcher.test.ts
import { describe, it, expect, vi } from 'vitest';
import { MessageDispatcher } from '../../../src/server/MessageDispatcher.js';

describe('MessageDispatcher', () => {
  it('should dispatch to registered handler', async () => {
    const dispatcher = new MessageDispatcher();
    const mockHandler = vi.fn();
    const mockSession = { userId: 'test' } as any;

    dispatcher.register('testCommand', mockHandler);
    await dispatcher.dispatch('testCommand', { data: 'test' }, mockSession);

    expect(mockHandler).toHaveBeenCalledWith({ data: 'test' }, mockSession);
  });

  it('should call fallback for unknown commands', async () => {
    const dispatcher = new MessageDispatcher();
    const mockFallback = vi.fn();
    const mockSession = { userId: 'test' } as any;

    dispatcher.setFallback(mockFallback);
    await dispatcher.dispatch('unknownCommand', {}, mockSession);

    expect(mockFallback).toHaveBeenCalled();
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd server && npm test -- tests/unit/server/MessageDispatcher.test.ts
```

Expected: FAIL

**Step 3: Create MessageDispatcher.ts**

```typescript
// server/src/server/MessageDispatcher.ts
import type { PlayerSession } from '../game/PlayerSession.js';

export type CommandHandler = (
  payload: unknown,
  session: PlayerSession
) => Promise<void>;

export class MessageDispatcher {
  private handlers: Map<string, CommandHandler> = new Map();
  private fallbackHandler: CommandHandler | null = null;

  register(command: string, handler: CommandHandler): void {
    this.handlers.set(command, handler);
  }

  registerAll(handlers: Record<string, CommandHandler>): void {
    for (const [command, handler] of Object.entries(handlers)) {
      this.register(command, handler);
    }
  }

  setFallback(handler: CommandHandler): void {
    this.fallbackHandler = handler;
  }

  async dispatch(
    command: string,
    payload: unknown,
    session: PlayerSession
  ): Promise<void> {
    const handler = this.handlers.get(command) ?? this.fallbackHandler;

    if (!handler) {
      console.warn(`No handler for command: ${command}`);
      return;
    }

    try {
      await handler(payload, session);
    } catch (error) {
      console.error(`Error handling command ${command}:`, error);
      // TODO: session.rollbackPendingCosts();
      // TODO: session.sendError(error.message);
      throw error;
    }
  }

  hasHandler(command: string): boolean {
    return this.handlers.has(command);
  }

  get registeredCommands(): string[] {
    return Array.from(this.handlers.keys());
  }
}

export const messageDispatcher = new MessageDispatcher();
```

**Step 4: Run test to verify it passes**

```bash
cd server && npm test -- tests/unit/server/MessageDispatcher.test.ts
```

Expected: PASS

**Step 5: Commit**

```bash
git add server/src/server/ server/tests/
git commit -m "feat(server): add MessageDispatcher with table-driven routing"
```

---

### Task 4.3: Create WebSocket Server

**Files:**
- Create: `server/src/server/WebSocketServer.ts`

**Step 1: Create WebSocketServer.ts**

```typescript
// server/src/server/WebSocketServer.ts
import { WebSocketServer as WSServer, WebSocket } from 'ws';
import { config } from '../config.js';
import { connectionRegistry } from './ConnectionRegistry.js';
import { messageDispatcher } from './MessageDispatcher.js';

export interface ClientMessage {
  type: string;
  message?: unknown;
}

export class WebSocketServer {
  private wss: WSServer | null = null;

  start(): void {
    this.wss = new WSServer({ port: config.PORT });

    this.wss.on('connection', (ws: WebSocket) => {
      console.log('New WebSocket connection');
      this.handleConnection(ws);
    });

    this.wss.on('error', (error) => {
      console.error('WebSocket server error:', error);
    });

    console.log(`WebSocket server listening on port ${config.PORT}`);
  }

  private handleConnection(ws: WebSocket): void {
    let userId: string | null = null;

    ws.on('message', async (data) => {
      try {
        const message = JSON.parse(data.toString()) as ClientMessage;

        // First message should be 'init' with userId
        if (message.type === 'init' && !userId) {
          userId = (message.message as { userID?: string })?.userID ?? null;
          if (userId) {
            await this.initializeSession(userId, ws);
          }
          return;
        }

        // Dispatch to handler
        if (userId) {
          const session = connectionRegistry.get(userId);
          if (session) {
            await messageDispatcher.dispatch(message.type, message.message, session);
          }
        }
      } catch (error) {
        console.error('Error processing message:', error);
      }
    });

    ws.on('close', async () => {
      if (userId) {
        const session = connectionRegistry.get(userId);
        if (session) {
          await session.savePlayer();
          session.disconnect();
        }
        connectionRegistry.unregister(userId);
        console.log(`User ${userId} disconnected`);
      }
    });

    ws.on('error', (error) => {
      console.error('WebSocket error:', error);
    });
  }

  private async initializeSession(userId: string, ws: WebSocket): Promise<void> {
    // TODO: Implement full session initialization
    // 1. Load player from DB or create new
    // 2. Create PlayerSession
    // 3. Register in connectionRegistry
    // 4. Start game loop
    // 5. Send playerObject to client
    console.log(`Initializing session for user: ${userId}`);
  }

  stop(): void {
    if (this.wss) {
      this.wss.close();
      this.wss = null;
    }
  }

  get isRunning(): boolean {
    return this.wss !== null;
  }
}

export const webSocketServer = new WebSocketServer();
```

**Step 2: Commit**

```bash
git add server/src/server/
git commit -m "feat(server): add WebSocketServer with connection handling"
```

---

### Task 4.4: Create Server Index and Types

**Files:**
- Create: `server/src/server/types.ts`
- Create: `server/src/server/index.ts`

**Step 1: Create types.ts**

```typescript
// server/src/server/types.ts
export interface ClientMessage {
  type: string;
  message?: unknown;
}

export interface InitMessage {
  userID: string;
}
```

**Step 2: Create index.ts**

```typescript
// server/src/server/index.ts
export { ConnectionRegistry, connectionRegistry } from './ConnectionRegistry.js';
export { MessageDispatcher, messageDispatcher, type CommandHandler } from './MessageDispatcher.js';
export { WebSocketServer, webSocketServer } from './WebSocketServer.js';
export * from './types.js';
```

**Step 3: Commit**

```bash
git add server/src/server/
git commit -m "feat(server): add server types and barrel export"
```

---

## Phase 5: Game Loop

### Task 5.1: Create BatchedUpdate

**Files:**
- Create: `server/src/game/BatchedUpdate.ts`
- Create: `server/tests/unit/game/BatchedUpdate.test.ts`

**Step 1: Write failing test**

```typescript
// server/tests/unit/game/BatchedUpdate.test.ts
import { describe, it, expect } from 'vitest';
import { BatchedUpdate } from '../../../src/game/BatchedUpdate.js';

describe('BatchedUpdate', () => {
  it('should accumulate updates', () => {
    const batch = new BatchedUpdate();

    batch.add('hourOfDay', 12);
    batch.add('money', 1000);
    batch.add('energy', 50);

    const result = batch.toMessage();

    expect(result.type).toBe('batch_update');
    expect(result.updates.hourOfDay).toBe(12);
    expect(result.updates.money).toBe(1000);
    expect(result.updates.energy).toBe(50);
  });

  it('should clear after toMessage', () => {
    const batch = new BatchedUpdate();
    batch.add('test', 1);
    batch.toMessage();

    expect(batch.isEmpty).toBe(true);
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd server && npm test -- tests/unit/game/BatchedUpdate.test.ts
```

Expected: FAIL

**Step 3: Create BatchedUpdate.ts**

```typescript
// server/src/game/BatchedUpdate.ts
export interface BatchedMessage {
  type: 'batch_update';
  updates: Record<string, unknown>;
}

export class BatchedUpdate {
  private updates: Map<string, unknown> = new Map();

  add(key: string, value: unknown): void {
    this.updates.set(key, value);
  }

  get isEmpty(): boolean {
    return this.updates.size === 0;
  }

  toMessage(): BatchedMessage {
    const updates: Record<string, unknown> = {};
    for (const [key, value] of this.updates) {
      updates[key] = value;
    }
    this.updates.clear();
    return { type: 'batch_update', updates };
  }

  clear(): void {
    this.updates.clear();
  }
}
```

**Step 4: Run test to verify it passes**

```bash
cd server && npm test -- tests/unit/game/BatchedUpdate.test.ts
```

Expected: PASS

**Step 5: Commit**

```bash
git add server/src/game/ server/tests/
git commit -m "feat(server): add BatchedUpdate for efficient state batching"
```

---

### Task 5.2: Create Full PlayerSession

**Files:**
- Update: `server/src/game/PlayerSession.ts`
- Create: `server/tests/unit/game/PlayerSession.test.ts`

**Step 1: Write failing test**

```typescript
// server/tests/unit/game/PlayerSession.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

describe('PlayerSession', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('should start and stop game loop', async () => {
    const { PlayerSession } = await import('../../../src/game/PlayerSession.js');
    const { Player } = await import('../../../src/models/Player.js');
    const { Person } = await import('../../../src/models/Person.js');

    const mockWs = {
      send: vi.fn(),
      readyState: 1, // OPEN
    } as any;

    const player = new Player({
      userId: 'test-user',
      character: new Person({
        id: 'char-1',
        name: 'Test',
        lastName: 'Player',
        age: 25,
        sex: 'male',
      }),
    });

    const session = new PlayerSession(mockWs, player);
    session.start();

    expect(session.isRunning).toBe(true);

    session.stop();

    expect(session.isRunning).toBe(false);
  });

  it('should advance time on ticks', async () => {
    const { PlayerSession } = await import('../../../src/game/PlayerSession.js');
    const { Player } = await import('../../../src/models/Player.js');
    const { Person } = await import('../../../src/models/Person.js');

    const mockWs = {
      send: vi.fn(),
      readyState: 1,
    } as any;

    const player = new Player({
      userId: 'test-user',
      character: new Person({
        id: 'char-1',
        name: 'Test',
        lastName: 'Player',
        age: 25,
        sex: 'male',
      }),
      status: 'playing',
      controller: 'active',
      gameSpeed: 1, // Fastest speed - 1 tick per minute
      hourOfDay: 8,
      minuteOfHour: 0,
    });

    const session = new PlayerSession(mockWs, player);
    session.start();

    // Advance 1 tick at 10ms interval
    vi.advanceTimersByTime(10);

    // At gameSpeed 1, each tick advances a minute
    expect(player.minuteOfHour).toBe(1);

    session.stop();
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd server && npm test -- tests/unit/game/PlayerSession.test.ts
```

Expected: FAIL

**Step 3: Update PlayerSession.ts with full implementation**

```typescript
// server/src/game/PlayerSession.ts
import { WebSocket } from 'ws';
import { Player } from '../models/Player.js';
import { BatchedUpdate } from './BatchedUpdate.js';
import { config } from '../config.js';

export class PlayerSession {
  readonly userId: string;
  readonly player: Player;
  private ws: WebSocket;
  private tickInterval: ReturnType<typeof setInterval> | null = null;
  private batchedUpdate: BatchedUpdate = new BatchedUpdate();
  private tickCounter: number = 0;

  constructor(ws: WebSocket, player: Player) {
    this.ws = ws;
    this.player = player;
    this.userId = player.userId;
  }

  start(): void {
    if (this.tickInterval) return;

    this.tickInterval = setInterval(() => {
      this.tick();
    }, config.TICK_INTERVAL);
  }

  stop(): void {
    if (this.tickInterval) {
      clearInterval(this.tickInterval);
      this.tickInterval = null;
    }
  }

  get isRunning(): boolean {
    return this.tickInterval !== null;
  }

  private tick(): void {
    if (!this.player.isActive) return;

    this.tickCounter++;
    const ticksNeeded = this.player.getTicksForSpeed();

    if (this.tickCounter >= ticksNeeded) {
      this.tickCounter = 0;
      this.advanceMinute();
    }
  }

  private advanceMinute(): void {
    this.player.minuteOfHour++;
    this.processMinuteTick();

    if (this.player.minuteOfHour >= 60) {
      this.player.minuteOfHour = 0;
      this.player.hourOfDay++;
      this.processHourTick();
    }

    if (this.player.hourOfDay >= 24) {
      this.player.hourOfDay = 0;
      this.player.dayOfYear++;
      this.player.dayOfWeek = ((this.player.dayOfWeek) % 7) + 1;
      this.processDayTick();
    }

    // Weekly tick on Monday midnight
    if (this.player.dayOfWeek === 1 && this.player.hourOfDay === 0 && this.player.minuteOfHour === 0) {
      this.processWeekTick();
    }
  }

  private processMinuteTick(): void {
    // TODO: Random events, location updates
  }

  private processHourTick(): void {
    // Add updates to batch
    this.batchedUpdate.add('hourOfDay', this.player.hourOfDay);
    this.batchedUpdate.add('minuteOfHour', this.player.minuteOfHour);
    this.batchedUpdate.add('date', this.player.date);
    this.batchedUpdate.add('season', this.player.season);
    this.batchedUpdate.add('dayOfYear', this.player.dayOfYear);
    this.batchedUpdate.add('dayOfWeek', this.player.dayOfWeek);

    // Send batched update
    this.sendBatchedUpdate();

    // TODO: Mood updates, event checking
  }

  private processDayTick(): void {
    // Energy recharge
    if (this.player.energyLevel < 100) {
      this.player.energyLevel = Math.min(100, this.player.energyLevel + 1);
    }

    // TODO: Birthday checks, daily events
  }

  private processWeekTick(): void {
    // TODO: Save game, finances, relationships
    this.savePlayer().catch(err => console.error('Error saving player:', err));
  }

  private sendBatchedUpdate(): void {
    if (this.batchedUpdate.isEmpty) return;

    const message = this.batchedUpdate.toMessage();
    this.send(message);
  }

  send(message: unknown): void {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(message));
    }
  }

  sendPlayerObject(): void {
    this.send({
      type: 'playerObject',
      player: this.player.toJSON(),
    });
  }

  async savePlayer(): Promise<void> {
    // TODO: Implement database save
    console.log(`Saving player ${this.userId}`);
  }

  disconnect(): void {
    this.stop();
    this.player.connection = 'disconnected';
  }
}
```

**Step 4: Run test to verify it passes**

```bash
cd server && npm test -- tests/unit/game/PlayerSession.test.ts
```

Expected: PASS

**Step 5: Commit**

```bash
git add server/src/game/ server/tests/
git commit -m "feat(server): implement PlayerSession with 10ms game loop"
```

---

### Task 5.3: Create Game Index

**Files:**
- Create: `server/src/game/index.ts`

**Step 1: Create barrel export**

```typescript
// server/src/game/index.ts
export { BatchedUpdate, type BatchedMessage } from './BatchedUpdate.js';
export { PlayerSession } from './PlayerSession.js';
```

**Step 2: Commit**

```bash
git add server/src/game/
git commit -m "feat(server): add game module barrel export"
```

---

## Phase 6: Command Handlers (Core Set)

### Task 6.1: Create Game Control Handlers

**Files:**
- Create: `server/src/handlers/gameControl.ts`
- Create: `server/tests/unit/handlers/gameControl.test.ts`

**Step 1: Write failing test**

```typescript
// server/tests/unit/handlers/gameControl.test.ts
import { describe, it, expect, vi } from 'vitest';

describe('gameControl handlers', () => {
  it('handleStart should set controller to active', async () => {
    const { handleStart } = await import('../../../src/handlers/gameControl.js');

    const mockSession = {
      player: { controller: 'inactive', status: 'playing' },
      start: vi.fn(),
      sendPlayerObject: vi.fn(),
    } as any;

    await handleStart({}, mockSession);

    expect(mockSession.player.controller).toBe('active');
    expect(mockSession.start).toHaveBeenCalled();
  });

  it('handleStop should set controller to inactive', async () => {
    const { handleStop } = await import('../../../src/handlers/gameControl.js');

    const mockSession = {
      player: { controller: 'active' },
      stop: vi.fn(),
    } as any;

    await handleStop({}, mockSession);

    expect(mockSession.player.controller).toBe('inactive');
    expect(mockSession.stop).toHaveBeenCalled();
  });

  it('handleSpeed should update game speed', async () => {
    const { handleSpeed } = await import('../../../src/handlers/gameControl.js');

    const mockSession = {
      player: { gameSpeed: 10000 },
      send: vi.fn(),
    } as any;

    await handleSpeed({ speed: 1000 }, mockSession);

    expect(mockSession.player.gameSpeed).toBe(1000);
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd server && npm test -- tests/unit/handlers/gameControl.test.ts
```

Expected: FAIL

**Step 3: Create gameControl.ts**

```typescript
// server/src/handlers/gameControl.ts
import type { PlayerSession } from '../game/PlayerSession.js';
import { config } from '../config.js';

export async function handleStart(
  _payload: unknown,
  session: PlayerSession
): Promise<void> {
  session.player.controller = 'active';
  session.player.status = 'playing';
  session.start();
  session.sendPlayerObject();
}

export async function handleStop(
  _payload: unknown,
  session: PlayerSession
): Promise<void> {
  session.player.controller = 'inactive';
  session.stop();
}

export async function handleRestart(
  _payload: unknown,
  session: PlayerSession
): Promise<void> {
  // Reset player state
  session.player.hourOfDay = 8;
  session.player.minuteOfHour = 0;
  session.player.dayOfYear = 1;
  session.player.dayOfWeek = 1;
  session.player.monthOfYear = 1;
  session.player.season = 'spring';
  session.player.energyLevel = 100;
  session.player.character.age = 0;
  session.player.status = 'playing';
  session.player.controller = 'active';
  session.player.events.clear();
  session.player.askedQuestions.clear();

  session.start();
  session.sendPlayerObject();
}

interface SpeedPayload {
  speed?: number | '+' | '-' | 'reset';
}

export async function handleSpeed(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  const { speed } = payload as SpeedPayload;

  if (speed === undefined) return;

  const speedValues = config.SPEED_BUTTON_VALUES;
  const currentIndex = speedValues.indexOf(session.player.gameSpeed as typeof speedValues[number]);

  if (speed === '+') {
    // Increase speed (lower value = faster)
    const newIndex = Math.max(0, currentIndex - 1);
    session.player.gameSpeed = speedValues[newIndex];
  } else if (speed === '-') {
    // Decrease speed (higher value = slower)
    const newIndex = Math.min(speedValues.length - 1, currentIndex + 1);
    session.player.gameSpeed = speedValues[newIndex];
  } else if (speed === 'reset') {
    session.player.gameSpeed = config.SPEED_DEFAULT;
  } else if (typeof speed === 'number') {
    session.player.gameSpeed = speed;
  }

  session.send({
    type: 'speedUpdate',
    speed: session.player.gameSpeed,
  });
}
```

**Step 4: Run test to verify it passes**

```bash
cd server && npm test -- tests/unit/handlers/gameControl.test.ts
```

Expected: PASS

**Step 5: Commit**

```bash
git add server/src/handlers/ server/tests/
git commit -m "feat(server): add game control handlers (start, stop, restart, speed)"
```

---

### Task 6.2: Create Handler Registry

**Files:**
- Create: `server/src/handlers/index.ts`

**Step 1: Create handler registry**

```typescript
// server/src/handlers/index.ts
import type { CommandHandler } from '../server/MessageDispatcher.js';
import { handleStart, handleStop, handleRestart, handleSpeed } from './gameControl.js';

// Command registry - add handlers as they are implemented
export const COMMAND_REGISTRY: Record<string, CommandHandler> = {
  // Game Control
  'start': handleStart,
  'stop': handleStop,
  'restart': handleRestart,
  'speed': handleSpeed,

  // Character (TODO)
  // 'characterSetup': handleCharacterSetup,
  // 'deviceToken': handleDeviceToken,

  // Activities (TODO)
  // 'applyForExtracurricular': handleApplyExtracurricular,
  // 'quitExtracurricular': handleQuitExtracurricular,

  // Conversations (TODO)
  // 'conversation': handleConversation,
  // 'retrievePerson': handleRetrievePerson,

  // Purchases (TODO)
  // 'purchaseEnergyRefill': handlePurchaseEnergyRefill,
  // 'purchaseTimeSkip': handlePurchaseTimeSkip,

  // Retention (TODO)
  // 'getAchievements': handleGetAchievements,
  // 'claimDailyReward': handleClaimDailyReward,

  // Events (TODO)
  // 'claimEvent': handleClaimEvent,

  // Romance (TODO)
  // 'getSwipeCharacter': handleGetSwipeCharacter,
  // 'swipeMatch': handleSwipeMatch,

  // Data (TODO)
  // 'exportData': handleExportData,
  // 'deleteAccount': handleDeleteAccount,
};

export function registerAllHandlers(): void {
  const { messageDispatcher } = require('../server/MessageDispatcher.js');
  messageDispatcher.registerAll(COMMAND_REGISTRY);
}
```

**Step 2: Commit**

```bash
git add server/src/handlers/
git commit -m "feat(server): add command registry with handler mapping"
```

---

## Phase 7: Entry Point

### Task 7.1: Complete Main Entry Point

**Files:**
- Update: `server/src/index.ts`

**Step 1: Update index.ts with full initialization**

```typescript
// server/src/index.ts
import { config } from './config.js';
import { initializePool, closePool } from './database/pool.js';
import { webSocketServer } from './server/WebSocketServer.js';
import { messageDispatcher } from './server/MessageDispatcher.js';
import { connectionRegistry } from './server/ConnectionRegistry.js';
import { COMMAND_REGISTRY } from './handlers/index.js';

async function main(): Promise<void> {
  console.log('BaoLife Server starting...');

  // Initialize database
  console.log('Initializing database connection pool...');
  initializePool();

  // Register command handlers
  console.log('Registering command handlers...');
  messageDispatcher.registerAll(COMMAND_REGISTRY);
  console.log(`Registered ${messageDispatcher.registeredCommands.length} commands`);

  // Start WebSocket server
  console.log(`Starting WebSocket server on port ${config.PORT}...`);
  webSocketServer.start();

  // Start background tasks
  startBackgroundTasks();

  console.log('BaoLife Server ready!');
}

function startBackgroundTasks(): void {
  // Every-minute task
  setInterval(async () => {
    try {
      // TODO: Iterate offline games
      console.log(`Active connections: ${connectionRegistry.size}`);
    } catch (error) {
      console.error('Background task error:', error);
    }
  }, 60_000);
}

async function shutdown(): Promise<void> {
  console.log('Shutting down...');

  // Stop accepting new connections
  webSocketServer.stop();

  // Save all active players
  console.log('Saving all active players...');
  await connectionRegistry.saveAll();

  // Close database pool
  console.log('Closing database pool...');
  await closePool();

  console.log('Shutdown complete');
  process.exit(0);
}

// Graceful shutdown handlers
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

// Start the server
main().catch((error) => {
  console.error('Fatal error:', error);
  process.exit(1);
});
```

**Step 2: Test that server starts**

```bash
cd server && npm run build && timeout 3 npm start || true
```

Expected: Server starts, prints initialization messages

**Step 3: Commit**

```bash
git add server/src/
git commit -m "feat(server): complete main entry point with initialization"
```

---

## Phase 8: Remaining Handlers (Stubs)

### Task 8.1: Create Handler Stubs for All Commands

**Files:**
- Create: `server/src/handlers/character.ts`
- Create: `server/src/handlers/activities.ts`
- Create: `server/src/handlers/conversations.ts`
- Create: `server/src/handlers/purchases.ts`
- Create: `server/src/handlers/retention.ts`
- Create: `server/src/handlers/romance.ts`
- Create: `server/src/handlers/data.ts`
- Create: `server/src/handlers/generic.ts`

**Step 1: Create all handler stubs**

Each file follows this pattern - create stubs that log and acknowledge:

```typescript
// server/src/handlers/character.ts
import type { PlayerSession } from '../game/PlayerSession.js';

export async function handleCharacterSetup(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python ../lichun/ws/character/character_manager.py
  console.log('characterSetup:', payload);
  session.sendPlayerObject();
}

export async function handleDeviceToken(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  const { token } = payload as { token?: string };
  if (token) {
    session.player.deviceToken = token;
  }
}
```

```typescript
// server/src/handlers/activities.ts
import type { PlayerSession } from '../game/PlayerSession.js';

export async function handleApplyExtracurricular(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('applyForExtracurricular:', payload);
}

export async function handleQuitExtracurricular(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('quitExtracurricular:', payload);
}

export async function handleApplyJob(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('applyForJob:', payload);
}

export async function handleQuitJob(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('quitJob:', payload);
}

export async function handleQuitHabit(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('quitHabit:', payload);
}

export async function handleStopQuitHabit(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('stopQuitHabit:', payload);
}
```

```typescript
// server/src/handlers/conversations.ts
import type { PlayerSession } from '../game/PlayerSession.js';

export async function handleConversation(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python ../lichun/ws/conversationEvents.py
  console.log('conversation:', payload);
}

export async function handleRetrievePerson(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('retrievePerson:', payload);
}

export async function handleMarkRead(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('markConversationAsRead:', payload);
}
```

```typescript
// server/src/handlers/purchases.ts
import type { PlayerSession } from '../game/PlayerSession.js';

export async function handlePurchaseItem(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python ../lichun/ws/shop/shop_manager.py
  console.log('purchaseItem:', payload);
}

export async function handlePurchaseInAppItem(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('purchaseInAppItem:', payload);
}

export async function handlePurchaseEnergyRefill(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python ../lichun/ws/monetization/energy_refills.py
  console.log('purchaseEnergyRefill:', payload);
}

export async function handlePurchaseTimeSkip(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python ../lichun/ws/monetization/time_skips.py
  console.log('purchaseTimeSkip:', payload);
}

export async function handleGetEnergyTiers(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  session.send({
    type: 'energyRefillTiers',
    tiers: [], // TODO: Implement tiers
  });
}

export async function handleGetTimeSkipTiers(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  session.send({
    type: 'timeSkipTiers',
    tiers: [], // TODO: Implement tiers
  });
}
```

```typescript
// server/src/handlers/retention.ts
import type { PlayerSession } from '../game/PlayerSession.js';

export async function handleGetAchievements(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python ../lichun/ws/retention/achievements.py
  session.send({
    type: 'achievementsList',
    achievements: [],
  });
}

export async function handleAcknowledgeAchievement(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('acknowledgeAchievement:', payload);
}

export async function handleGetDailyRewards(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python ../lichun/ws/retention/daily_rewards.py
  session.send({
    type: 'dailyRewards',
    rewards: [],
  });
}

export async function handleClaimDailyReward(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('claimDailyReward:', payload);
}

export async function handleGetDailyQuests(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python ../lichun/ws/retention/daily_quests.py
  session.send({
    type: 'dailyQuests',
    quests: [],
  });
}

export async function handleClaimQuestReward(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('claimQuestReward:', payload);
}

export async function handleClaimEvent(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('claimEvent:', payload);
}
```

```typescript
// server/src/handlers/romance.ts
import type { PlayerSession } from '../game/PlayerSession.js';

export async function handleRomance(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python ../lichun/ws/dating/
  console.log('romance:', payload);
}

export async function handleDateNight(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('dateNight:', payload);
}

export async function handleBreakUp(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('breakUp:', payload);
}

export async function handleDivorce(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('divorce:', payload);
}

export async function handlePartnerGift(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('partnerGift:', payload);
}

export async function handleGetSwipeCharacter(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python ../lichun/ws/dating/matching.py
  console.log('getSwipeCharacter:', payload);
}

export async function handleSwipeMatch(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('swipeMatch:', payload);
}
```

```typescript
// server/src/handlers/data.ts
import type { PlayerSession } from '../game/PlayerSession.js';

export async function handleExportData(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python ../lichun/ws/api/data_management.py
  console.log('exportData:', payload);
  session.send({
    type: 'dataExportComplete',
    data: {}, // TODO: Implement GDPR export
  });
}

export async function handleDeleteAccount(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python
  console.log('deleteAccount:', payload);
  session.send({
    type: 'accountDeletionScheduled',
    scheduledAt: new Date().toISOString(),
  });
}

export async function handleGetStatistics(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // TODO: Port from Python ../lichun/ws/retention/statistics.py
  session.send({
    type: 'playerStatistics',
    statistics: {},
  });
}
```

```typescript
// server/src/handlers/generic.ts
import type { PlayerSession } from '../game/PlayerSession.js';

export async function handleGenericEvent(
  payload: unknown,
  session: PlayerSession
): Promise<void> {
  // Handle event responses (answer choices)
  const { type, key, message } = payload as {
    type?: string;
    key?: string;
    message?: unknown;
  };

  console.log('Generic event:', { type, key, message });

  // TODO: Port event handling from Python
  // call_event_handler(event_type, player, 'answer', key, message)
}
```

**Step 2: Update handler index with all handlers**

```typescript
// server/src/handlers/index.ts
import type { CommandHandler } from '../server/MessageDispatcher.js';
import { handleStart, handleStop, handleRestart, handleSpeed } from './gameControl.js';
import { handleCharacterSetup, handleDeviceToken } from './character.js';
import {
  handleApplyExtracurricular,
  handleQuitExtracurricular,
  handleApplyJob,
  handleQuitJob,
  handleQuitHabit,
  handleStopQuitHabit,
} from './activities.js';
import { handleConversation, handleRetrievePerson, handleMarkRead } from './conversations.js';
import {
  handlePurchaseItem,
  handlePurchaseInAppItem,
  handlePurchaseEnergyRefill,
  handlePurchaseTimeSkip,
  handleGetEnergyTiers,
  handleGetTimeSkipTiers,
} from './purchases.js';
import {
  handleGetAchievements,
  handleAcknowledgeAchievement,
  handleGetDailyRewards,
  handleClaimDailyReward,
  handleGetDailyQuests,
  handleClaimQuestReward,
  handleClaimEvent,
} from './retention.js';
import {
  handleRomance,
  handleDateNight,
  handleBreakUp,
  handleDivorce,
  handlePartnerGift,
  handleGetSwipeCharacter,
  handleSwipeMatch,
} from './romance.js';
import { handleExportData, handleDeleteAccount, handleGetStatistics } from './data.js';
import { handleGenericEvent } from './generic.js';

export const COMMAND_REGISTRY: Record<string, CommandHandler> = {
  // Game Control
  'start': handleStart,
  'stop': handleStop,
  'restart': handleRestart,
  'speed': handleSpeed,

  // Character
  'characterSetup': handleCharacterSetup,
  'deviceToken': handleDeviceToken,

  // Activities
  'applyForExtracurricular': handleApplyExtracurricular,
  'quitExtracurricular': handleQuitExtracurricular,
  'applyForJob': handleApplyJob,
  'quitJob': handleQuitJob,
  'quitHabit': handleQuitHabit,
  'stopQuitHabit': handleStopQuitHabit,

  // Conversations
  'conversation': handleConversation,
  'retrievePerson': handleRetrievePerson,
  'markConversationAsRead': handleMarkRead,

  // Purchases
  'purchaseItem': handlePurchaseItem,
  'purchaseInAppItem': handlePurchaseInAppItem,
  'purchaseEnergyRefill': handlePurchaseEnergyRefill,
  'purchaseTimeSkip': handlePurchaseTimeSkip,
  'getEnergyRefillTiers': handleGetEnergyTiers,
  'getTimeSkipTiers': handleGetTimeSkipTiers,

  // Retention
  'getAchievements': handleGetAchievements,
  'acknowledgeAchievement': handleAcknowledgeAchievement,
  'getDailyRewards': handleGetDailyRewards,
  'claimDailyReward': handleClaimDailyReward,
  'getDailyQuests': handleGetDailyQuests,
  'claimQuestReward': handleClaimQuestReward,
  'claimEvent': handleClaimEvent,

  // Romance
  'romance': handleRomance,
  'dateNight': handleDateNight,
  'breakUp': handleBreakUp,
  'divorce': handleDivorce,
  'partnerGift': handlePartnerGift,
  'getSwipeCharacter': handleGetSwipeCharacter,
  'swipeMatch': handleSwipeMatch,

  // Data
  'exportData': handleExportData,
  'deleteAccount': handleDeleteAccount,
  'getPlayerStatistics': handleGetStatistics,
};

// Fallback for unknown commands (event answers)
export { handleGenericEvent };
```

**Step 3: Verify build passes**

```bash
cd server && npm run build
```

Expected: No errors

**Step 4: Commit**

```bash
git add server/src/handlers/
git commit -m "feat(server): add all 35+ command handler stubs"
```

---

## Remaining Phases (Summary)

The following phases require porting Python logic and are tracked as follow-up work:

### Phase 9: Event System (Port from Python)
- Port `../lichun/ws/events/` structure
- Port event registration from `event_registration.py`
- Port all 13 event categories

### Phase 10: Services Layer
- Port `ConversationService` from `conversationEvents.py`
- Port `MatchingService` from `dating/`
- Port `AchievementService` from `retention/`

### Phase 11: Monetization
- Port energy refill logic
- Port time skip logic
- Port diamond economy

### Phase 12: Retention
- Port achievements system
- Port daily rewards
- Port daily quests
- Port tutorial

### Phase 13: Dating System
- Port bio generator
- Port compatibility scoring
- Port matching algorithm
- Port date activities

### Phase 14: Integration Testing
- WebSocket connection tests
- Full message round-trip tests
- Database integration tests

### Phase 15: Deployment
- Docker configuration
- PM2 setup
- Load testing
- Migration execution

---

## Summary

**Completed in this plan:**
- Project setup with TypeScript
- Database layer with connection pooling
- Core models (Player, Person, MessageEvent, Question)
- Server infrastructure (WebSocketServer, ConnectionRegistry, MessageDispatcher)
- Game loop with 10ms ticks
- 35+ command handler stubs

**Ready to implement next:**
- Fill in handler logic by porting from Python
- Event system architecture
- Full service layer

**Estimated remaining work:** Port Python business logic (~200k lines) into the TypeScript framework established here.

---

## Execution Checklist

- [ ] Phase 1: Project Setup (Tasks 1.1-1.3)
- [ ] Phase 2: Database Layer (Tasks 2.1-2.3)
- [ ] Phase 3: Core Models (Tasks 3.1-3.6)
- [ ] Phase 4: Server Infrastructure (Tasks 4.1-4.4)
- [ ] Phase 5: Game Loop (Tasks 5.1-5.3)
- [ ] Phase 6: Command Handlers Core (Tasks 6.1-6.2)
- [ ] Phase 7: Entry Point (Task 7.1)
- [ ] Phase 8: Handler Stubs (Task 8.1)
