refactor(arcade): migrate game settings to normalized database schema
### Schema Changes - Create `room_game_configs` table with one row per game per room - Migrate existing gameConfig data from arcade_rooms.game_config JSON column - Add unique index on (roomId, gameName) for efficient queries ### Benefits - ✅ Type-safe config access with shared types - ✅ Smaller rows (only configs for used games) - ✅ Easier updates (single row vs entire JSON blob) - ✅ Better concurrency (no lock contention between games) - ✅ Foundation for per-game audit trail ### Core Changes 1. **Shared Config Types** (`game-configs.ts`) - `MatchingGameConfig`, `MemoryQuizGameConfig` interfaces - Default configs for each game - Single source of truth for all settings 2. **Helper Functions** (`game-config-helpers.ts`) - `getGameConfig<T>()` - type-safe config retrieval with defaults - `setGameConfig()` - upsert game config - `getAllGameConfigs()` - aggregate all game configs for a room - `validateGameConfig()` - runtime validation 3. **API Routes** - `/api/arcade/rooms/current`: Aggregates configs from new table - `/api/arcade/rooms/[roomId]/settings`: Writes to new table 4. **Socket Server** (`socket-server.ts`) - Uses `getGameConfig()` helper for session creation - Eliminates manual config extraction and defaults 5. **Validators** - `MemoryQuizGameValidator.getInitialState(config: MemoryQuizGameConfig)` - `MatchingGameValidator.getInitialState(config: MatchingGameConfig)` - Type signatures enforce consistency ### Migration Path - Existing data migrated automatically (SQL in migration file) - Old `gameConfig` column preserved temporarily - Client-side providers unchanged (read from aggregated response) Next steps: - Test settings persistence thoroughly - Drop old `gameConfig` column after validation - Update documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
39
apps/web/drizzle/0011_add_room_game_configs.sql
Normal file
39
apps/web/drizzle/0011_add_room_game_configs.sql
Normal file
@@ -0,0 +1,39 @@
|
||||
-- Create room_game_configs table for per-game settings storage
|
||||
-- This replaces the monolithic gameConfig JSON column with a normalized table
|
||||
CREATE TABLE `room_game_configs` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`room_id` text NOT NULL,
|
||||
`game_name` text NOT NULL,
|
||||
`config` text NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
FOREIGN KEY (`room_id`) REFERENCES `arcade_rooms`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `room_game_idx` ON `room_game_configs` (`room_id`, `game_name`);
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Migrate existing 'matching' configs from arcade_rooms.game_config
|
||||
INSERT INTO `room_game_configs` (`id`, `room_id`, `game_name`, `config`, `created_at`, `updated_at`)
|
||||
SELECT
|
||||
lower(hex(randomblob(16))),
|
||||
`id` as room_id,
|
||||
'matching' as game_name,
|
||||
json_extract(`game_config`, '$.matching') as config,
|
||||
`created_at`,
|
||||
`last_activity` as updated_at
|
||||
FROM `arcade_rooms`
|
||||
WHERE json_extract(`game_config`, '$.matching') IS NOT NULL;
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Migrate existing 'memory-quiz' configs from arcade_rooms.game_config
|
||||
INSERT INTO `room_game_configs` (`id`, `room_id`, `game_name`, `config`, `created_at`, `updated_at`)
|
||||
SELECT
|
||||
lower(hex(randomblob(16))),
|
||||
`id` as room_id,
|
||||
'memory-quiz' as game_name,
|
||||
json_extract(`game_config`, '$."memory-quiz"') as config,
|
||||
`created_at`,
|
||||
`last_activity` as updated_at
|
||||
FROM `arcade_rooms`
|
||||
WHERE json_extract(`game_config`, '$."memory-quiz"') IS NOT NULL;
|
||||
@@ -7,6 +7,8 @@ import { recordRoomMemberHistory } from '@/lib/arcade/room-member-history'
|
||||
import { getRoomMembers } from '@/lib/arcade/room-membership'
|
||||
import { getSocketIO } from '@/lib/socket-io'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
import { getAllGameConfigs, setGameConfig } from '@/lib/arcade/game-config-helpers'
|
||||
import type { GameName } from '@/lib/arcade/validation'
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ roomId: string }>
|
||||
@@ -122,9 +124,16 @@ export async function PATCH(req: NextRequest, context: RouteContext) {
|
||||
updateData.gameName = body.gameName
|
||||
}
|
||||
|
||||
// Update game config if provided
|
||||
if (body.gameConfig !== undefined) {
|
||||
updateData.gameConfig = body.gameConfig
|
||||
// Handle game config updates - write to new room_game_configs table
|
||||
if (body.gameConfig !== undefined && body.gameConfig !== null) {
|
||||
// body.gameConfig is expected to be nested by game name: { matching: {...}, memory-quiz: {...} }
|
||||
// Extract each game's config and write to the new table
|
||||
for (const [gameName, config] of Object.entries(body.gameConfig)) {
|
||||
if (config && typeof config === 'object') {
|
||||
await setGameConfig(roomId, gameName as GameName, config)
|
||||
console.log(`[Settings API] Wrote ${gameName} config to room_game_configs table`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
@@ -146,12 +155,15 @@ export async function PATCH(req: NextRequest, context: RouteContext) {
|
||||
.where(eq(schema.arcadeRooms.id, roomId))
|
||||
.returning()
|
||||
|
||||
// Get aggregated game configs from new table
|
||||
const gameConfig = await getAllGameConfigs(roomId)
|
||||
|
||||
console.log(
|
||||
'[Settings API] Room state in database AFTER update:',
|
||||
JSON.stringify(
|
||||
{
|
||||
gameName: updatedRoom.gameName,
|
||||
gameConfig: updatedRoom.gameConfig,
|
||||
gameConfig,
|
||||
},
|
||||
null,
|
||||
2
|
||||
@@ -171,11 +183,7 @@ export async function PATCH(req: NextRequest, context: RouteContext) {
|
||||
} = {
|
||||
roomId,
|
||||
gameName: body.gameName,
|
||||
}
|
||||
|
||||
// Only include gameConfig if it was explicitly provided
|
||||
if (body.gameConfig !== undefined) {
|
||||
broadcastData.gameConfig = body.gameConfig
|
||||
gameConfig, // Include aggregated configs from new table
|
||||
}
|
||||
|
||||
io.to(`room:${roomId}`).emit('room-game-changed', broadcastData)
|
||||
@@ -251,7 +259,15 @@ export async function PATCH(req: NextRequest, context: RouteContext) {
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ room: updatedRoom }, { status: 200 })
|
||||
return NextResponse.json(
|
||||
{
|
||||
room: {
|
||||
...updatedRoom,
|
||||
gameConfig, // Include aggregated configs from new table
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error: any) {
|
||||
console.error('Failed to update room settings:', error)
|
||||
return NextResponse.json({ error: 'Failed to update room settings' }, { status: 500 })
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getRoomById } from '@/lib/arcade/room-manager'
|
||||
import { getRoomMembers } from '@/lib/arcade/room-membership'
|
||||
import { getRoomActivePlayers } from '@/lib/arcade/player-manager'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
import { getAllGameConfigs } from '@/lib/arcade/game-config-helpers'
|
||||
|
||||
/**
|
||||
* GET /api/arcade/rooms/current
|
||||
@@ -28,13 +29,16 @@ export async function GET() {
|
||||
return NextResponse.json({ error: 'Room not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Get game configs from new room_game_configs table
|
||||
const gameConfig = await getAllGameConfigs(roomId)
|
||||
|
||||
console.log(
|
||||
'[Current Room API] Room data READ from database:',
|
||||
JSON.stringify(
|
||||
{
|
||||
roomId,
|
||||
gameName: room.gameName,
|
||||
gameConfig: room.gameConfig,
|
||||
gameConfig,
|
||||
},
|
||||
null,
|
||||
2
|
||||
@@ -54,7 +58,10 @@ export async function GET() {
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
room,
|
||||
room: {
|
||||
...room,
|
||||
gameConfig, // Override with configs from new table
|
||||
},
|
||||
members,
|
||||
memberPlayers: memberPlayersObj,
|
||||
})
|
||||
|
||||
@@ -15,5 +15,6 @@ export * from './room-invitations'
|
||||
export * from './room-reports'
|
||||
export * from './room-bans'
|
||||
export * from './room-join-requests'
|
||||
export * from './room-game-configs'
|
||||
export * from './user-stats'
|
||||
export * from './users'
|
||||
|
||||
48
apps/web/src/db/schema/room-game-configs.ts
Normal file
48
apps/web/src/db/schema/room-game-configs.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'
|
||||
import { arcadeRooms } from './arcade-rooms'
|
||||
|
||||
/**
|
||||
* Game-specific configuration settings for arcade rooms
|
||||
* Each row represents one game's settings for one room
|
||||
*/
|
||||
export const roomGameConfigs = sqliteTable(
|
||||
'room_game_configs',
|
||||
{
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => createId()),
|
||||
|
||||
// Room reference
|
||||
roomId: text('room_id')
|
||||
.notNull()
|
||||
.references(() => arcadeRooms.id, { onDelete: 'cascade' }),
|
||||
|
||||
// Game identifier
|
||||
gameName: text('game_name', {
|
||||
enum: ['matching', 'memory-quiz', 'complement-race'],
|
||||
}).notNull(),
|
||||
|
||||
// Game-specific configuration JSON
|
||||
// Structure depends on gameName:
|
||||
// - matching: { gameType, difficulty, turnTimer }
|
||||
// - memory-quiz: { selectedCount, displayTime, selectedDifficulty, playMode }
|
||||
// - complement-race: TBD
|
||||
config: text('config', { mode: 'json' }).notNull(),
|
||||
|
||||
// Timestamps
|
||||
createdAt: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
},
|
||||
(table) => ({
|
||||
// Ensure only one config per game per room
|
||||
uniqueRoomGame: uniqueIndex('room_game_idx').on(table.roomId, table.gameName),
|
||||
})
|
||||
)
|
||||
|
||||
export type RoomGameConfig = typeof roomGameConfigs.$inferSelect
|
||||
export type NewRoomGameConfig = typeof roomGameConfigs.$inferInsert
|
||||
197
apps/web/src/lib/arcade/game-config-helpers.ts
Normal file
197
apps/web/src/lib/arcade/game-config-helpers.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Game configuration helpers
|
||||
*
|
||||
* Centralized functions for reading and writing game configs from the database.
|
||||
* Uses the room_game_configs table (one row per game per room).
|
||||
*/
|
||||
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { db, schema } from '@/db'
|
||||
import type { GameName } from './validation'
|
||||
import type { GameConfigByName } from './game-configs'
|
||||
import {
|
||||
DEFAULT_MATCHING_CONFIG,
|
||||
DEFAULT_MEMORY_QUIZ_CONFIG,
|
||||
DEFAULT_COMPLEMENT_RACE_CONFIG,
|
||||
} from './game-configs'
|
||||
|
||||
/**
|
||||
* Get default config for a game
|
||||
*/
|
||||
function getDefaultGameConfig(gameName: GameName): GameConfigByName[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
|
||||
default:
|
||||
throw new Error(`Unknown game: ${gameName}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get game-specific config from database with defaults
|
||||
* Type-safe: returns the correct config type based on gameName
|
||||
*/
|
||||
export async function getGameConfig<T extends GameName>(
|
||||
roomId: string,
|
||||
gameName: T
|
||||
): Promise<GameConfigByName[T]> {
|
||||
// Query the room_game_configs table for this specific room+game
|
||||
const configRow = await db.query.roomGameConfigs.findFirst({
|
||||
where: and(
|
||||
eq(schema.roomGameConfigs.roomId, roomId),
|
||||
eq(schema.roomGameConfigs.gameName, gameName)
|
||||
),
|
||||
})
|
||||
|
||||
// If no config exists, return defaults
|
||||
if (!configRow) {
|
||||
return getDefaultGameConfig(gameName) as GameConfigByName[T]
|
||||
}
|
||||
|
||||
// Merge saved config with defaults to handle missing fields
|
||||
const defaults = getDefaultGameConfig(gameName)
|
||||
return { ...defaults, ...(configRow.config as object) } as GameConfigByName[T]
|
||||
}
|
||||
|
||||
/**
|
||||
* Set (upsert) a game's config in the database
|
||||
* Creates a new row if it doesn't exist, updates if it does
|
||||
*/
|
||||
export async function setGameConfig<T extends GameName>(
|
||||
roomId: string,
|
||||
gameName: T,
|
||||
config: Partial<GameConfigByName[T]>
|
||||
): Promise<void> {
|
||||
const now = new Date()
|
||||
|
||||
// Check if config already exists
|
||||
const existing = await db.query.roomGameConfigs.findFirst({
|
||||
where: and(
|
||||
eq(schema.roomGameConfigs.roomId, roomId),
|
||||
eq(schema.roomGameConfigs.gameName, gameName)
|
||||
),
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
// Update existing config (merge with existing values)
|
||||
const mergedConfig = { ...existing.config, ...config }
|
||||
await db
|
||||
.update(schema.roomGameConfigs)
|
||||
.set({
|
||||
config: mergedConfig as any,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(schema.roomGameConfigs.id, existing.id))
|
||||
} else {
|
||||
// Insert new config (merge with defaults)
|
||||
const defaults = getDefaultGameConfig(gameName)
|
||||
const mergedConfig = { ...defaults, ...config }
|
||||
|
||||
await db.insert(schema.roomGameConfigs).values({
|
||||
id: createId(),
|
||||
roomId,
|
||||
gameName,
|
||||
config: mergedConfig as any,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`[GameConfig] Updated ${gameName} config for room ${roomId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific field in a game's config
|
||||
* Convenience wrapper around setGameConfig
|
||||
*/
|
||||
export async function updateGameConfigField<
|
||||
T extends GameName,
|
||||
K extends keyof GameConfigByName[T],
|
||||
>(roomId: string, gameName: T, field: K, value: GameConfigByName[T][K]): Promise<void> {
|
||||
await setGameConfig(roomId, gameName, { [field]: value } as Partial<GameConfigByName[T]>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a game's config from the database
|
||||
* Useful when clearing game selection or cleaning up
|
||||
*/
|
||||
export async function deleteGameConfig(roomId: string, gameName: GameName): Promise<void> {
|
||||
await db
|
||||
.delete(schema.roomGameConfigs)
|
||||
.where(
|
||||
and(eq(schema.roomGameConfigs.roomId, roomId), eq(schema.roomGameConfigs.gameName, gameName))
|
||||
)
|
||||
|
||||
console.log(`[GameConfig] Deleted ${gameName} config for room ${roomId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all game configs for a room (all games)
|
||||
* Returns a map of gameName -> config
|
||||
*/
|
||||
export async function getAllGameConfigs(
|
||||
roomId: string
|
||||
): Promise<Partial<Record<GameName, unknown>>> {
|
||||
const configs = await db.query.roomGameConfigs.findMany({
|
||||
where: eq(schema.roomGameConfigs.roomId, roomId),
|
||||
})
|
||||
|
||||
const result: Partial<Record<GameName, unknown>> = {}
|
||||
for (const config of configs) {
|
||||
result[config.gameName as GameName] = config.config
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all game configs for a room
|
||||
* Called when deleting a room (cascade should handle this, but useful for explicit cleanup)
|
||||
*/
|
||||
export async function deleteAllGameConfigs(roomId: string): Promise<void> {
|
||||
await db.delete(schema.roomGameConfigs).where(eq(schema.roomGameConfigs.roomId, roomId))
|
||||
console.log(`[GameConfig] Deleted all configs for room ${roomId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a game config at runtime
|
||||
* Returns true if the config is valid for the given game
|
||||
*/
|
||||
export function validateGameConfig(gameName: GameName, config: any): boolean {
|
||||
switch (gameName) {
|
||||
case 'matching':
|
||||
return (
|
||||
typeof config === 'object' &&
|
||||
config !== null &&
|
||||
['abacus-numeral', 'complement-pairs'].includes(config.gameType) &&
|
||||
typeof config.difficulty === 'number' &&
|
||||
[6, 8, 12, 15].includes(config.difficulty) &&
|
||||
typeof config.turnTimer === 'number' &&
|
||||
config.turnTimer >= 5 &&
|
||||
config.turnTimer <= 300
|
||||
)
|
||||
|
||||
case 'memory-quiz':
|
||||
return (
|
||||
typeof config === 'object' &&
|
||||
config !== null &&
|
||||
[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)
|
||||
)
|
||||
|
||||
case 'complement-race':
|
||||
// TODO: Add validation when complement-race settings are defined
|
||||
return typeof config === 'object' && config !== null
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
79
apps/web/src/lib/arcade/game-configs.ts
Normal file
79
apps/web/src/lib/arcade/game-configs.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Shared game configuration types
|
||||
*
|
||||
* This is the single source of truth for all game settings.
|
||||
* These types are used across:
|
||||
* - Database storage (room_game_configs table)
|
||||
* - Validators (getInitialState method signatures)
|
||||
* - Client providers (settings UI and state management)
|
||||
* - Helper functions (reading/writing configs)
|
||||
*/
|
||||
|
||||
import type { DifficultyLevel } from '@/app/arcade/memory-quiz/types'
|
||||
|
||||
/**
|
||||
* Configuration for matching (memory pairs) game
|
||||
*/
|
||||
export interface MatchingGameConfig {
|
||||
gameType: 'abacus-numeral' | 'complement-pairs'
|
||||
difficulty: number
|
||||
turnTimer: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for memory-quiz (soroban lightning) game
|
||||
*/
|
||||
export interface MemoryQuizGameConfig {
|
||||
selectedCount: 2 | 5 | 8 | 12 | 15
|
||||
displayTime: number
|
||||
selectedDifficulty: DifficultyLevel
|
||||
playMode: 'cooperative' | 'competitive'
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for complement-race game
|
||||
* TODO: Define when implementing complement-race settings
|
||||
*/
|
||||
export interface ComplementRaceGameConfig {
|
||||
// Future settings will go here
|
||||
placeholder?: never
|
||||
}
|
||||
|
||||
/**
|
||||
* Union type of all game configs for type-safe access
|
||||
*/
|
||||
export type GameConfigByName = {
|
||||
matching: MatchingGameConfig
|
||||
'memory-quiz': MemoryQuizGameConfig
|
||||
'complement-race': ComplementRaceGameConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Room's game configuration object (nested by game name)
|
||||
* This matches the structure stored in room_game_configs table
|
||||
*/
|
||||
export interface RoomGameConfig {
|
||||
matching?: MatchingGameConfig
|
||||
'memory-quiz'?: MemoryQuizGameConfig
|
||||
'complement-race'?: ComplementRaceGameConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Default configurations for each game
|
||||
*/
|
||||
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',
|
||||
}
|
||||
|
||||
export const DEFAULT_COMPLEMENT_RACE_CONFIG: ComplementRaceGameConfig = {
|
||||
// Future defaults will go here
|
||||
}
|
||||
@@ -3,15 +3,10 @@
|
||||
* Validates all game moves and state transitions
|
||||
*/
|
||||
|
||||
import type {
|
||||
Difficulty,
|
||||
GameCard,
|
||||
GameType,
|
||||
MemoryPairsState,
|
||||
Player,
|
||||
} from '@/app/games/matching/context/types'
|
||||
import type { GameCard, MemoryPairsState, Player } from '@/app/games/matching/context/types'
|
||||
import { generateGameCards } from '@/app/games/matching/utils/cardGeneration'
|
||||
import { canFlipCard, validateMatch } from '@/app/games/matching/utils/matchValidation'
|
||||
import type { MatchingGameConfig } from '@/lib/arcade/game-configs'
|
||||
import type { GameValidator, MatchingGameMove, ValidationResult } from './types'
|
||||
|
||||
export class MatchingGameValidator implements GameValidator<MemoryPairsState, MatchingGameMove> {
|
||||
@@ -536,11 +531,7 @@ export class MatchingGameValidator implements GameValidator<MemoryPairsState, Ma
|
||||
return state.gamePhase === 'results' || state.matchedPairs === state.totalPairs
|
||||
}
|
||||
|
||||
getInitialState(config: {
|
||||
difficulty: Difficulty
|
||||
gameType: GameType
|
||||
turnTimer: number
|
||||
}): MemoryPairsState {
|
||||
getInitialState(config: MatchingGameConfig): MemoryPairsState {
|
||||
return {
|
||||
cards: [],
|
||||
gameCards: [],
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* Validates all game moves and state transitions
|
||||
*/
|
||||
|
||||
import type { DifficultyLevel, SorobanQuizState } from '@/app/arcade/memory-quiz/types'
|
||||
import type { SorobanQuizState } from '@/app/arcade/memory-quiz/types'
|
||||
import type { MemoryQuizGameConfig } from '@/lib/arcade/game-configs'
|
||||
import type {
|
||||
GameValidator,
|
||||
MemoryQuizGameMove,
|
||||
@@ -395,12 +396,7 @@ export class MemoryQuizGameValidator
|
||||
return state.gamePhase === 'results'
|
||||
}
|
||||
|
||||
getInitialState(config: {
|
||||
selectedCount: number
|
||||
displayTime: number
|
||||
selectedDifficulty: DifficultyLevel
|
||||
playMode?: 'cooperative' | 'competitive'
|
||||
}): SorobanQuizState {
|
||||
getInitialState(config: MemoryQuizGameConfig): SorobanQuizState {
|
||||
return {
|
||||
cards: [],
|
||||
quizCards: [],
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getRoomMembers, getUserRooms, setMemberOnline } from './lib/arcade/room
|
||||
import { getRoomActivePlayers, getRoomPlayerIds } from './lib/arcade/player-manager'
|
||||
import type { GameMove, GameName } from './lib/arcade/validation'
|
||||
import { getValidator } from './lib/arcade/validation'
|
||||
import { getGameConfig } from './lib/arcade/game-config-helpers'
|
||||
|
||||
// Use globalThis to store socket.io instance to avoid module isolation issues
|
||||
// This ensures the same instance is accessible across dynamic imports
|
||||
@@ -81,29 +82,9 @@ export function initializeSocketServer(httpServer: HTTPServer) {
|
||||
const validator = getValidator(room.gameName as GameName)
|
||||
console.log('[join-arcade-session] Got validator for:', room.gameName)
|
||||
|
||||
// Different games have different initial configs
|
||||
let initialState: any
|
||||
if (room.gameName === 'matching') {
|
||||
// Access nested gameConfig: { matching: { gameType, difficulty, turnTimer } }
|
||||
const matchingConfig = (room.gameConfig as any)?.matching || {}
|
||||
initialState = validator.getInitialState({
|
||||
difficulty: matchingConfig.difficulty || 6,
|
||||
gameType: matchingConfig.gameType || 'abacus-numeral',
|
||||
turnTimer: matchingConfig.turnTimer || 30,
|
||||
})
|
||||
} else if (room.gameName === 'memory-quiz') {
|
||||
// Access nested gameConfig: { 'memory-quiz': { selectedCount, displayTime, selectedDifficulty, playMode } }
|
||||
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',
|
||||
})
|
||||
} else {
|
||||
// Fallback for other games
|
||||
initialState = validator.getInitialState(room.gameConfig || {})
|
||||
}
|
||||
// Get game-specific config from database (type-safe)
|
||||
const gameConfig = await getGameConfig(roomId, room.gameName as GameName)
|
||||
const initialState = validator.getInitialState(gameConfig)
|
||||
|
||||
session = await createArcadeSession({
|
||||
userId,
|
||||
|
||||
Reference in New Issue
Block a user