docs(arcade): document game settings persistence architecture
Added comprehensive documentation for game settings persistence system after fixing multiple settings bugs (gameType, playMode not persisting). New documentation: - .claude/GAME_SETTINGS_PERSISTENCE.md: Complete architecture guide - How settings are structured (nested by game name) - Three critical systems that must stay in sync - Common bugs with detailed solutions - Debugging checklist - Step-by-step guide for adding new settings - .claude/GAME_SETTINGS_REFACTORING.md: Recommended improvements - Shared config types to prevent type mismatches - Helper functions to reduce duplication (getGameConfig, updateGameConfig) - Validator config type enforcement - Exhaustiveness checking - Runtime validation - Migration strategy with priority order Updated .claude/CLAUDE.md to reference these docs with quick reference guide. This documentation will prevent similar bugs in the future by making the architecture explicit and providing clear patterns to follow. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -108,3 +108,31 @@ npm run check # Biome check (format + lint + organize imports)
|
||||
- Lint checks
|
||||
|
||||
**Status:** Known issue, does not block development or deployment.
|
||||
|
||||
## Game Settings Persistence
|
||||
|
||||
When working on arcade room game settings, refer to:
|
||||
|
||||
- **`.claude/GAME_SETTINGS_PERSISTENCE.md`** - Complete architecture documentation
|
||||
- How settings are stored (nested by game name)
|
||||
- Three critical systems that must stay in sync
|
||||
- Common bugs and their solutions
|
||||
- Debugging checklist
|
||||
- Step-by-step guide for adding new settings
|
||||
|
||||
- **`.claude/GAME_SETTINGS_REFACTORING.md`** - Recommended improvements
|
||||
- Shared config types to prevent inconsistencies
|
||||
- Helper functions to reduce duplication
|
||||
- Type-safe validation
|
||||
- Migration strategy
|
||||
|
||||
**Quick Reference:**
|
||||
|
||||
Settings are stored as: `gameConfig[gameName][setting]`
|
||||
|
||||
Three places must handle settings correctly:
|
||||
1. **Provider** (`Room{Game}Provider.tsx`) - Merges saved config with defaults
|
||||
2. **Socket Server** (`socket-server.ts`) - Creates session from saved config
|
||||
3. **Validator** (`{Game}Validator.ts`) - `getInitialState()` must accept ALL settings
|
||||
|
||||
If a setting doesn't persist, check all three locations.
|
||||
|
||||
237
apps/web/.claude/GAME_SETTINGS_PERSISTENCE.md
Normal file
237
apps/web/.claude/GAME_SETTINGS_PERSISTENCE.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Game Settings Persistence Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Game settings in room mode persist across game switches using a nested gameConfig structure. This document describes the architecture and common pitfalls.
|
||||
|
||||
## Data Structure
|
||||
|
||||
Settings are stored in the `arcadeRooms` table's `gameConfig` column with this structure:
|
||||
|
||||
```typescript
|
||||
{
|
||||
"matching": {
|
||||
"gameType": "complement-pairs",
|
||||
"difficulty": 15,
|
||||
"turnTimer": 60
|
||||
},
|
||||
"memory-quiz": {
|
||||
"selectedCount": 8,
|
||||
"displayTime": 3.0,
|
||||
"selectedDifficulty": "medium",
|
||||
"playMode": "competitive"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Settings are **nested by game name** (`matching`, `memory-quiz`, etc.)
|
||||
- Each game has its own isolated settings object
|
||||
- This allows switching games without losing settings
|
||||
|
||||
## Critical Components
|
||||
|
||||
Settings persistence requires coordination between THREE systems:
|
||||
|
||||
### 1. Client-Side Provider
|
||||
**Location:** `src/app/arcade/{game}/context/Room{Game}Provider.tsx`
|
||||
|
||||
**Responsibilities:**
|
||||
- Load saved settings from `roomData.gameConfig[gameName]`
|
||||
- Merge saved settings with `initialState` defaults
|
||||
- Save settings changes to database via `updateGameConfig`
|
||||
|
||||
**Example:** `RoomMemoryQuizProvider.tsx:211-247`
|
||||
```typescript
|
||||
const mergedInitialState = useMemo(() => {
|
||||
const gameConfig = roomData?.gameConfig as Record<string, any>
|
||||
const savedConfig = gameConfig?.['memory-quiz']
|
||||
|
||||
return {
|
||||
...initialState,
|
||||
selectedCount: savedConfig?.selectedCount ?? initialState.selectedCount,
|
||||
displayTime: savedConfig?.displayTime ?? initialState.displayTime,
|
||||
selectedDifficulty: savedConfig?.selectedDifficulty ?? initialState.selectedDifficulty,
|
||||
playMode: savedConfig?.playMode ?? initialState.playMode,
|
||||
}
|
||||
}, [roomData?.gameConfig])
|
||||
```
|
||||
|
||||
### 2. Socket Server (Session Creation)
|
||||
**Location:** `src/socket-server.ts:86-125`
|
||||
|
||||
**Responsibilities:**
|
||||
- Create initial arcade session when user joins room
|
||||
- Read saved settings from `room.gameConfig[gameName]`
|
||||
- Pass settings to validator's `getInitialState()`
|
||||
|
||||
**Example:** `socket-server.ts:94-110`
|
||||
```typescript
|
||||
const memoryQuizConfig = (room.gameConfig as any)?.['memory-quiz'] || {}
|
||||
initialState = validator.getInitialState({
|
||||
selectedCount: memoryQuizConfig.selectedCount || 5,
|
||||
displayTime: memoryQuizConfig.displayTime || 2.0,
|
||||
selectedDifficulty: memoryQuizConfig.selectedDifficulty || 'easy',
|
||||
playMode: memoryQuizConfig.playMode || 'cooperative',
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Game Validator
|
||||
**Location:** `src/lib/arcade/validation/{Game}Validator.ts`
|
||||
|
||||
**Responsibilities:**
|
||||
- Define `getInitialState()` method that creates initial game state
|
||||
- Accept ALL game settings as parameters
|
||||
- Use provided values or fall back to defaults
|
||||
|
||||
**Example:** `MemoryQuizGameValidator.ts:404-442`
|
||||
```typescript
|
||||
getInitialState(config: {
|
||||
selectedCount: number
|
||||
displayTime: number
|
||||
selectedDifficulty: DifficultyLevel
|
||||
playMode?: 'cooperative' | 'competitive' // ← Must include ALL settings!
|
||||
}): SorobanQuizState {
|
||||
return {
|
||||
// ...
|
||||
selectedCount: config.selectedCount,
|
||||
displayTime: config.displayTime,
|
||||
selectedDifficulty: config.selectedDifficulty,
|
||||
playMode: config.playMode || 'cooperative', // ← Use config value!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Bugs and Solutions
|
||||
|
||||
### Bug #1: Settings Wiped When Returning to Game Selection
|
||||
|
||||
**Symptom:** Settings reset to defaults after going back to game selection
|
||||
|
||||
**Root Cause:** `clearRoomGameApi` was sending `gameConfig: null`
|
||||
|
||||
**Solution:** Only send `gameName: null`, don't send `gameConfig` at all
|
||||
```typescript
|
||||
// ❌ WRONG
|
||||
body: JSON.stringify({ gameName: null, gameConfig: null })
|
||||
|
||||
// ✅ CORRECT
|
||||
body: JSON.stringify({ gameName: null })
|
||||
```
|
||||
|
||||
**Fixed in:** `useRoomData.ts:638-654`
|
||||
|
||||
### Bug #2: Settings Not Loaded from Database
|
||||
|
||||
**Symptom:** Session always uses default settings, ignoring saved values
|
||||
|
||||
**Root Cause:** Socket server reading from wrong level of nesting
|
||||
|
||||
**Example Problem:**
|
||||
```typescript
|
||||
// ❌ WRONG - reads from root level
|
||||
const gameType = room.gameConfig.gameType // undefined!
|
||||
|
||||
// ✅ CORRECT - reads from nested game config
|
||||
const matchingConfig = room.gameConfig.matching
|
||||
const gameType = matchingConfig.gameType
|
||||
```
|
||||
|
||||
**Fixed in:** `socket-server.ts:86-125`
|
||||
|
||||
### Bug #3: Validator Ignores Setting
|
||||
|
||||
**Symptom:** Specific setting always resets to default (e.g., playMode always "cooperative")
|
||||
|
||||
**Root Cause:** Validator's `getInitialState()` config parameter missing the setting
|
||||
|
||||
**How to Diagnose:**
|
||||
1. Check validator's `getInitialState()` TypeScript signature
|
||||
2. Ensure ALL game settings are included as parameters
|
||||
3. Ensure settings use `config.{setting}` not hardcoded values
|
||||
|
||||
**Example Problem:**
|
||||
```typescript
|
||||
// ❌ WRONG - playMode not in config type
|
||||
getInitialState(config: {
|
||||
selectedCount: number
|
||||
displayTime: number
|
||||
selectedDifficulty: DifficultyLevel
|
||||
}): SorobanQuizState {
|
||||
return {
|
||||
playMode: 'cooperative', // ← Hardcoded!
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ CORRECT - playMode in config type and used
|
||||
getInitialState(config: {
|
||||
selectedCount: number
|
||||
displayTime: number
|
||||
selectedDifficulty: DifficultyLevel
|
||||
playMode?: 'cooperative' | 'competitive'
|
||||
}): SorobanQuizState {
|
||||
return {
|
||||
playMode: config.playMode || 'cooperative', // ← Uses config!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fixed in:** `MemoryQuizGameValidator.ts:404-442`
|
||||
|
||||
## Debugging Checklist
|
||||
|
||||
When a setting doesn't persist:
|
||||
|
||||
1. **Check database:**
|
||||
- Add logging in `/api/arcade/rooms/[roomId]/settings/route.ts`
|
||||
- Verify setting is saved to `gameConfig[gameName]`
|
||||
|
||||
2. **Check socket server:**
|
||||
- Add logging in `socket-server.ts` join-arcade-session handler
|
||||
- Verify `room.gameConfig[gameName].{setting}` is read correctly
|
||||
- Verify setting is passed to `validator.getInitialState()`
|
||||
- Verify `initialState.{setting}` has correct value after getInitialState()
|
||||
|
||||
3. **Check validator:**
|
||||
- Verify `getInitialState()` config parameter includes the setting
|
||||
- Verify setting uses `config.{setting}` not a hardcoded value
|
||||
- Add logging to see what config is received and what state is returned
|
||||
|
||||
4. **Check client provider:**
|
||||
- Add logging in `Room{Game}Provider.tsx` mergedInitialState useMemo
|
||||
- Verify `roomData.gameConfig[gameName].{setting}` is read correctly
|
||||
- Verify merged state includes the saved value
|
||||
|
||||
## Adding a New Setting
|
||||
|
||||
To add a new setting to an existing game:
|
||||
|
||||
1. **Update the game's state type** (`types.ts`)
|
||||
2. **Update the Provider** (`Room{Game}Provider.tsx`):
|
||||
- Add to `mergedInitialState` useMemo
|
||||
- Add to `setConfig` function
|
||||
- Add to `updateGameConfig` call
|
||||
3. **Update the Validator** (`{Game}Validator.ts`):
|
||||
- Add to `getInitialState()` config parameter type
|
||||
- Add to returned state object, using `config.{setting}`
|
||||
- Add validation case in `validateSetConfig()`
|
||||
4. **Update Socket Server** (`socket-server.ts`):
|
||||
- Add to `{game}Config` extraction
|
||||
- Add to `getInitialState()` call
|
||||
5. **Update the UI component** to expose the setting
|
||||
|
||||
## Testing Settings Persistence
|
||||
|
||||
Manual test procedure:
|
||||
|
||||
1. Join a room and select a game
|
||||
2. Change each setting to a non-default value
|
||||
3. Go back to game selection (gameName becomes null)
|
||||
4. Select the same game again
|
||||
5. Verify ALL settings retained their values
|
||||
|
||||
**Expected behavior:** All settings should be exactly as you left them.
|
||||
|
||||
## Refactoring Recommendations
|
||||
|
||||
See next section for suggested improvements to prevent these bugs.
|
||||
331
apps/web/.claude/GAME_SETTINGS_REFACTORING.md
Normal file
331
apps/web/.claude/GAME_SETTINGS_REFACTORING.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# Game Settings Persistence - Refactoring Recommendations
|
||||
|
||||
## Current Pain Points
|
||||
|
||||
1. **Type safety is weak** - Easy to forget to add a setting in one place
|
||||
2. **Duplication** - Config reading logic duplicated in socket-server.ts for each game
|
||||
3. **Manual synchronization** - Have to manually keep validator signature, provider, and socket server in sync
|
||||
4. **Error-prone** - Easy to hardcode values or forget to read from config
|
||||
|
||||
## Recommended Refactorings
|
||||
|
||||
### 1. Create Shared Config Types (HIGHEST PRIORITY)
|
||||
|
||||
**Problem:** Each game's settings are defined in multiple places with no type enforcement
|
||||
|
||||
**Solution:** Define a single source of truth for each game's config
|
||||
|
||||
```typescript
|
||||
// src/lib/arcade/game-configs.ts
|
||||
|
||||
export interface MatchingGameConfig {
|
||||
gameType: 'abacus-numeral' | 'complement-pairs'
|
||||
difficulty: number
|
||||
turnTimer: number
|
||||
}
|
||||
|
||||
export interface MemoryQuizGameConfig {
|
||||
selectedCount: 2 | 5 | 8 | 12 | 15
|
||||
displayTime: number
|
||||
selectedDifficulty: DifficultyLevel
|
||||
playMode: 'cooperative' | 'competitive'
|
||||
}
|
||||
|
||||
export interface ComplementRaceGameConfig {
|
||||
// ... future settings
|
||||
}
|
||||
|
||||
export interface RoomGameConfig {
|
||||
matching?: MatchingGameConfig
|
||||
'memory-quiz'?: MemoryQuizGameConfig
|
||||
'complement-race'?: ComplementRaceGameConfig
|
||||
}
|
||||
|
||||
// Default configs
|
||||
export const DEFAULT_MATCHING_CONFIG: MatchingGameConfig = {
|
||||
gameType: 'abacus-numeral',
|
||||
difficulty: 6,
|
||||
turnTimer: 30,
|
||||
}
|
||||
|
||||
export const DEFAULT_MEMORY_QUIZ_CONFIG: MemoryQuizGameConfig = {
|
||||
selectedCount: 5,
|
||||
displayTime: 2.0,
|
||||
selectedDifficulty: 'easy',
|
||||
playMode: 'cooperative',
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Single source of truth for each game's settings
|
||||
- TypeScript enforces consistency across codebase
|
||||
- Easy to see what settings each game has
|
||||
|
||||
### 2. Create Config Helper Functions
|
||||
|
||||
**Problem:** Config reading logic is duplicated and error-prone
|
||||
|
||||
**Solution:** Centralized helper functions with type safety
|
||||
|
||||
```typescript
|
||||
// src/lib/arcade/game-config-helpers.ts
|
||||
|
||||
import type { GameName } from './validation'
|
||||
import type { RoomGameConfig, MatchingGameConfig, MemoryQuizGameConfig } from './game-configs'
|
||||
import { DEFAULT_MATCHING_CONFIG, DEFAULT_MEMORY_QUIZ_CONFIG } from './game-configs'
|
||||
|
||||
/**
|
||||
* Get game-specific config from room's gameConfig with defaults
|
||||
*/
|
||||
export function getGameConfig<T extends GameName>(
|
||||
roomGameConfig: RoomGameConfig | null | undefined,
|
||||
gameName: T
|
||||
): T extends 'matching'
|
||||
? MatchingGameConfig
|
||||
: T extends 'memory-quiz'
|
||||
? MemoryQuizGameConfig
|
||||
: never {
|
||||
|
||||
if (!roomGameConfig) {
|
||||
return getDefaultGameConfig(gameName) as any
|
||||
}
|
||||
|
||||
const savedConfig = roomGameConfig[gameName]
|
||||
if (!savedConfig) {
|
||||
return getDefaultGameConfig(gameName) as any
|
||||
}
|
||||
|
||||
// Merge saved config with defaults to handle missing fields
|
||||
const defaults = getDefaultGameConfig(gameName)
|
||||
return { ...defaults, ...savedConfig } as any
|
||||
}
|
||||
|
||||
function getDefaultGameConfig(gameName: GameName) {
|
||||
switch (gameName) {
|
||||
case 'matching':
|
||||
return DEFAULT_MATCHING_CONFIG
|
||||
case 'memory-quiz':
|
||||
return DEFAULT_MEMORY_QUIZ_CONFIG
|
||||
case 'complement-race':
|
||||
// return DEFAULT_COMPLEMENT_RACE_CONFIG
|
||||
throw new Error('complement-race config not implemented')
|
||||
default:
|
||||
throw new Error(`Unknown game: ${gameName}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific game's config in the room's gameConfig
|
||||
*/
|
||||
export function updateGameConfig<T extends GameName>(
|
||||
currentRoomConfig: RoomGameConfig | null | undefined,
|
||||
gameName: T,
|
||||
updates: Partial<T extends 'matching' ? MatchingGameConfig : T extends 'memory-quiz' ? MemoryQuizGameConfig : never>
|
||||
): RoomGameConfig {
|
||||
const current = currentRoomConfig || {}
|
||||
const gameConfig = current[gameName] || getDefaultGameConfig(gameName)
|
||||
|
||||
return {
|
||||
...current,
|
||||
[gameName]: {
|
||||
...gameConfig,
|
||||
...updates,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in socket-server.ts:**
|
||||
```typescript
|
||||
// BEFORE (error-prone, duplicated)
|
||||
const memoryQuizConfig = (room.gameConfig as any)?.['memory-quiz'] || {}
|
||||
initialState = validator.getInitialState({
|
||||
selectedCount: memoryQuizConfig.selectedCount || 5,
|
||||
displayTime: memoryQuizConfig.displayTime || 2.0,
|
||||
selectedDifficulty: memoryQuizConfig.selectedDifficulty || 'easy',
|
||||
playMode: memoryQuizConfig.playMode || 'cooperative',
|
||||
})
|
||||
|
||||
// AFTER (type-safe, concise)
|
||||
const config = getGameConfig(room.gameConfig, 'memory-quiz')
|
||||
initialState = validator.getInitialState(config)
|
||||
```
|
||||
|
||||
**Usage in RoomMemoryQuizProvider.tsx:**
|
||||
```typescript
|
||||
// BEFORE (verbose, error-prone)
|
||||
const mergedInitialState = useMemo(() => {
|
||||
const gameConfig = roomData?.gameConfig as Record<string, any>
|
||||
const savedConfig = gameConfig?.['memory-quiz']
|
||||
|
||||
return {
|
||||
...initialState,
|
||||
selectedCount: savedConfig?.selectedCount ?? initialState.selectedCount,
|
||||
displayTime: savedConfig?.displayTime ?? initialState.displayTime,
|
||||
selectedDifficulty: savedConfig?.selectedDifficulty ?? initialState.selectedDifficulty,
|
||||
playMode: savedConfig?.playMode ?? initialState.playMode,
|
||||
}
|
||||
}, [roomData?.gameConfig])
|
||||
|
||||
// AFTER (type-safe, concise)
|
||||
const mergedInitialState = useMemo(() => {
|
||||
const config = getGameConfig(roomData?.gameConfig, 'memory-quiz')
|
||||
return {
|
||||
...initialState,
|
||||
...config, // Spread config directly - all settings included
|
||||
}
|
||||
}, [roomData?.gameConfig])
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- No more manual property-by-property merging
|
||||
- Type-safe
|
||||
- Defaults handled automatically
|
||||
- Reusable across codebase
|
||||
|
||||
### 3. Enforce Validator Config Type from Game Config
|
||||
|
||||
**Problem:** Easy to forget to add a new setting to validator's `getInitialState()` signature
|
||||
|
||||
**Solution:** Make validator use the shared config type
|
||||
|
||||
```typescript
|
||||
// src/lib/arcade/validation/MemoryQuizGameValidator.ts
|
||||
|
||||
import type { MemoryQuizGameConfig } from '@/lib/arcade/game-configs'
|
||||
|
||||
export class MemoryQuizGameValidator {
|
||||
// BEFORE: Manual type definition
|
||||
// getInitialState(config: {
|
||||
// selectedCount: number
|
||||
// displayTime: number
|
||||
// selectedDifficulty: DifficultyLevel
|
||||
// playMode?: 'cooperative' | 'competitive'
|
||||
// }): SorobanQuizState
|
||||
|
||||
// AFTER: Use shared type
|
||||
getInitialState(config: MemoryQuizGameConfig): SorobanQuizState {
|
||||
return {
|
||||
// ...
|
||||
selectedCount: config.selectedCount,
|
||||
displayTime: config.displayTime,
|
||||
selectedDifficulty: config.selectedDifficulty,
|
||||
playMode: config.playMode, // TypeScript ensures all fields are handled
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- If you add a setting to `MemoryQuizGameConfig`, TypeScript forces you to handle it
|
||||
- Impossible to forget a setting
|
||||
- Impossible to use wrong type
|
||||
|
||||
### 4. Add Exhaustiveness Checking
|
||||
|
||||
**Problem:** Easy to miss handling a setting field
|
||||
|
||||
**Solution:** Use TypeScript's exhaustiveness checking
|
||||
|
||||
```typescript
|
||||
// src/lib/arcade/validation/MemoryQuizGameValidator.ts
|
||||
|
||||
getInitialState(config: MemoryQuizGameConfig): SorobanQuizState {
|
||||
// Exhaustiveness check - ensures all config fields are used
|
||||
const _exhaustivenessCheck: Record<keyof MemoryQuizGameConfig, boolean> = {
|
||||
selectedCount: true,
|
||||
displayTime: true,
|
||||
selectedDifficulty: true,
|
||||
playMode: true,
|
||||
}
|
||||
|
||||
return {
|
||||
// ... use all config fields
|
||||
selectedCount: config.selectedCount,
|
||||
displayTime: config.displayTime,
|
||||
selectedDifficulty: config.selectedDifficulty,
|
||||
playMode: config.playMode,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you add a new field to `MemoryQuizGameConfig`, TypeScript will error on `_exhaustivenessCheck` until you add it.
|
||||
|
||||
### 5. Validate Config on Save
|
||||
|
||||
**Problem:** Invalid config can be saved to database
|
||||
|
||||
**Solution:** Add runtime validation
|
||||
|
||||
```typescript
|
||||
// src/lib/arcade/game-config-helpers.ts
|
||||
|
||||
export function validateGameConfig(
|
||||
gameName: GameName,
|
||||
config: any
|
||||
): config is MatchingGameConfig | MemoryQuizGameConfig {
|
||||
switch (gameName) {
|
||||
case 'matching':
|
||||
return (
|
||||
typeof config.gameType === 'string' &&
|
||||
['abacus-numeral', 'complement-pairs'].includes(config.gameType) &&
|
||||
typeof config.difficulty === 'number' &&
|
||||
config.difficulty > 0 &&
|
||||
typeof config.turnTimer === 'number' &&
|
||||
config.turnTimer > 0
|
||||
)
|
||||
|
||||
case 'memory-quiz':
|
||||
return (
|
||||
[2, 5, 8, 12, 15].includes(config.selectedCount) &&
|
||||
typeof config.displayTime === 'number' &&
|
||||
config.displayTime > 0 &&
|
||||
['beginner', 'easy', 'medium', 'hard', 'expert'].includes(config.selectedDifficulty) &&
|
||||
['cooperative', 'competitive'].includes(config.playMode)
|
||||
)
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use in settings API:
|
||||
```typescript
|
||||
// src/app/api/arcade/rooms/[roomId]/settings/route.ts
|
||||
|
||||
if (body.gameConfig !== undefined) {
|
||||
if (!validateGameConfig(room.gameName, body.gameConfig[room.gameName])) {
|
||||
return NextResponse.json({ error: 'Invalid game config' }, { status: 400 })
|
||||
}
|
||||
updateData.gameConfig = body.gameConfig
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. **HIGH:** Create shared config types (`game-configs.ts`) - Prevents type mismatches
|
||||
2. **HIGH:** Create helper functions (`game-config-helpers.ts`) - Reduces duplication
|
||||
3. **MEDIUM:** Update validators to use shared types - Enforces consistency
|
||||
4. **MEDIUM:** Add exhaustiveness checking - Catches missing fields at compile time
|
||||
5. **LOW:** Add runtime validation - Prevents invalid data from being saved
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
1. Create new files without modifying existing code
|
||||
2. Add shared types and helpers
|
||||
3. Migrate validators one at a time
|
||||
4. Migrate providers one at a time
|
||||
5. Migrate socket server
|
||||
6. Remove old inline config reading logic
|
||||
7. Add runtime validation last (optional)
|
||||
|
||||
## Benefits Summary
|
||||
|
||||
- **Type Safety:** TypeScript enforces consistency across all systems
|
||||
- **DRY:** Config reading logic not duplicated
|
||||
- **Maintainability:** Adding a setting requires changes in fewer places
|
||||
- **Correctness:** Impossible to forget a setting or use wrong type
|
||||
- **Debugging:** Centralized config logic easier to trace
|
||||
- **Testing:** Can test config helpers in isolation
|
||||
Reference in New Issue
Block a user