Files
soroban-abacus-flashcards/apps/web/docs/GAME_MIGRATION_PLAYBOOK.md
Thomas Hallock 7c33d0246f fix: prevent undefined displayRules error in worksheet generator
Fixes production error "Cannot read properties of undefined (reading 'carryBoxes')"
that occurred when users tried to adjust difficulty settings.

Root cause: displayRules was undefined for new users or users with old V1 config
in database. Difficulty adjustment buttons accessed displayRules.carryBoxes without
checking if displayRules existed first.

Changes:
- AdditionWorksheetClient: Initialize displayRules with defaults when missing
- ConfigPanel: Use null-coalescing operators instead of non-null assertions
- ConfigPanel: Add error logging when required fields are missing
- NEW: WorksheetErrorBoundary component to catch all errors in worksheet page
- page.tsx: Wrap client component with error boundary

This ensures users see helpful error messages instead of blank pages,
and never need to open the browser console to understand what went wrong.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 13:01:54 -06:00

26 KiB

Game Migration Playbook

Version: 1.0 Last Updated: 2025-01-16 For: Migrating legacy arcade games to the Modular Game Platform


Overview

This playbook provides step-by-step instructions for migrating a legacy arcade game to the new modular game system. Follow these steps in order to ensure a smooth, low-risk migration.

⚠️ Important: Local mode is deprecated. All games must operate in room mode only (even solo play is a single-player room).

Estimated Time: 4-8 hours per game (varies by complexity)


Prerequisites

Before starting, ensure:

  • You understand the Game SDK architecture (/src/arcade-games/README.md)
  • You've read ARCHITECTURAL_IMPROVEMENTS.md
  • The game is currently working in production
  • You have a test plan for the game

Phase 1: Analysis (30-60 minutes)

Step 1.1: Document Current Architecture

Create a migration plan document (docs/[GAME_NAME]_MIGRATION_PLAN.md) with:

Current File Structure:

List all files the game currently uses:
- State types
- Move types
- Providers (local, room, unified?)
- Components
- Validator (if exists)
- Page routes

Current State Shape:

// Document the current state interface
interface CurrentGameState {
  // List all fields
  // Mark which are UI-only vs game logic
  // Identify multiplayer fields
}

Current Move Types:

// Document all move types
type CurrentGameMove =
  | { type: 'MOVE_1', ... }
  | { type: 'MOVE_2', ... }

Current Config:

// Document game configuration
interface CurrentGameConfig {
  // List all settings
}

Step 1.2: Identify Validator Status

Check if validator exists:

grep -r "YourGameValidator" src/lib/arcade/validation/

Status:

  • Validator exists: Migration is easier (skip validator creation)
  • No validator: Need to create one (add 2-3 hours)

Step 1.3: Assess Current Provider Pattern

Which pattern does the game use?

  • Reducer Pattern: Has reducer.ts, uses useReducer, dispatches actions
  • ArcadeSession Pattern: Uses useArcadeSession, sends moves
  • Hybrid: Uses both (uh oh)

How many providers?

  • Single Provider: One provider (room mode only)
  • Dual Providers: Separate LocalProvider.tsx and RoomProvider.tsx (both will be replaced)

Note: All providers will be replaced with a single room-mode provider.

Step 1.4: Identify Complexity Factors

Rate each factor (Low/Medium/High):

  • UI State Complexity: Animations, keyboard state, timeouts
  • Timing Requirements: Card sequences, turn timers, synchronization
  • Input Handling: Real-time input, debouncing, local optimization
  • Multiplayer Scoring: Cooperative vs competitive modes, player attribution
  • Configuration Persistence: Settings saved per room/game

Complexity Score:

  • 0-2 High: Easy (2-4 hours)
  • 3-4 High: Medium (4-6 hours)
  • 5+ High: Hard (6-8+ hours)

Phase 2: Preparation (1 hour)

Step 2.1: Create New Directory Structure

mkdir -p src/arcade-games/[game-name]
mkdir -p src/arcade-games/[game-name]/components

Directory structure:

src/arcade-games/[game-name]/
├── index.ts              # Game definition (will create)
├── Validator.ts          # Move existing or create new
├── Provider.tsx          # Unified provider (will create)
├── types.ts              # Copy and update
├── game.yaml             # Optional manifest
└── components/
    ├── GameComponent.tsx # Main wrapper (will create)
    ├── SetupPhase.tsx    # Copy and update
    ├── PlayingPhase.tsx  # Copy and update
    └── ResultsPhase.tsx  # Copy and update

Step 2.2: Copy Validator

If validator exists:

cp src/lib/arcade/validation/[Game]Validator.ts \
   src/arcade-games/[game-name]/Validator.ts

Update imports in the validator:

- import type { SomeOldType } from '@/app/arcade/[game]'
+ import type { SomeNewType } from './types'

If validator doesn't exist: See Appendix A for validator creation guide.

Step 2.3: Copy Types

cp src/app/arcade/[game-name]/types.ts \
   src/arcade-games/[game-name]/types.ts

Note: You'll update these types in Phase 3.

Step 2.4: Document Migration Checklist

In your migration plan, add:

## Migration Checklist

- [ ] Phase 1: Analysis complete
- [ ] Phase 2: Preparation complete
- [ ] Phase 3: Game definition created
- [ ] Phase 4: Types updated
- [ ] Phase 5: Provider created
- [ ] Phase 6: Components updated
- [ ] Phase 7: Page route updated
- [ ] Phase 8: Testing complete
- [ ] Phase 9: Cleanup done

Phase 3: Game Definition (1 hour)

Step 3.1: Create Manifest

Option A: YAML file (recommended):

# src/arcade-games/[game-name]/game.yaml
name: game-name
displayName: Game Display Name
icon: 🎮
description: Short one-line description
longDescription: |
  Longer description explaining gameplay,
  rules, and what makes the game fun.
maxPlayers: 8
difficulty: Beginner | Intermediate | Advanced
chips:
  - 👥 Multiplayer
  - ⚡ Fast-Paced
  - 🧠 Mental Challenge
color: blue | purple | orange | green
gradient: linear-gradient(135deg, #color1, #color2)
borderColor: blue.200
available: true

Option B: Inline object:

const manifest: GameManifest = {
  name: "game-name",
  displayName: "Game Display Name",
  // ... all fields from above
};

Step 3.2: Define Default Config

const defaultConfig: YourGameConfig = {
  setting1: defaultValue1,
  setting2: defaultValue2,
  // ... all game settings with sensible defaults
};

Step 3.3: Create Config Validator

function validateYourGameConfig(config: unknown): config is YourGameConfig {
  return (
    typeof config === "object" &&
    config !== null &&
    // Check each field exists
    "setting1" in config &&
    "setting2" in config &&
    // Validate each field's type and value
    typeof (config as any).setting1 === "string" &&
    typeof (config as any).setting2 === "number" &&
    // Validate constraints
    (config as any).setting2 >= 1 &&
    (config as any).setting2 <= 100
  );
}

Step 3.4: Create Game Definition

// src/arcade-games/[game-name]/index.ts
import { defineGame } from "@/lib/arcade/game-sdk";
import type { GameManifest } from "@/lib/arcade/game-sdk";
import { GameComponent } from "./components/GameComponent";
import { YourGameProvider } from "./Provider";
import type { YourGameConfig, YourGameMove, YourGameState } from "./types";
import { yourGameValidator } from "./Validator";

const manifest: GameManifest = {
  // ... manifest from Step 3.1
};

const defaultConfig: YourGameConfig = {
  // ... config from Step 3.2
};

function validateYourGameConfig(config: unknown): config is YourGameConfig {
  // ... validator from Step 3.3
}

export const yourGame = defineGame<YourGameConfig, YourGameState, YourGameMove>(
  {
    manifest,
    Provider: YourGameProvider,
    GameComponent,
    validator: yourGameValidator,
    defaultConfig,
    validateConfig: validateYourGameConfig,
  },
);

Step 3.5: Register Game

In src/lib/arcade/game-registry.ts:

import { yourGame } from "@/arcade-games/[game-name]";

registerGame(yourGame);

In src/lib/arcade/validators.ts:

import { yourGameValidator } from "@/arcade-games/[game-name]/Validator";

export const validatorRegistry = {
  // ... other games
  "game-name": yourGameValidator,
} as const;

Step 3.6: Add Type Inference

In src/lib/arcade/game-configs.ts:

// Add type-only import
import type { yourGame } from "@/arcade-games/[game-name]";

// Infer config type
export type YourGameConfig = InferGameConfig<typeof yourGame>;

// Add to GameConfigByName
export type GameConfigByName = {
  // ... other games
  "game-name": YourGameConfig;
};

// Add default config constant
export const DEFAULT_YOUR_GAME_CONFIG: YourGameConfig = {
  setting1: defaultValue1,
  setting2: defaultValue2,
};

Phase 4: Update Types (30 minutes)

Step 4.1: Update State Interface

// src/arcade-games/[game-name]/types.ts
import type { GameConfig, GameState, GameMove } from "@/lib/arcade/game-sdk";

export interface YourGameConfig extends GameConfig {
  setting1: string;
  setting2: number;
  // ... all game settings
}

export interface YourGameState extends GameState {
  // Game-specific fields
  gamePhase: "setup" | "playing" | "results";

  // Multiplayer fields (from GameState)
  activePlayers: string[];
  playerMetadata: Record<string, PlayerMetadata>;

  // Your game's custom fields
  score: Record<string, number>;
  // ...
}

Step 4.2: Update Move Types

Ensure all moves have required fields:

export type YourGameMove =
  | {
      type: "START_GAME";
      playerId: string; // Required
      userId: string; // Required
      timestamp: number; // Added by SDK
      data: {
        activePlayers: string[];
        playerMetadata: Record<string, any>;
      };
    }
  | {
      type: "MAKE_MOVE";
      playerId: string;
      userId: string;
      timestamp: number;
      data: {
        // Move-specific data
      };
    };

Key Requirements:

  • Every move must have: playerId, userId, timestamp, data
  • Use TEAM_MOVE constant for moves where specific player doesn't matter
  • data is always an object (never primitive)

Step 4.3: Export Types

export type { PlayerMetadata } from "@/lib/arcade/player-ownership.client";
// ... any other needed types

Phase 5: Create Provider (2 hours)

Step 5.1: Provider Template (Room Mode Only)

// src/arcade-games/[game-name]/Provider.tsx
'use client'

import { createContext, useCallback, useContext, useMemo } from 'react'
import {
  useArcadeSession,
  useGameMode,
  useRoomData,
  useViewerId,
  useUpdateGameConfig,
  buildPlayerMetadata,
  TEAM_MOVE,
} from '@/lib/arcade/game-sdk'
import type { YourGameState, YourGameMove } from './types'

// Context interface
interface YourGameContextValue {
  state: YourGameState
  lastError: string | null
  clearError: () => void
  exitSession: () => void

  // Game-specific actions
  startGame: () => void
  makeMove: (data: any) => void
  // ... all your game's actions
}

const YourGameContext = createContext<YourGameContextValue | null>(null)

export function useYourGame() {
  const context = useContext(YourGameContext)
  if (!context) throw new Error('useYourGame must be used within YourGameProvider')
  return context
}

export function YourGameProvider({ children }: { children: React.ReactNode }) {
  const { data: viewerId } = useViewerId()
  const { roomData } = useRoomData()
  const { activePlayers: activePlayerIds, players } = useGameMode()
  const { mutate: updateGameConfig } = useUpdateGameConfig()

  const activePlayers = Array.from(activePlayerIds)

  // Merge saved config from room
  const initialState = useMemo(() => {
    const gameConfig = roomData?.gameConfig?.['game-name']
    return {
      // Default state
      gamePhase: 'setup' as const,
      activePlayers: [],
      playerMetadata: {},
      score: {},
      // ... your default state

      // Override with saved config from room
      setting1: gameConfig?.setting1 ?? defaultValue1,
      setting2: gameConfig?.setting2 ?? defaultValue2,
    }
  }, [roomData])

  const { state, sendMove, exitSession, lastError, clearError } =
    useArcadeSession<YourGameState>({
      userId: viewerId || '',
      roomId: roomData?.id, // Always provided (room mode only)
      initialState,
      applyMove: (state) => state, // Server handles all state updates
    })

  // Action: Start Game
  const startGame = useCallback(() => {
    const playerMetadata = buildPlayerMetadata(activePlayers, {}, players, viewerId)

    sendMove({
      type: 'START_GAME',
      playerId: activePlayers[0], // Or TEAM_MOVE
      userId: viewerId || '',
      data: { activePlayers, playerMetadata },
    })
  }, [activePlayers, players, viewerId, sendMove])

  // Action: Make Move
  const makeMove = useCallback((data: any) => {
    sendMove({
      type: 'MAKE_MOVE',
      playerId: state.currentPlayer, // Or TEAM_MOVE
      userId: viewerId || '',
      data,
    })
  }, [state.currentPlayer, viewerId, sendMove])

  // ... more action creators

  return (
    <YourGameContext.Provider value={{
      state,
      lastError,
      clearError,
      exitSession,
      startGame,
      makeMove,
      // ... all actions
    }}>
      {children}
    </YourGameContext.Provider>
  )
}

Step 5.2: Handle Config Persistence

For any setting that should persist:

const setConfig = useCallback(
  (field: keyof YourGameConfig, value: any) => {
    // Send move to update state immediately
    sendMove({
      type: "SET_CONFIG",
      playerId: TEAM_MOVE,
      userId: viewerId || "",
      data: { field, value },
    });

    // Persist to database (always - room mode only)
    if (roomData?.id) {
      const currentGameConfig =
        (roomData.gameConfig as Record<string, any>) || {};
      const currentConfig =
        (currentGameConfig["game-name"] as Record<string, any>) || {};

      updateGameConfig({
        roomId: roomData.id,
        gameConfig: {
          ...currentGameConfig,
          "game-name": {
            ...currentConfig,
            [field]: value,
          },
        },
      });
    }
  },
  [viewerId, sendMove, roomData, updateGameConfig],
);

Step 5.3: Handle Local-Only State

For UI state that doesn't need network sync:

// Example: Input field that updates every keystroke
const [localInput, setLocalInput] = useState("");

// Merge with network state
const mergedState = {
  ...state,
  currentInput: localInput, // Override with local value
};

// Only send to network when submitting
const submitAnswer = useCallback(
  (answer: string) => {
    sendMove({
      type: "SUBMIT_ANSWER",
      playerId: state.currentPlayer,
      userId: viewerId || "",
      data: { answer },
    });
    setLocalInput(""); // Clear local state
  },
  [state.currentPlayer, viewerId, sendMove],
);

Phase 6: Update Components (1 hour)

Step 6.1: Create GameComponent

// src/arcade-games/[game-name]/components/GameComponent.tsx
'use client'

import { useRouter } from 'next/navigation'
import { PageWithNav } from '@/components/PageWithNav'
import { useYourGame } from '../Provider'
import { SetupPhase } from './SetupPhase'
import { PlayingPhase } from './PlayingPhase'
import { ResultsPhase } from './ResultsPhase'

export function GameComponent() {
  const router = useRouter()
  const { state, exitSession } = useYourGame()

  // Determine current player for turn indicator (if turn-based)
  const currentPlayerId = state.gamePhase === 'playing' ? state.currentPlayer : undefined

  return (
    <PageWithNav
      navTitle="Your Game Name"
      navEmoji="🎮"
      emphasizePlayerSelection={state.gamePhase === 'setup'}
      currentPlayerId={currentPlayerId}
      playerScores={state.score}
      onExitSession={() => {
        exitSession()
        router.push('/arcade')
      }}
    >
      {state.gamePhase === 'setup' && <SetupPhase />}
      {state.gamePhase === 'playing' && <PlayingPhase />}
      {state.gamePhase === 'results' && <ResultsPhase />}
    </PageWithNav>
  )
}

Step 6.2: Update Phase Components

Pattern for each component:

import { useYourGame } from '../Provider'

export function YourPhase() {
  const { state, actionName1, actionName2 } = useYourGame()

  // Replace dispatch() calls with action creators
  // Before: dispatch({ type: 'SOME_ACTION', data })
  // After:  actionName1(data)

  return (
    // ... JSX (mostly unchanged)
  )
}

Common Changes:

- const { state, dispatch } = useYourGame()
+ const { state, actionName } = useYourGame()

- dispatch({ type: 'START_GAME' })
+ startGame()

- dispatch({ type: 'MAKE_MOVE', data: { value } })
+ makeMove({ value })

- dispatch({ type: 'SET_CONFIG', field, value })
+ setConfig(field, value)

Step 6.3: Handle Error Display

Add error banner to appropriate phase(s):

const { lastError, clearError } = useYourGame()

{lastError && (
  <div className="error-banner">
    <span>⚠️ {lastError}</span>
    <button onClick={clearError}>Dismiss</button>
  </div>
)}

Phase 7: Update Page Route (15 minutes)

Step 7.1: Update Arcade Page

// src/app/arcade/[game-name]/page.tsx
'use client'

import { yourGame } from '@/arcade-games/[game-name]'

const { Provider, GameComponent } = yourGame

export default function YourGamePage() {
  return (
    <Provider>
      <GameComponent />
    </Provider>
  )
}

That's it! The game is now on the modular platform.

Step 7.2: Update Game Selector (if needed)

Check if GameSelector.tsx needs updates. It should automatically pick up registered games from the registry.


Phase 8: Testing (1 hour)

Step 8.1: Solo Play Testing

Test all functionality with a single player in a room:

  • Setup Phase

    • All settings render correctly
    • Can change all settings
    • Settings have sensible defaults
    • Settings persist across page reloads
    • Can start game
  • Playing Phase

    • Game progresses correctly
    • All moves work
    • Scoring works
    • Turn indicators work (if turn-based)
    • Animations work
    • Input handling works
  • Results Phase

    • Scores display correctly
    • Can play again
    • Can exit game

Step 8.2: Multiplayer Testing

Test with multiple players:

  • Settings Persistence

    • Settings save when changed
    • Settings persist across page reloads
    • Settings don't leak across games
  • Synchronization

    • All players see same state
    • Moves from any player work
    • Turn indicators work (if turn-based)
    • Timing is synchronized
  • Scoring

    • Player attribution works
    • Scores track correctly per player
    • Multiplayer modes work (if applicable)

Step 8.3: Edge Case Testing

  • Navigation

    • Can exit mid-game
    • Can switch games
    • Settings preserved when returning
  • Error Handling

    • Invalid moves rejected gracefully
    • Network errors handled
    • Server validation errors shown

Step 8.4: Pre-Commit Checks

npm run pre-commit

Must pass:

  • TypeScript compilation (0 errors)
  • Format check (all files formatted)
  • Lint check (0 errors, 0 warnings)

Phase 9: Cleanup (30 minutes)

Step 9.1: Delete Old Files

# If reducer pattern was used
rm src/app/arcade/[game-name]/reducer.ts

# Delete any local mode providers (deprecated)
rm src/app/arcade/[game-name]/context/LocalProvider.tsx
rm src/app/arcade/[game-name]/context/Local*Provider.tsx

# If dual providers were used
rm src/app/arcade/[game-name]/context/RoomProvider.tsx

# Delete old local mode page (if exists)
rm src/app/games/[game-name]/page.tsx

# If validator was moved
rm src/lib/arcade/validation/[Game]Validator.ts

Note: Only the arcade version (/app/arcade/[game-name]/page.tsx) should remain, using the new modular game definition.

Step 9.2: Update Documentation

In ARCHITECTURAL_IMPROVEMENTS.md:

### Migrated Games

- ✅ Number Guesser (v4.0.0)
- ✅ Math Sprint (v4.0.1)
- ✅ Memory Quiz (v4.1.0) - Completed 2025-01-16

In your migration plan:

## Migration Complete ✅

**Date Completed**: YYYY-MM-DD
**Final Commit**: [commit hash]
**Testing**: All tests passed
**Status**: Live in production

Step 9.3: Commit Migration

git add -A
git commit -m "feat(arcade): migrate [game-name] to modular platform

Migrates [game-name] from legacy architecture to modular game SDK.

Changes:
- Moved from /app/arcade/[game-name] to /arcade-games/[game-name]
- Created game definition with defineGame()
- Unified provider for local and room modes
- Added config validation
- Updated all components to use SDK patterns

Closes #[issue-number]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>"

git push

Troubleshooting

Issue: TypeScript errors after migration

Symptom: Lots of type errors in components

Solution:

  1. Check that move types include playerId, userId, timestamp
  2. Verify state extends GameState from SDK
  3. Update component imports to use new provider location

Issue: State not syncing across players

Symptom: Players see different states

Solution:

  1. Verify roomId is passed to useArcadeSession (should always be present)
  2. Check validator is handling moves correctly
  3. Ensure applyMove returns state (doesn't mutate)
  4. Verify room actually has a roomId (not undefined)

Issue: Settings not persisting

Symptom: Settings reset on page reload

Solution:

  1. Check useUpdateGameConfig is called in setConfig
  2. Verify config is scoped by game name: gameConfig['game-name']
  3. Ensure initialState useMemo depends on roomData

Issue: Moves rejected with "Unknown move type"

Symptom: Validator rejects all moves

Solution:

  1. Check move type strings match validator switch cases
  2. Verify validator is registered correctly
  3. Check move structure includes all required fields

Issue: Components crash on mount

Symptom: useContext errors or null reference errors

Solution:

  1. Verify Provider wraps all components
  2. Check context exports match usage
  3. Ensure hooks are called within Provider

Appendix A: Creating a Validator

If your game doesn't have a validator, create one:

// src/arcade-games/[game-name]/Validator.ts
import type { GameValidator, ValidationResult } from "@/lib/arcade/game-sdk";
import type { YourGameState, YourGameMove } from "./types";

export class YourGameValidator
  implements GameValidator<YourGameState, YourGameMove>
{
  validateMove(
    state: YourGameState,
    move: YourGameMove,
    context?: { userId?: string },
  ): ValidationResult {
    switch (move.type) {
      case "START_GAME":
        return this.validateStartGame(state, move.data);

      case "MAKE_MOVE":
        return this.validateMakeMove(state, move.playerId, move.data);

      // ... handle each move type

      default:
        return { valid: false, error: "Unknown move type" };
    }
  }

  private validateStartGame(state: YourGameState, data: any): ValidationResult {
    // Check preconditions
    if (state.gamePhase !== "setup") {
      return { valid: false, error: "Can only start from setup phase" };
    }

    // Create new state
    const newState: YourGameState = {
      ...state,
      gamePhase: "playing",
      activePlayers: data.activePlayers || [],
      playerMetadata: data.playerMetadata || {},
      // ... initialize game state
    };

    return { valid: true, newState };
  }

  // ... more validation methods

  isGameComplete(state: YourGameState): boolean {
    return state.gamePhase === "results";
  }

  getInitialState(config: YourGameConfig): YourGameState {
    return {
      gamePhase: "setup",
      activePlayers: [],
      playerMetadata: {},
      // ... all initial state from config
    };
  }
}

export const yourGameValidator = new YourGameValidator();

Key Principles:

  1. Validator is authoritative (client is display only)
  2. Always return new state (never mutate)
  3. Validate all preconditions before state changes
  4. Return clear error messages for invalid moves
  5. Handle all edge cases

Appendix B: Common Patterns

Pattern: Turn-Based Games

// Validator
if (move.playerId !== state.currentPlayer) {
  return { valid: false, error: "Not your turn" };
}

// After valid move, rotate turn
const nextPlayerIndex = (currentIndex + 1) % state.activePlayers.length;
const newState = {
  ...state,
  currentPlayer: state.activePlayers[nextPlayerIndex],
};

Pattern: Team Moves

// Use TEAM_MOVE for moves where any player can act
sendMove({
  type: "SUBMIT_ANSWER",
  playerId: TEAM_MOVE, // Not attributed to specific player
  userId: viewerId || "",
  data: { answer },
});

Pattern: Timed Actions

// Store timestamp in state, check elapsed time
const elapsedTime = Date.now() - state.startTime;
if (elapsedTime > state.timeLimit) {
  return { valid: false, error: "Time expired" };
}

Pattern: Scoring by User

// Track scores by userId (not playerId) for multi-character games
const newScores = { ...state.scores };
if (move.userId) {
  newScores[move.userId] = (newScores[move.userId] || 0) + points;
}

Summary Checklist

Use this as a final verification before declaring migration complete:

Code Quality

  • TypeScript compiles with 0 errors
  • Linter passes with 0 errors, 0 warnings
  • Formatter has been run
  • No console errors in browser
  • No console warnings in terminal

Architecture

  • Game definition created with defineGame()
  • Validator implements GameValidator interface
  • Provider uses SDK hooks exclusively
  • Types extend SDK base types
  • Config validation function exists

Registration

  • Game registered in game-registry.ts
  • Validator registered in validators.ts
  • Config type inferred in game-configs.ts
  • Default config constant added

Functionality

  • All game phases work in local mode
  • All game phases work in room mode
  • Settings persist in room mode
  • Multiplayer synchronization works
  • Scoring tracks correctly
  • Can exit and re-enter game

Documentation

  • Migration plan created
  • Breaking changes documented
  • Testing results recorded
  • Commit messages are clear

Conclusion

You've now successfully migrated a game to the modular platform! 🎉

The game is now:

  • Self-contained in /arcade-games/[game-name]/
  • Using the Game SDK for all functionality
  • Room mode only (no local mode code)
  • Automatically type-checked
  • Validated server-side
  • Settings always persist to room config
  • Easier to maintain and extend

Next Steps:

  1. Monitor game in production for any issues
  2. Consider adding game-specific settings
  3. Migrate the next legacy game!

Questions or issues? Refer to:

  • Game SDK Docs: /src/arcade-games/README.md
  • Architecture Docs: /docs/ARCHITECTURAL_IMPROVEMENTS.md
  • Example Games: Number Guesser, Math Sprint, Memory Quiz