Skip to content

Latest commit

 

History

History
297 lines (234 loc) · 10.6 KB

File metadata and controls

297 lines (234 loc) · 10.6 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project: Klura - Multiplayer Board Game Platform

Klura is a real-time, multiplayer digital board game platform with asymmetric gameplay. Players join via QR code/link and receive role-specific interfaces based on their assigned persona (e.g., Detective vs Murderer in a Murder Mystery game).

Current Status: Greenfield - Architecture designed, project structure created, implementation in progress.

Critical Architectural Patterns

"Command & Sync" Communication Pattern

Golden Rule: Mutations go through tRPC. Updates come through Socket.IO.

Client Action → tRPC (validates) → Backend Service (processes) → Redis (updates state)
                                                                         ↓
Client UI ← Socket.IO (broadcasts) ← GameGateway (emits GAME_UPDATE) ←┘

Constraints:

  • Client NEVER calculates game logic (dice rolls, move validation, etc.)
  • tRPC mutations return { success: true }, NOT the new game state
  • Game state arrives via Socket.IO GAME_UPDATE event to ensure all clients sync
  • Client uses Socket.IO for LISTENING only (no client→server emits except heartbeat)

Multi-Game Scalability: Game-as-Data Pattern

The system scales to 100s of games via a 3-tier content architecture:

Tier 1: Game Templates       → Reusable mechanics (murder-mystery-engine)
Tier 2: Game Variants        → Themed instances (murder-mystery-mansion)
Tier 3: Story Definitions    → Swappable narratives (blackwood-narrative)

Key Principle: Game content is data (JSON/YAML), NOT code. New games are added via configuration files in /game-content/, not new NestJS modules.

Core Engine vs Plugins:

  • /apps/backend/src/core/ = Game-agnostic logic (game-engine, persona-engine, phase-engine, action-engine, content-loader)
  • /apps/backend/src/plugins/ = Game-specific mechanics (murder-mystery.plugin.ts implements custom actions like ACCUSE_PLAYER)

Content Location:

  • Development: /game-content/ directory (JSON/YAML files)
  • Production: PostgreSQL (loaded via GameDefinitionService, cached in-memory for 30min)

Development Commands

Frontend (/apps/klura-front-end)

pnpm dev          # Start Vite dev server
pnpm build        # Type-check with vue-tsc, then build
pnpm preview      # Preview production build

Backend (/apps/backend)

Note: Backend is not yet initialized. When implemented:

pnpm dev          # Start NestJS dev server with hot reload
pnpm build        # Build TypeScript
pnpm test         # Run unit tests (Vitest/Jest)
pnpm test:e2e     # Run end-to-end tests

Shared Package (/packages/shared)

When implemented:

pnpm build        # Compile TypeScript, generate type definitions
pnpm test         # Run schema validation tests

Code Architecture

Monorepo Structure (pnpm Workspaces)

/apps/
  ├── backend/                    # NestJS + Fastify + tRPC + Socket.IO
  │   └── src/
  │       ├── core/               # Game-agnostic engine components
  │       ├── plugins/            # Game-specific mechanics
  │       ├── admin/              # Content Management API
  │       └── modules/            # tRPC routers, auth, websocket
  │
  └── klura-front-end/            # Vue 3 + Vite + Pinia + Socket.IO-client

/packages/
  └── shared/                     # Shared contracts (Zod schemas, TS types, themes)
      ├── schemas/                # Runtime validation (Zod)
      ├── types/                  # Compile-time types (TypeScript)
      └── themes/                 # Game theming (colors, fonts, mood)

/game-content/                    # Game definitions (JSON/YAML)
  ├── templates/                  # Reusable game mechanics
  ├── variants/                   # Themed game instances
  └── stories/                    # Narrative content

Backend Core Engine Flow

When a player performs an action:

  1. Client calls trpc.game.action.mutate({ roomId, actionType: 'MOVE_PLAYER', payload })
  2. tRPC Router validates input via Zod schema
  3. GameEngineService orchestrates:
    • Fetch current state from Redis
    • Load game config via GameDefinitionService
    • Delegate to ActionValidatorService (checks if action allowed)
    • Delegate to PluginRegistry (finds game-specific handler)
    • Execute action via plugin's customActions['MOVE_PLAYER']
    • Update state via GameStateManager
    • Save to Redis
  4. GameGateway emits GAME_UPDATE via Socket.IO to room
  5. PersonaEngine filters state per player (players only see info their persona allows)
  6. All Clients receive filtered state, update Pinia store, UI re-renders

Frontend Data Flow

Component (Presentation Only)
    ↓ uses
Composable (e.g., useGameSession)
    ↓ calls
tRPC Client (type-safe API calls)
    ↓ listens to
Socket.IO Client (state updates)
    ↓ updates
Pinia Store (reactive state)
    ↓ feeds back to
Component (via computed properties)

Critical: Vue components MUST NOT contain business logic. All logic goes in composables/services/stores.

Shared Contracts (/packages/shared)

All data structures are defined here FIRST (Contract-First Development):

  1. Define Zod Schema (runtime validation):

    // schemas/game-variant.schema.ts
    export const GameVariantSchema = z.object({
      id: z.string().min(1),
      templateId: z.string(),
      personas: z.array(PersonaDefinitionSchema).min(2),
      // ...
    });
  2. Infer TypeScript Type:

    // types/game.types.ts
    export type GameVariant = z.infer<typeof GameVariantSchema>;
  3. Use in Backend (validate with Zod):

    const validatedInput = GameVariantSchema.parse(input);
  4. Use in Frontend (type-safe via tRPC):

    const { data } = trpc.game.getVariant.useQuery({ variantId });
    // data is fully typed as GameVariant

Rule: any type is FORBIDDEN in /packages/shared/.

Development Workflow

Test-Driven Development (TDD)

  1. Write failing test (Red)
  2. Implement minimal code to pass (Green)
  3. Refactor for quality (Refactor)
  4. Minimum 80% test coverage for all new code

Task Management

  • Break work into atomic tasks (few hours each)
  • Commit after EVERY completed task
  • Update relevant documentation with changes

Type Safety

  • TypeScript strict mode REQUIRED (strict: true in tsconfig)
  • Avoid any; use unknown if truly uncertain
  • Explicitly declare return types for all functions

Code Style

TypeScript:

  • Interfaces: PascalCase (no I prefix)
  • Variables/Functions: camelCase
  • Constants: UPPER_SNAKE_CASE
  • Prefer const over let
  • Use readonly for immutable properties
  • Early returns to reduce nesting
  • async/await over .then() chains

Vue:

  • Always use <script setup lang="ts">
  • Components: PascalCase (e.g., GameCard.vue)
  • Props: camelCase in code, kebab-case in templates
  • Events: emit('game-started')
  • Unique :key in all v-for loops

Security & State Management

State Sanitization

Server MUST filter game state before broadcast. Players only receive information their persona can see.

Example: In Murder Mystery, the murderer's identity is in state.privateData.murdererId. This must be:

  • Sent to the murderer
  • Hidden from all other players

Implement via PersonaEngine.filterStateForPersona().

Reconnection Handling

  • Socket disconnects trigger "Reconnecting..." overlay (blocks user input)
  • On reconnect: call trpc.game.getState() to fetch authoritative state
  • Grace period: 60s before marking player disconnected
  • Rejoin window: 10 minutes to rejoin in-progress game
  • Heartbeat: Client sends every 30s; server times out after 90s

Redis State Management

  • All game state stored in Redis with key pattern: game:{roomId}
  • TTL: 24 hours (prevents memory leaks from abandoned games)
  • On game end: archive final state to PostgreSQL, delete Redis key

Theming System (Core Feature)

Each game defines its own theme in /packages/shared/themes/:

interface GameTheme {
  id: string;
  colors: { primary, secondary, background, surface, text, accent, danger, success };
  fonts: { heading, body, mono };
  mood: 'light' | 'dark' | 'dramatic';
  backgroundImage?: string;
  sounds?: { turnStart, actionConfirm, reveal };
}

Implementation:

  • Vue provide/inject at app root
  • CSS custom properties for runtime switching
  • Tailwind plugin reads theme tokens
  • 300ms animated transitions on theme change

Key Documentation

  • /conductor/code_styleguides/architecture.md - Comprehensive scalability architecture
  • /apps/backend/src/core/README.md - Core engine components
  • /apps/backend/src/plugins/README.md - Plugin system guide
  • /apps/backend/src/admin/README.md - Content Management API
  • /game-content/README.md - Content structure and examples
  • /packages/shared/README.md - Shared types and schemas
  • /initial.md - Command & Sync pattern deep dive
  • /conductor/tech-stack.md - Technology decisions and API contracts
  • /PROJECT_STRUCTURE.md - Complete project overview

API Contracts

tRPC Procedures (Client → Server)

  • game.create - Host creates lobby
  • game.join - Player joins via QR/link
  • game.start - Host starts game
  • game.getState - Fetch current state (reconnect/refresh)
  • game.action - Player performs action
  • game.leave - Player exits

All mutations return { success: true } on success, throw error on failure.

Socket.IO Events (Server → Client)

  • GAME_UPDATE - Full/partial state sync
  • PLAYER_JOINED / PLAYER_LEFT - Presence updates
  • GAME_STARTED / GAME_ENDED - Lifecycle events
  • TURN_CHANGED - Active player rotation
  • PERSONA_REVEALED - Private: sent only to target player
  • PHASE_CHANGED - Game phase transitions
  • ERROR - Server error notification

Migration Path (Phases)

Phase 1 (MVP - Current): Hardcoded Murder Mystery module in /apps/backend/src/modules/murder-mystery/ Phase 2: Extract to JSON config, implement GameDefinitionService Phase 3: Persona & story abstraction, visibility filtering Phase 4: Plugin system, refactor Murder Mystery to plugin Phase 5: Admin API for content management, hot-reload

Technology Stack

  • Frontend: Vue 3, Vite, Tailwind CSS, Pinia, socket.io-client
  • Backend: NestJS (Fastify), tRPC, Socket.IO, TypeScript
  • Database: Redis (hot), PostgreSQL/Supabase (cold)
  • Validation: Zod (runtime), TypeScript (compile-time)
  • Monorepo: pnpm workspaces