diff --git a/apps/web/.gitignore b/apps/web/.gitignore index aab5216b..e7e90209 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -34,6 +34,11 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts +# compiled server-side code (from tsconfig.server.json) +/dist +src/**/*.js +src/**/*.js.map + # vitest /.vitest diff --git a/apps/web/src/app/games/matching/context/types.js b/apps/web/src/app/games/matching/context/types.js deleted file mode 100644 index 2fb77ec4..00000000 --- a/apps/web/src/app/games/matching/context/types.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; -// TypeScript interfaces for Memory Pairs Challenge game -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/apps/web/src/app/games/matching/utils/cardGeneration.js b/apps/web/src/app/games/matching/utils/cardGeneration.js deleted file mode 100644 index 2453060e..00000000 --- a/apps/web/src/app/games/matching/utils/cardGeneration.js +++ /dev/null @@ -1,164 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.generateAbacusNumeralCards = generateAbacusNumeralCards; -exports.generateComplementCards = generateComplementCards; -exports.generateGameCards = generateGameCards; -exports.getGridConfiguration = getGridConfiguration; -exports.generateCardId = generateCardId; -// Utility function to generate unique random numbers -function generateUniqueNumbers(count, options) { - const numbers = new Set(); - const { min, max } = options; - while (numbers.size < count) { - const randomNum = Math.floor(Math.random() * (max - min + 1)) + min; - numbers.add(randomNum); - } - return Array.from(numbers); -} -// Utility function to shuffle an array -function shuffleArray(array) { - const shuffled = [...array]; - for (let i = shuffled.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; - } - return shuffled; -} -// Generate cards for abacus-numeral game mode -function generateAbacusNumeralCards(pairs) { - // Generate unique numbers based on difficulty - // For easier games, use smaller numbers; for harder games, use larger ranges - const numberRanges = { - 6: { min: 1, max: 50 }, // 6 pairs: 1-50 - 8: { min: 1, max: 100 }, // 8 pairs: 1-100 - 12: { min: 1, max: 200 }, // 12 pairs: 1-200 - 15: { min: 1, max: 300 }, // 15 pairs: 1-300 - }; - const range = numberRanges[pairs]; - const numbers = generateUniqueNumbers(pairs, range); - const cards = []; - numbers.forEach((number) => { - // Abacus representation card - cards.push({ - id: `abacus_${number}`, - type: 'abacus', - number, - matched: false, - }); - // Numerical representation card - cards.push({ - id: `number_${number}`, - type: 'number', - number, - matched: false, - }); - }); - return shuffleArray(cards); -} -// Generate cards for complement pairs game mode -function generateComplementCards(pairs) { - // Define complement pairs for friends of 5 and friends of 10 - const complementPairs = [ - // Friends of 5 - { pair: [0, 5], targetSum: 5 }, - { pair: [1, 4], targetSum: 5 }, - { pair: [2, 3], targetSum: 5 }, - // Friends of 10 - { pair: [0, 10], targetSum: 10 }, - { pair: [1, 9], targetSum: 10 }, - { pair: [2, 8], targetSum: 10 }, - { pair: [3, 7], targetSum: 10 }, - { pair: [4, 6], targetSum: 10 }, - { pair: [5, 5], targetSum: 10 }, - // Additional pairs for higher difficulties - { pair: [6, 4], targetSum: 10 }, - { pair: [7, 3], targetSum: 10 }, - { pair: [8, 2], targetSum: 10 }, - { pair: [9, 1], targetSum: 10 }, - { pair: [10, 0], targetSum: 10 }, - // More challenging pairs (can be used for expert mode) - { pair: [11, 9], targetSum: 20 }, - { pair: [12, 8], targetSum: 20 }, - ]; - // Select the required number of complement pairs - const selectedPairs = complementPairs.slice(0, pairs); - const cards = []; - selectedPairs.forEach(({ pair: [num1, num2], targetSum }, index) => { - // First number in the pair - cards.push({ - id: `comp1_${index}_${num1}`, - type: 'complement', - number: num1, - complement: num2, - targetSum, - matched: false, - }); - // Second number in the pair - cards.push({ - id: `comp2_${index}_${num2}`, - type: 'complement', - number: num2, - complement: num1, - targetSum, - matched: false, - }); - }); - return shuffleArray(cards); -} -// Main card generation function -function generateGameCards(gameType, difficulty) { - switch (gameType) { - case 'abacus-numeral': - return generateAbacusNumeralCards(difficulty); - case 'complement-pairs': - return generateComplementCards(difficulty); - default: - throw new Error(`Unknown game type: ${gameType}`); - } -} -// Utility function to get responsive grid configuration based on difficulty and screen size -function getGridConfiguration(difficulty) { - const configs = { - 6: { - totalCards: 12, - mobileColumns: 3, // 3x4 grid in portrait - tabletColumns: 4, // 4x3 grid on tablet - desktopColumns: 4, // 4x3 grid on desktop - landscapeColumns: 6, // 6x2 grid in landscape - cardSize: { width: '140px', height: '180px' }, - gridTemplate: 'repeat(3, 1fr)', - }, - 8: { - totalCards: 16, - mobileColumns: 3, // 3x6 grid in portrait (some spillover) - tabletColumns: 4, // 4x4 grid on tablet - desktopColumns: 4, // 4x4 grid on desktop - landscapeColumns: 6, // 6x3 grid in landscape (some spillover) - cardSize: { width: '120px', height: '160px' }, - gridTemplate: 'repeat(3, 1fr)', - }, - 12: { - totalCards: 24, - mobileColumns: 3, // 3x8 grid in portrait - tabletColumns: 4, // 4x6 grid on tablet - desktopColumns: 6, // 6x4 grid on desktop - landscapeColumns: 6, // 6x4 grid in landscape (changed from 8x3) - cardSize: { width: '100px', height: '140px' }, - gridTemplate: 'repeat(3, 1fr)', - }, - 15: { - totalCards: 30, - mobileColumns: 3, // 3x10 grid in portrait - tabletColumns: 5, // 5x6 grid on tablet - desktopColumns: 6, // 6x5 grid on desktop - landscapeColumns: 10, // 10x3 grid in landscape - cardSize: { width: '90px', height: '120px' }, - gridTemplate: 'repeat(3, 1fr)', - }, - }; - return configs[difficulty]; -} -// Generate a unique ID for cards -function generateCardId(type, identifier) { - return `${type}_${identifier}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; -} diff --git a/apps/web/src/app/games/matching/utils/matchValidation.js b/apps/web/src/app/games/matching/utils/matchValidation.js deleted file mode 100644 index 2b8cd9c5..00000000 --- a/apps/web/src/app/games/matching/utils/matchValidation.js +++ /dev/null @@ -1,188 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.validateAbacusNumeralMatch = validateAbacusNumeralMatch; -exports.validateComplementMatch = validateComplementMatch; -exports.validateMatch = validateMatch; -exports.canFlipCard = canFlipCard; -exports.getMatchHint = getMatchHint; -exports.calculateMatchScore = calculateMatchScore; -exports.analyzeGamePerformance = analyzeGamePerformance; -// Validate abacus-numeral match (abacus card matches with number card of same value) -function validateAbacusNumeralMatch(card1, card2) { - // Both cards must have the same number - if (card1.number !== card2.number) { - return { - isValid: false, - reason: 'Numbers do not match', - type: 'invalid', - }; - } - // Cards must be different types (one abacus, one number) - if (card1.type === card2.type) { - return { - isValid: false, - reason: 'Both cards are the same type', - type: 'invalid', - }; - } - // One must be abacus, one must be number - const hasAbacus = card1.type === 'abacus' || card2.type === 'abacus'; - const hasNumber = card1.type === 'number' || card2.type === 'number'; - if (!hasAbacus || !hasNumber) { - return { - isValid: false, - reason: 'Must match abacus with number representation', - type: 'invalid', - }; - } - // Neither should be complement type for this game mode - if (card1.type === 'complement' || card2.type === 'complement') { - return { - isValid: false, - reason: 'Complement cards not valid in abacus-numeral mode', - type: 'invalid', - }; - } - return { - isValid: true, - type: 'abacus-numeral', - }; -} -// Validate complement match (two numbers that add up to target sum) -function validateComplementMatch(card1, card2) { - // Both cards must be complement type - if (card1.type !== 'complement' || card2.type !== 'complement') { - return { - isValid: false, - reason: 'Both cards must be complement type', - type: 'invalid', - }; - } - // Both cards must have the same target sum - if (card1.targetSum !== card2.targetSum) { - return { - isValid: false, - reason: 'Cards have different target sums', - type: 'invalid', - }; - } - // Check if the numbers are actually complements - if (!card1.complement || !card2.complement) { - return { - isValid: false, - reason: 'Complement information missing', - type: 'invalid', - }; - } - // Verify the complement relationship - if (card1.number !== card2.complement || card2.number !== card1.complement) { - return { - isValid: false, - reason: 'Numbers are not complements of each other', - type: 'invalid', - }; - } - // Verify the sum equals the target - const sum = card1.number + card2.number; - if (sum !== card1.targetSum) { - return { - isValid: false, - reason: `Sum ${sum} does not equal target ${card1.targetSum}`, - type: 'invalid', - }; - } - return { - isValid: true, - type: 'complement', - }; -} -// Main validation function that determines which validation to use -function validateMatch(card1, card2) { - // Cannot match the same card with itself - if (card1.id === card2.id) { - return { - isValid: false, - reason: 'Cannot match card with itself', - type: 'invalid', - }; - } - // Cannot match already matched cards - if (card1.matched || card2.matched) { - return { - isValid: false, - reason: 'Cannot match already matched cards', - type: 'invalid', - }; - } - // Determine which type of match to validate based on card types - const hasComplement = card1.type === 'complement' || card2.type === 'complement'; - if (hasComplement) { - // If either card is complement type, use complement validation - return validateComplementMatch(card1, card2); - } - else { - // Otherwise, use abacus-numeral validation - return validateAbacusNumeralMatch(card1, card2); - } -} -// Helper function to check if a card can be flipped -function canFlipCard(card, flippedCards, isProcessingMove) { - // Cannot flip if processing a move - if (isProcessingMove) - return false; - // Cannot flip already matched cards - if (card.matched) - return false; - // Cannot flip if already flipped - if (flippedCards.some((c) => c.id === card.id)) - return false; - // Cannot flip if two cards are already flipped - if (flippedCards.length >= 2) - return false; - return true; -} -// Get hint for what kind of match the player should look for -function getMatchHint(card) { - switch (card.type) { - case 'abacus': - return `Find the number ${card.number}`; - case 'number': - return `Find the abacus showing ${card.number}`; - case 'complement': - if (card.complement !== undefined && card.targetSum !== undefined) { - return `Find ${card.complement} to make ${card.targetSum}`; - } - return 'Find the matching complement'; - default: - return 'Find the matching card'; - } -} -// Calculate match score based on difficulty and time -function calculateMatchScore(difficulty, timeForMatch, isComplementMatch) { - const baseScore = isComplementMatch ? 15 : 10; // Complement matches worth more - const difficultyMultiplier = difficulty / 6; // Scale with difficulty - const timeBonus = Math.max(0, (10000 - timeForMatch) / 1000); // Bonus for speed - return Math.round(baseScore * difficultyMultiplier + timeBonus); -} -// Analyze game performance -function analyzeGamePerformance(totalMoves, matchedPairs, totalPairs, gameTime) { - const accuracy = totalMoves > 0 ? (matchedPairs / totalMoves) * 100 : 0; - const efficiency = totalPairs > 0 ? (matchedPairs / (totalPairs * 2)) * 100 : 0; // Ideal is 100% (each pair found in 2 moves) - const averageTimePerMove = totalMoves > 0 ? gameTime / totalMoves : 0; - // Calculate grade based on accuracy and efficiency - let grade = 'F'; - if (accuracy >= 90 && efficiency >= 80) - grade = 'A'; - else if (accuracy >= 80 && efficiency >= 70) - grade = 'B'; - else if (accuracy >= 70 && efficiency >= 60) - grade = 'C'; - else if (accuracy >= 60 && efficiency >= 50) - grade = 'D'; - return { - accuracy, - efficiency, - averageTimePerMove, - grade, - }; -} diff --git a/apps/web/src/db/index.js b/apps/web/src/db/index.js deleted file mode 100644 index 23d047a3..00000000 --- a/apps/web/src/db/index.js +++ /dev/null @@ -1,80 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.schema = exports.db = void 0; -const better_sqlite3_1 = __importDefault(require("better-sqlite3")); -const better_sqlite3_2 = require("drizzle-orm/better-sqlite3"); -const schema = __importStar(require("./schema")); -exports.schema = schema; -/** - * Database connection and client - * - * Creates a singleton SQLite connection with Drizzle ORM. - * Enables foreign key constraints (required for cascading deletes). - * - * IMPORTANT: The database connection is lazy-loaded to avoid accessing - * the database at module import time, which would cause build failures - * when the database doesn't exist (e.g., in CI/CD environments). - */ -const databaseUrl = process.env.DATABASE_URL || './data/sqlite.db'; -let _sqlite = null; -let _db = null; -/** - * Get the database connection (lazy-loaded singleton) - * Only creates the connection when first accessed at runtime - */ -function getDb() { - if (!_db) { - _sqlite = new better_sqlite3_1.default(databaseUrl); - // Enable foreign keys (SQLite requires explicit enable) - _sqlite.pragma('foreign_keys = ON'); - // Enable WAL mode for better concurrency - _sqlite.pragma('journal_mode = WAL'); - _db = (0, better_sqlite3_2.drizzle)(_sqlite, { schema }); - } - return _db; -} -/** - * Database client instance - * Uses a Proxy to lazy-load the connection on first access - */ -exports.db = new Proxy({}, { - get(_target, prop) { - return getDb()[prop]; - }, -}); diff --git a/apps/web/src/db/migrate.js b/apps/web/src/db/migrate.js deleted file mode 100644 index e5eb1451..00000000 --- a/apps/web/src/db/migrate.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const migrator_1 = require("drizzle-orm/better-sqlite3/migrator"); -const index_1 = require("./index"); -/** - * Migration runner - * - * Runs all pending migrations in the drizzle/ folder. - * Safe to run multiple times (migrations are idempotent). - * - * Usage: pnpm db:migrate - */ -try { - console.log('๐Ÿ”„ Running migrations...'); - (0, migrator_1.migrate)(index_1.db, { migrationsFolder: './drizzle' }); - console.log('โœ… Migrations complete'); - process.exit(0); -} -catch (error) { - console.error('โŒ Migration failed:', error); - process.exit(1); -} diff --git a/apps/web/src/db/schema/abacus-settings.js b/apps/web/src/db/schema/abacus-settings.js deleted file mode 100644 index ee8d308a..00000000 --- a/apps/web/src/db/schema/abacus-settings.js +++ /dev/null @@ -1,53 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.abacusSettings = void 0; -const sqlite_core_1 = require("drizzle-orm/sqlite-core"); -const users_1 = require("./users"); -/** - * Abacus display settings table - UI preferences per user - * - * One-to-one with users table. Stores abacus display configuration. - * Deleted when user is deleted (cascade). - */ -exports.abacusSettings = (0, sqlite_core_1.sqliteTable)('abacus_settings', { - /** Primary key and foreign key to users table */ - userId: (0, sqlite_core_1.text)('user_id') - .primaryKey() - .references(() => users_1.users.id, { onDelete: 'cascade' }), - /** Color scheme for beads */ - colorScheme: (0, sqlite_core_1.text)('color_scheme', { - enum: ['monochrome', 'place-value', 'heaven-earth', 'alternating'], - }) - .notNull() - .default('place-value'), - /** Bead shape */ - beadShape: (0, sqlite_core_1.text)('bead_shape', { - enum: ['diamond', 'circle', 'square'], - }) - .notNull() - .default('diamond'), - /** Color palette */ - colorPalette: (0, sqlite_core_1.text)('color_palette', { - enum: ['default', 'colorblind', 'mnemonic', 'grayscale', 'nature'], - }) - .notNull() - .default('default'), - /** Hide inactive beads */ - hideInactiveBeads: (0, sqlite_core_1.integer)('hide_inactive_beads', { mode: 'boolean' }).notNull().default(false), - /** Color numerals based on place value */ - coloredNumerals: (0, sqlite_core_1.integer)('colored_numerals', { mode: 'boolean' }).notNull().default(false), - /** Scale factor for abacus size */ - scaleFactor: (0, sqlite_core_1.real)('scale_factor').notNull().default(1.0), - /** Show numbers below abacus */ - showNumbers: (0, sqlite_core_1.integer)('show_numbers', { mode: 'boolean' }).notNull().default(true), - /** Enable animations */ - animated: (0, sqlite_core_1.integer)('animated', { mode: 'boolean' }).notNull().default(true), - /** Enable interaction */ - interactive: (0, sqlite_core_1.integer)('interactive', { mode: 'boolean' }).notNull().default(false), - /** Enable gesture controls */ - gestures: (0, sqlite_core_1.integer)('gestures', { mode: 'boolean' }).notNull().default(false), - /** Enable sound effects */ - soundEnabled: (0, sqlite_core_1.integer)('sound_enabled', { mode: 'boolean' }).notNull().default(true), - /** Sound volume (0.0 - 1.0) */ - soundVolume: (0, sqlite_core_1.real)('sound_volume').notNull().default(0.8), -}); diff --git a/apps/web/src/db/schema/arcade-rooms.js b/apps/web/src/db/schema/arcade-rooms.js deleted file mode 100644 index 79860417..00000000 --- a/apps/web/src/db/schema/arcade-rooms.js +++ /dev/null @@ -1,39 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.arcadeRooms = void 0; -const cuid2_1 = require("@paralleldrive/cuid2"); -const sqlite_core_1 = require("drizzle-orm/sqlite-core"); -exports.arcadeRooms = (0, sqlite_core_1.sqliteTable)('arcade_rooms', { - id: (0, sqlite_core_1.text)('id') - .primaryKey() - .$defaultFn(() => (0, cuid2_1.createId)()), - // Room identity - code: (0, sqlite_core_1.text)('code', { length: 6 }).notNull().unique(), // e.g., "ABC123" - name: (0, sqlite_core_1.text)('name', { length: 50 }).notNull(), - // Creator info - createdBy: (0, sqlite_core_1.text)('created_by').notNull(), // User/guest ID - creatorName: (0, sqlite_core_1.text)('creator_name', { length: 50 }).notNull(), - createdAt: (0, sqlite_core_1.integer)('created_at', { mode: 'timestamp' }) - .notNull() - .$defaultFn(() => new Date()), - // Lifecycle - lastActivity: (0, sqlite_core_1.integer)('last_activity', { mode: 'timestamp' }) - .notNull() - .$defaultFn(() => new Date()), - ttlMinutes: (0, sqlite_core_1.integer)('ttl_minutes').notNull().default(60), // Time to live - isLocked: (0, sqlite_core_1.integer)('is_locked', { mode: 'boolean' }).notNull().default(false), - // Game configuration - gameName: (0, sqlite_core_1.text)('game_name', { - enum: ['matching', 'memory-quiz', 'complement-race'], - }).notNull(), - gameConfig: (0, sqlite_core_1.text)('game_config', { mode: 'json' }).notNull(), // Game-specific settings - // Current state - status: (0, sqlite_core_1.text)('status', { - enum: ['lobby', 'playing', 'finished'], - }) - .notNull() - .default('lobby'), - currentSessionId: (0, sqlite_core_1.text)('current_session_id'), // FK to arcade_sessions (nullable) - // Metadata - totalGamesPlayed: (0, sqlite_core_1.integer)('total_games_played').notNull().default(0), -}); diff --git a/apps/web/src/db/schema/arcade-sessions.js b/apps/web/src/db/schema/arcade-sessions.js deleted file mode 100644 index 70e394e5..00000000 --- a/apps/web/src/db/schema/arcade-sessions.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.arcadeSessions = void 0; -const sqlite_core_1 = require("drizzle-orm/sqlite-core"); -const arcade_rooms_1 = require("./arcade-rooms"); -const users_1 = require("./users"); -exports.arcadeSessions = (0, sqlite_core_1.sqliteTable)('arcade_sessions', { - userId: (0, sqlite_core_1.text)('user_id') - .primaryKey() - .references(() => users_1.users.id, { onDelete: 'cascade' }), - // Session metadata - currentGame: (0, sqlite_core_1.text)('current_game', { - enum: ['matching', 'memory-quiz', 'complement-race'], - }).notNull(), - gameUrl: (0, sqlite_core_1.text)('game_url').notNull(), // e.g., '/arcade/matching' - // Game state (JSON blob) - gameState: (0, sqlite_core_1.text)('game_state', { mode: 'json' }).notNull(), - // Active players snapshot (for quick access) - activePlayers: (0, sqlite_core_1.text)('active_players', { mode: 'json' }).notNull(), - // Room association (null for solo play) - roomId: (0, sqlite_core_1.text)('room_id').references(() => arcade_rooms_1.arcadeRooms.id, { onDelete: 'set null' }), - // Timing & TTL - startedAt: (0, sqlite_core_1.integer)('started_at', { mode: 'timestamp' }).notNull(), - lastActivityAt: (0, sqlite_core_1.integer)('last_activity_at', { mode: 'timestamp' }).notNull(), - expiresAt: (0, sqlite_core_1.integer)('expires_at', { mode: 'timestamp' }).notNull(), // TTL-based - // Status - isActive: (0, sqlite_core_1.integer)('is_active', { mode: 'boolean' }).notNull().default(true), - // Version for optimistic locking - version: (0, sqlite_core_1.integer)('version').notNull().default(1), -}); diff --git a/apps/web/src/db/schema/index.js b/apps/web/src/db/schema/index.js deleted file mode 100644 index 49217572..00000000 --- a/apps/web/src/db/schema/index.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; -/** - * Database schema exports - * - * This is the single source of truth for the database schema. - * All tables, relations, and types are exported from here. - */ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -__exportStar(require("./abacus-settings"), exports); -__exportStar(require("./arcade-rooms"), exports); -__exportStar(require("./arcade-sessions"), exports); -__exportStar(require("./players"), exports); -__exportStar(require("./room-members"), exports); -__exportStar(require("./user-stats"), exports); -__exportStar(require("./users"), exports); diff --git a/apps/web/src/db/schema/players.js b/apps/web/src/db/schema/players.js deleted file mode 100644 index f5a88fa5..00000000 --- a/apps/web/src/db/schema/players.js +++ /dev/null @@ -1,36 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.players = void 0; -const cuid2_1 = require("@paralleldrive/cuid2"); -const sqlite_core_1 = require("drizzle-orm/sqlite-core"); -const users_1 = require("./users"); -/** - * Players table - user-created player profiles for games - * - * Each user can have multiple players (for multi-player modes). - * Players are scoped to a user and deleted when user is deleted. - */ -exports.players = (0, sqlite_core_1.sqliteTable)('players', { - id: (0, sqlite_core_1.text)('id') - .primaryKey() - .$defaultFn(() => (0, cuid2_1.createId)()), - /** Foreign key to users table - cascades on delete */ - userId: (0, sqlite_core_1.text)('user_id') - .notNull() - .references(() => users_1.users.id, { onDelete: 'cascade' }), - /** Player display name */ - name: (0, sqlite_core_1.text)('name').notNull(), - /** Player emoji avatar */ - emoji: (0, sqlite_core_1.text)('emoji').notNull(), - /** Player color (hex) for UI theming */ - color: (0, sqlite_core_1.text)('color').notNull(), - /** Whether this player is currently active in games */ - isActive: (0, sqlite_core_1.integer)('is_active', { mode: 'boolean' }).notNull().default(false), - /** When this player was created */ - createdAt: (0, sqlite_core_1.integer)('created_at', { mode: 'timestamp' }) - .notNull() - .$defaultFn(() => new Date()), -}, (table) => ({ - /** Index for fast lookups by userId */ - userIdIdx: (0, sqlite_core_1.index)('players_user_id_idx').on(table.userId), -})); diff --git a/apps/web/src/db/schema/room-members.js b/apps/web/src/db/schema/room-members.js deleted file mode 100644 index 98ab9a25..00000000 --- a/apps/web/src/db/schema/room-members.js +++ /dev/null @@ -1,27 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.roomMembers = void 0; -const cuid2_1 = require("@paralleldrive/cuid2"); -const sqlite_core_1 = require("drizzle-orm/sqlite-core"); -const arcade_rooms_1 = require("./arcade-rooms"); -exports.roomMembers = (0, sqlite_core_1.sqliteTable)('room_members', { - id: (0, sqlite_core_1.text)('id') - .primaryKey() - .$defaultFn(() => (0, cuid2_1.createId)()), - roomId: (0, sqlite_core_1.text)('room_id') - .notNull() - .references(() => arcade_rooms_1.arcadeRooms.id, { onDelete: 'cascade' }), - userId: (0, sqlite_core_1.text)('user_id').notNull(), // User/guest ID - UNIQUE: one room per user (enforced by index below) - displayName: (0, sqlite_core_1.text)('display_name', { length: 50 }).notNull(), - isCreator: (0, sqlite_core_1.integer)('is_creator', { mode: 'boolean' }).notNull().default(false), - joinedAt: (0, sqlite_core_1.integer)('joined_at', { mode: 'timestamp' }) - .notNull() - .$defaultFn(() => new Date()), - lastSeen: (0, sqlite_core_1.integer)('last_seen', { mode: 'timestamp' }) - .notNull() - .$defaultFn(() => new Date()), - isOnline: (0, sqlite_core_1.integer)('is_online', { mode: 'boolean' }).notNull().default(true), -}, (table) => ({ - // Explicit unique index for clarity and database-level enforcement - userIdIdx: (0, sqlite_core_1.uniqueIndex)('idx_room_members_user_id_unique').on(table.userId), -})); diff --git a/apps/web/src/db/schema/user-stats.js b/apps/web/src/db/schema/user-stats.js deleted file mode 100644 index d74b6387..00000000 --- a/apps/web/src/db/schema/user-stats.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.userStats = void 0; -const sqlite_core_1 = require("drizzle-orm/sqlite-core"); -const users_1 = require("./users"); -/** - * User stats table - game statistics per user - * - * One-to-one with users table. Tracks aggregate game performance. - * Deleted when user is deleted (cascade). - */ -exports.userStats = (0, sqlite_core_1.sqliteTable)('user_stats', { - /** Primary key and foreign key to users table */ - userId: (0, sqlite_core_1.text)('user_id') - .primaryKey() - .references(() => users_1.users.id, { onDelete: 'cascade' }), - /** Total number of games played */ - gamesPlayed: (0, sqlite_core_1.integer)('games_played').notNull().default(0), - /** Total number of games won */ - totalWins: (0, sqlite_core_1.integer)('total_wins').notNull().default(0), - /** User's most-played game type */ - favoriteGameType: (0, sqlite_core_1.text)('favorite_game_type', { - enum: ['abacus-numeral', 'complement-pairs'], - }), - /** Best completion time in milliseconds */ - bestTime: (0, sqlite_core_1.integer)('best_time'), - /** Highest accuracy percentage (0.0 - 1.0) */ - highestAccuracy: (0, sqlite_core_1.real)('highest_accuracy').notNull().default(0), -}); diff --git a/apps/web/src/db/schema/users.js b/apps/web/src/db/schema/users.js deleted file mode 100644 index 34a699b1..00000000 --- a/apps/web/src/db/schema/users.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.users = void 0; -const cuid2_1 = require("@paralleldrive/cuid2"); -const sqlite_core_1 = require("drizzle-orm/sqlite-core"); -/** - * Users table - stores both guest and authenticated users - * - * Guest users are created automatically on first visit via middleware. - * They can upgrade to full accounts later while preserving their data. - */ -exports.users = (0, sqlite_core_1.sqliteTable)('users', { - id: (0, sqlite_core_1.text)('id') - .primaryKey() - .$defaultFn(() => (0, cuid2_1.createId)()), - /** Stable guest ID from HttpOnly cookie - unique per browser session */ - guestId: (0, sqlite_core_1.text)('guest_id').notNull().unique(), - /** When this user record was created */ - createdAt: (0, sqlite_core_1.integer)('created_at', { mode: 'timestamp' }) - .notNull() - .$defaultFn(() => new Date()), - /** When guest upgraded to full account (null for guests) */ - upgradedAt: (0, sqlite_core_1.integer)('upgraded_at', { mode: 'timestamp' }), - /** Email (only set after upgrade) */ - email: (0, sqlite_core_1.text)('email').unique(), - /** Display name (only set after upgrade) */ - name: (0, sqlite_core_1.text)('name'), -}); diff --git a/apps/web/src/lib/arcade/player-manager.js b/apps/web/src/lib/arcade/player-manager.js deleted file mode 100644 index bed7a44f..00000000 --- a/apps/web/src/lib/arcade/player-manager.js +++ /dev/null @@ -1,120 +0,0 @@ -"use strict"; -/** - * Player manager for arcade rooms - * Handles fetching and validating player participation in rooms - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getAllPlayers = getAllPlayers; -exports.getActivePlayers = getActivePlayers; -exports.getRoomActivePlayers = getRoomActivePlayers; -exports.getRoomPlayerIds = getRoomPlayerIds; -exports.validatePlayerInRoom = validatePlayerInRoom; -exports.getPlayer = getPlayer; -exports.getPlayers = getPlayers; -const drizzle_orm_1 = require("drizzle-orm"); -const db_1 = require("../../db"); -/** - * Get all players for a user (regardless of isActive status) - * @param viewerId - The guestId from the cookie (same as what getViewerId() returns) - */ -async function getAllPlayers(viewerId) { - // First get the user record by guestId - const user = await db_1.db.query.users.findFirst({ - where: (0, drizzle_orm_1.eq)(db_1.schema.users.guestId, viewerId), - }); - if (!user) { - return []; - } - // Now query all players by the actual user.id (no isActive filter) - return await db_1.db.query.players.findMany({ - where: (0, drizzle_orm_1.eq)(db_1.schema.players.userId, user.id), - orderBy: db_1.schema.players.createdAt, - }); -} -/** - * Get a user's active players (solo mode) - * These are the players that will participate when the user joins a solo game - * @param viewerId - The guestId from the cookie (same as what getViewerId() returns) - */ -async function getActivePlayers(viewerId) { - // First get the user record by guestId - const user = await db_1.db.query.users.findFirst({ - where: (0, drizzle_orm_1.eq)(db_1.schema.users.guestId, viewerId), - }); - if (!user) { - return []; - } - // Now query players by the actual user.id - return await db_1.db.query.players.findMany({ - where: (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.schema.players.userId, user.id), (0, drizzle_orm_1.eq)(db_1.schema.players.isActive, true)), - orderBy: db_1.schema.players.createdAt, - }); -} -/** - * Get active players for all members in a room - * Returns only players marked isActive=true from each room member - * Returns a map of userId -> Player[] - */ -async function getRoomActivePlayers(roomId) { - // Get all room members - const members = await db_1.db.query.roomMembers.findMany({ - where: (0, drizzle_orm_1.eq)(db_1.schema.roomMembers.roomId, roomId), - }); - // Fetch active players for each member (respects isActive flag) - const playerMap = new Map(); - for (const member of members) { - const players = await getActivePlayers(member.userId); - playerMap.set(member.userId, players); - } - return playerMap; -} -/** - * Get all player IDs that should participate in a room game - * Flattens the player lists from all room members - */ -async function getRoomPlayerIds(roomId) { - const playerMap = await getRoomActivePlayers(roomId); - const allPlayers = []; - for (const players of playerMap.values()) { - allPlayers.push(...players.map((p) => p.id)); - } - return allPlayers; -} -/** - * Validate that a player ID belongs to a user who is a member of a room - */ -async function validatePlayerInRoom(playerId, roomId) { - // Get the player - const player = await db_1.db.query.players.findFirst({ - where: (0, drizzle_orm_1.eq)(db_1.schema.players.id, playerId), - }); - if (!player) - return false; - // Check if the player's user is a member of the room - const member = await db_1.db.query.roomMembers.findFirst({ - where: (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.schema.roomMembers.roomId, roomId), (0, drizzle_orm_1.eq)(db_1.schema.roomMembers.userId, player.userId)), - }); - return !!member; -} -/** - * Get player details by ID - */ -async function getPlayer(playerId) { - return await db_1.db.query.players.findFirst({ - where: (0, drizzle_orm_1.eq)(db_1.schema.players.id, playerId), - }); -} -/** - * Get multiple players by IDs - */ -async function getPlayers(playerIds) { - if (playerIds.length === 0) - return []; - const players = []; - for (const id of playerIds) { - const player = await getPlayer(id); - if (player) - players.push(player); - } - return players; -} diff --git a/apps/web/src/lib/arcade/room-code.js b/apps/web/src/lib/arcade/room-code.js deleted file mode 100644 index fc5817b6..00000000 --- a/apps/web/src/lib/arcade/room-code.js +++ /dev/null @@ -1,37 +0,0 @@ -"use strict"; -/** - * Room code generation utility - * Generates short, memorable codes for joining rooms - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.generateRoomCode = generateRoomCode; -exports.isValidRoomCode = isValidRoomCode; -exports.normalizeRoomCode = normalizeRoomCode; -const CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Removed ambiguous chars: 0,O,1,I -const CODE_LENGTH = 6; -/** - * Generate a random 6-character room code - * Format: ABC123 (uppercase letters + numbers, no ambiguous chars) - */ -function generateRoomCode() { - let code = ''; - for (let i = 0; i < CODE_LENGTH; i++) { - const randomIndex = Math.floor(Math.random() * CHARS.length); - code += CHARS[randomIndex]; - } - return code; -} -/** - * Validate a room code format - */ -function isValidRoomCode(code) { - if (code.length !== CODE_LENGTH) - return false; - return code.split('').every((char) => CHARS.includes(char)); -} -/** - * Normalize a room code (uppercase, remove spaces/dashes) - */ -function normalizeRoomCode(code) { - return code.toUpperCase().replace(/[\s-]/g, ''); -} diff --git a/apps/web/src/lib/arcade/room-manager.js b/apps/web/src/lib/arcade/room-manager.js deleted file mode 100644 index beb34dd8..00000000 --- a/apps/web/src/lib/arcade/room-manager.js +++ /dev/null @@ -1,154 +0,0 @@ -"use strict"; -/** - * Arcade room manager - * Handles database operations for arcade rooms - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createRoom = createRoom; -exports.getRoomById = getRoomById; -exports.getRoomByCode = getRoomByCode; -exports.updateRoom = updateRoom; -exports.touchRoom = touchRoom; -exports.deleteRoom = deleteRoom; -exports.listActiveRooms = listActiveRooms; -exports.cleanupExpiredRooms = cleanupExpiredRooms; -exports.isRoomCreator = isRoomCreator; -const drizzle_orm_1 = require("drizzle-orm"); -const db_1 = require("../../db"); -const room_code_1 = require("./room-code"); -/** - * Create a new arcade room - * Generates a unique room code and creates the room in the database - */ -async function createRoom(options) { - const now = new Date(); - // Generate unique room code (retry up to 5 times if collision) - let code = (0, room_code_1.generateRoomCode)(); - let attempts = 0; - const MAX_ATTEMPTS = 5; - while (attempts < MAX_ATTEMPTS) { - const existing = await getRoomByCode(code); - if (!existing) - break; - code = (0, room_code_1.generateRoomCode)(); - attempts++; - } - if (attempts === MAX_ATTEMPTS) { - throw new Error('Failed to generate unique room code'); - } - const newRoom = { - code, - name: options.name, - createdBy: options.createdBy, - creatorName: options.creatorName, - createdAt: now, - lastActivity: now, - ttlMinutes: options.ttlMinutes || 60, - isLocked: false, - gameName: options.gameName, - gameConfig: options.gameConfig, - status: 'lobby', - currentSessionId: null, - totalGamesPlayed: 0, - }; - const [room] = await db_1.db.insert(db_1.schema.arcadeRooms).values(newRoom).returning(); - console.log('[Room Manager] Created room:', room.id, 'code:', room.code); - return room; -} -/** - * Get a room by ID - */ -async function getRoomById(roomId) { - return await db_1.db.query.arcadeRooms.findFirst({ - where: (0, drizzle_orm_1.eq)(db_1.schema.arcadeRooms.id, roomId), - }); -} -/** - * Get a room by code - */ -async function getRoomByCode(code) { - return await db_1.db.query.arcadeRooms.findFirst({ - where: (0, drizzle_orm_1.eq)(db_1.schema.arcadeRooms.code, code.toUpperCase()), - }); -} -/** - * Update a room - */ -async function updateRoom(roomId, updates) { - const now = new Date(); - // Always update lastActivity on any room update - const updateData = { - ...updates, - lastActivity: now, - }; - const [updated] = await db_1.db - .update(db_1.schema.arcadeRooms) - .set(updateData) - .where((0, drizzle_orm_1.eq)(db_1.schema.arcadeRooms.id, roomId)) - .returning(); - return updated; -} -/** - * Update room activity timestamp - * Call this on any room activity to refresh TTL - */ -async function touchRoom(roomId) { - await db_1.db - .update(db_1.schema.arcadeRooms) - .set({ lastActivity: new Date() }) - .where((0, drizzle_orm_1.eq)(db_1.schema.arcadeRooms.id, roomId)); -} -/** - * Delete a room - * Cascade deletes all room members - */ -async function deleteRoom(roomId) { - await db_1.db.delete(db_1.schema.arcadeRooms).where((0, drizzle_orm_1.eq)(db_1.schema.arcadeRooms.id, roomId)); - console.log('[Room Manager] Deleted room:', roomId); -} -/** - * List active rooms - * Returns rooms ordered by most recently active - */ -async function listActiveRooms(gameName) { - const whereConditions = []; - // Filter by game if specified - if (gameName) { - whereConditions.push((0, drizzle_orm_1.eq)(db_1.schema.arcadeRooms.gameName, gameName)); - } - // Only return non-locked rooms in lobby or playing status - whereConditions.push((0, drizzle_orm_1.eq)(db_1.schema.arcadeRooms.isLocked, false), (0, drizzle_orm_1.or)((0, drizzle_orm_1.eq)(db_1.schema.arcadeRooms.status, 'lobby'), (0, drizzle_orm_1.eq)(db_1.schema.arcadeRooms.status, 'playing'))); - return await db_1.db.query.arcadeRooms.findMany({ - where: whereConditions.length > 0 ? (0, drizzle_orm_1.and)(...whereConditions) : undefined, - orderBy: [(0, drizzle_orm_1.desc)(db_1.schema.arcadeRooms.lastActivity)], - limit: 50, // Limit to 50 most recent rooms - }); -} -/** - * Clean up expired rooms - * Delete rooms that have exceeded their TTL - */ -async function cleanupExpiredRooms() { - const now = new Date(); - // Find rooms where lastActivity + ttlMinutes < now - const expiredRooms = await db_1.db.query.arcadeRooms.findMany({ - columns: { id: true, ttlMinutes: true, lastActivity: true }, - }); - const toDelete = expiredRooms.filter((room) => { - const expiresAt = new Date(room.lastActivity.getTime() + room.ttlMinutes * 60 * 1000); - return expiresAt < now; - }); - if (toDelete.length > 0) { - const ids = toDelete.map((r) => r.id); - await db_1.db.delete(db_1.schema.arcadeRooms).where((0, drizzle_orm_1.or)(...ids.map((id) => (0, drizzle_orm_1.eq)(db_1.schema.arcadeRooms.id, id)))); - console.log(`[Room Manager] Cleaned up ${toDelete.length} expired rooms`); - } - return toDelete.length; -} -/** - * Check if a user is the creator of a room - */ -async function isRoomCreator(roomId, userId) { - const room = await getRoomById(roomId); - return room?.createdBy === userId; -} diff --git a/apps/web/src/lib/arcade/room-membership.js b/apps/web/src/lib/arcade/room-membership.js deleted file mode 100644 index 25a1aa04..00000000 --- a/apps/web/src/lib/arcade/room-membership.js +++ /dev/null @@ -1,179 +0,0 @@ -"use strict"; -/** - * Room membership manager - * Handles database operations for room members - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.addRoomMember = addRoomMember; -exports.getRoomMember = getRoomMember; -exports.getRoomMembers = getRoomMembers; -exports.getOnlineRoomMembers = getOnlineRoomMembers; -exports.setMemberOnline = setMemberOnline; -exports.touchMember = touchMember; -exports.removeMember = removeMember; -exports.removeAllMembers = removeAllMembers; -exports.getOnlineMemberCount = getOnlineMemberCount; -exports.isMember = isMember; -exports.getUserRooms = getUserRooms; -const drizzle_orm_1 = require("drizzle-orm"); -const db_1 = require("../../db"); -/** - * Add a member to a room - * Automatically removes user from any other rooms they're in (modal room enforcement) - * Returns the new membership and info about rooms that were auto-left - */ -async function addRoomMember(options) { - const now = new Date(); - // Check if member already exists in THIS room - const existing = await getRoomMember(options.roomId, options.userId); - if (existing) { - // Already in this room - just update status (no auto-leave needed) - const [updated] = await db_1.db - .update(db_1.schema.roomMembers) - .set({ - isOnline: true, - lastSeen: now, - }) - .where((0, drizzle_orm_1.eq)(db_1.schema.roomMembers.id, existing.id)) - .returning(); - return { member: updated }; - } - // AUTO-LEAVE LOGIC: Remove from all other rooms before joining this one - const currentRooms = await getUserRooms(options.userId); - const autoLeaveResult = { - leftRooms: [], - previousRoomMembers: [], - }; - for (const roomId of currentRooms) { - if (roomId !== options.roomId) { - // Get member info before removing (for socket events) - const memberToRemove = await getRoomMember(roomId, options.userId); - if (memberToRemove) { - autoLeaveResult.previousRoomMembers.push({ - roomId, - member: memberToRemove, - }); - } - // Remove from room - await removeMember(roomId, options.userId); - autoLeaveResult.leftRooms.push(roomId); - console.log(`[Room Membership] Auto-left room ${roomId} for user ${options.userId}`); - } - } - // Now add to new room - const newMember = { - roomId: options.roomId, - userId: options.userId, - displayName: options.displayName, - isCreator: options.isCreator || false, - joinedAt: now, - lastSeen: now, - isOnline: true, - }; - try { - const [member] = await db_1.db.insert(db_1.schema.roomMembers).values(newMember).returning(); - console.log('[Room Membership] Added member:', member.userId, 'to room:', member.roomId); - return { - member, - autoLeaveResult: autoLeaveResult.leftRooms.length > 0 ? autoLeaveResult : undefined, - }; - } - catch (error) { - // Handle unique constraint violation - // This should rarely happen due to auto-leave logic above, but catch it for safety - if (error.code === 'SQLITE_CONSTRAINT' || - error.message?.includes('UNIQUE') || - error.message?.includes('unique')) { - console.error('[Room Membership] Unique constraint violation:', error.message); - throw new Error('ROOM_MEMBERSHIP_CONFLICT: User is already in another room. This should have been handled by auto-leave logic.'); - } - throw error; - } -} -/** - * Get a specific room member - */ -async function getRoomMember(roomId, userId) { - return await db_1.db.query.roomMembers.findFirst({ - where: (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.schema.roomMembers.roomId, roomId), (0, drizzle_orm_1.eq)(db_1.schema.roomMembers.userId, userId)), - }); -} -/** - * Get all members in a room - */ -async function getRoomMembers(roomId) { - return await db_1.db.query.roomMembers.findMany({ - where: (0, drizzle_orm_1.eq)(db_1.schema.roomMembers.roomId, roomId), - orderBy: db_1.schema.roomMembers.joinedAt, - }); -} -/** - * Get online members in a room - */ -async function getOnlineRoomMembers(roomId) { - return await db_1.db.query.roomMembers.findMany({ - where: (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.schema.roomMembers.roomId, roomId), (0, drizzle_orm_1.eq)(db_1.schema.roomMembers.isOnline, true)), - orderBy: db_1.schema.roomMembers.joinedAt, - }); -} -/** - * Update member's online status - */ -async function setMemberOnline(roomId, userId, isOnline) { - await db_1.db - .update(db_1.schema.roomMembers) - .set({ - isOnline, - lastSeen: new Date(), - }) - .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.schema.roomMembers.roomId, roomId), (0, drizzle_orm_1.eq)(db_1.schema.roomMembers.userId, userId))); -} -/** - * Update member's last seen timestamp - */ -async function touchMember(roomId, userId) { - await db_1.db - .update(db_1.schema.roomMembers) - .set({ lastSeen: new Date() }) - .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.schema.roomMembers.roomId, roomId), (0, drizzle_orm_1.eq)(db_1.schema.roomMembers.userId, userId))); -} -/** - * Remove a member from a room - */ -async function removeMember(roomId, userId) { - await db_1.db - .delete(db_1.schema.roomMembers) - .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.schema.roomMembers.roomId, roomId), (0, drizzle_orm_1.eq)(db_1.schema.roomMembers.userId, userId))); - console.log('[Room Membership] Removed member:', userId, 'from room:', roomId); -} -/** - * Remove all members from a room - */ -async function removeAllMembers(roomId) { - await db_1.db.delete(db_1.schema.roomMembers).where((0, drizzle_orm_1.eq)(db_1.schema.roomMembers.roomId, roomId)); - console.log('[Room Membership] Removed all members from room:', roomId); -} -/** - * Get count of online members in a room - */ -async function getOnlineMemberCount(roomId) { - const members = await getOnlineRoomMembers(roomId); - return members.length; -} -/** - * Check if a user is a member of a room - */ -async function isMember(roomId, userId) { - const member = await getRoomMember(roomId, userId); - return !!member; -} -/** - * Get all rooms a user is a member of - */ -async function getUserRooms(userId) { - const memberships = await db_1.db.query.roomMembers.findMany({ - where: (0, drizzle_orm_1.eq)(db_1.schema.roomMembers.userId, userId), - columns: { roomId: true }, - }); - return memberships.map((m) => m.roomId); -} diff --git a/apps/web/src/lib/arcade/room-ttl-cleanup.js b/apps/web/src/lib/arcade/room-ttl-cleanup.js deleted file mode 100644 index 04587210..00000000 --- a/apps/web/src/lib/arcade/room-ttl-cleanup.js +++ /dev/null @@ -1,55 +0,0 @@ -"use strict"; -/** - * Room TTL Cleanup Scheduler - * Periodically cleans up expired rooms - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.startRoomTTLCleanup = startRoomTTLCleanup; -exports.stopRoomTTLCleanup = stopRoomTTLCleanup; -const room_manager_1 = require("./room-manager"); -// Cleanup interval: run every 5 minutes -const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; -let cleanupInterval = null; -/** - * Start the TTL cleanup scheduler - * Runs cleanup every 5 minutes - */ -function startRoomTTLCleanup() { - if (cleanupInterval) { - console.log('[Room TTL] Cleanup scheduler already running'); - return; - } - console.log('[Room TTL] Starting cleanup scheduler (every 5 minutes)'); - // Run immediately on start - (0, room_manager_1.cleanupExpiredRooms)() - .then((count) => { - if (count > 0) { - console.log(`[Room TTL] Initial cleanup removed ${count} expired rooms`); - } - }) - .catch((error) => { - console.error('[Room TTL] Initial cleanup failed:', error); - }); - // Then run periodically - cleanupInterval = setInterval(async () => { - try { - const count = await (0, room_manager_1.cleanupExpiredRooms)(); - if (count > 0) { - console.log(`[Room TTL] Cleanup removed ${count} expired rooms`); - } - } - catch (error) { - console.error('[Room TTL] Cleanup failed:', error); - } - }, CLEANUP_INTERVAL_MS); -} -/** - * Stop the TTL cleanup scheduler - */ -function stopRoomTTLCleanup() { - if (cleanupInterval) { - clearInterval(cleanupInterval); - cleanupInterval = null; - console.log('[Room TTL] Cleanup scheduler stopped'); - } -} diff --git a/apps/web/src/lib/arcade/session-manager.js b/apps/web/src/lib/arcade/session-manager.js deleted file mode 100644 index 0e3fb0a8..00000000 --- a/apps/web/src/lib/arcade/session-manager.js +++ /dev/null @@ -1,296 +0,0 @@ -"use strict"; -/** - * Arcade session manager - * Handles database operations and validation for arcade sessions - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getArcadeSessionByRoom = getArcadeSessionByRoom; -exports.createArcadeSession = createArcadeSession; -exports.getArcadeSession = getArcadeSession; -exports.applyGameMove = applyGameMove; -exports.deleteArcadeSession = deleteArcadeSession; -exports.updateSessionActivity = updateSessionActivity; -exports.cleanupExpiredSessions = cleanupExpiredSessions; -const drizzle_orm_1 = require("drizzle-orm"); -const db_1 = require("../../db"); -const validation_1 = require("./validation"); -const TTL_HOURS = 24; -/** - * Helper: Get database user ID from guest ID - * The API uses guestId (from cookies) but database FKs use the internal user.id - */ -async function getUserIdFromGuestId(guestId) { - const user = await db_1.db.query.users.findFirst({ - where: (0, drizzle_orm_1.eq)(db_1.schema.users.guestId, guestId), - columns: { id: true }, - }); - return user?.id; -} -/** - * Get arcade session by room ID (for room-based multiplayer games) - * Returns the shared session for all room members - * @param roomId - The room ID - */ -async function getArcadeSessionByRoom(roomId) { - const [session] = await db_1.db - .select() - .from(db_1.schema.arcadeSessions) - .where((0, drizzle_orm_1.eq)(db_1.schema.arcadeSessions.roomId, roomId)) - .limit(1); - if (!session) - return undefined; - // Check if session has expired - if (session.expiresAt < new Date()) { - // Clean up expired room session - await db_1.db.delete(db_1.schema.arcadeSessions).where((0, drizzle_orm_1.eq)(db_1.schema.arcadeSessions.roomId, roomId)); - return undefined; - } - return session; -} -/** - * Create a new arcade session - * For room-based games, checks if a session already exists for the room - */ -async function createArcadeSession(options) { - const now = new Date(); - const expiresAt = new Date(now.getTime() + TTL_HOURS * 60 * 60 * 1000); - // For room-based games, check if session already exists for this room - if (options.roomId) { - const existingRoomSession = await getArcadeSessionByRoom(options.roomId); - if (existingRoomSession) { - console.log('[Session Manager] Room session already exists, returning existing:', { - roomId: options.roomId, - sessionUserId: existingRoomSession.userId, - version: existingRoomSession.version, - }); - return existingRoomSession; - } - } - // Find or create user by guest ID - let user = await db_1.db.query.users.findFirst({ - where: (0, drizzle_orm_1.eq)(db_1.schema.users.guestId, options.userId), - }); - if (!user) { - console.log('[Session Manager] Creating new user with guestId:', options.userId); - const [newUser] = await db_1.db - .insert(db_1.schema.users) - .values({ - guestId: options.userId, // Let id auto-generate via $defaultFn - createdAt: now, - }) - .returning(); - user = newUser; - console.log('[Session Manager] Created user with id:', user.id); - } - else { - console.log('[Session Manager] Found existing user with id:', user.id); - } - const newSession = { - userId: user.id, // Use the actual database ID, not the guestId - currentGame: options.gameName, - gameUrl: options.gameUrl, - gameState: options.initialState, - activePlayers: options.activePlayers, - roomId: options.roomId, // Associate session with room - startedAt: now, - lastActivityAt: now, - expiresAt, - isActive: true, - version: 1, - }; - console.log('[Session Manager] Creating new session:', { - userId: user.id, - roomId: options.roomId, - gameName: options.gameName, - }); - const [session] = await db_1.db.insert(db_1.schema.arcadeSessions).values(newSession).returning(); - return session; -} -/** - * Get active arcade session for a user - * @param guestId - The guest ID from the cookie (not the database user.id) - */ -async function getArcadeSession(guestId) { - const userId = await getUserIdFromGuestId(guestId); - if (!userId) - return undefined; - const [session] = await db_1.db - .select() - .from(db_1.schema.arcadeSessions) - .where((0, drizzle_orm_1.eq)(db_1.schema.arcadeSessions.userId, userId)) - .limit(1); - if (!session) - return undefined; - // Check if session has expired - if (session.expiresAt < new Date()) { - await deleteArcadeSession(guestId); - return undefined; - } - // Check if session has a valid room association - // Sessions without rooms are orphaned and should be cleaned up - if (!session.roomId) { - console.log('[Session Manager] Deleting orphaned session without room:', session.userId); - await deleteArcadeSession(guestId); - return undefined; - } - // Verify the room still exists - const room = await db_1.db.query.arcadeRooms.findFirst({ - where: (0, drizzle_orm_1.eq)(db_1.schema.arcadeRooms.id, session.roomId), - }); - if (!room) { - console.log('[Session Manager] Deleting session with non-existent room:', session.roomId); - await deleteArcadeSession(guestId); - return undefined; - } - return session; -} -/** - * Apply a game move to the session (with validation) - * @param userId - The guest ID from the cookie - * @param move - The game move to apply - * @param roomId - Optional room ID for room-based games (enables shared session) - */ -async function applyGameMove(userId, move, roomId) { - // For room-based games, look up the shared room session - // For solo games, look up the user's personal session - const session = roomId ? await getArcadeSessionByRoom(roomId) : await getArcadeSession(userId); - if (!session) { - return { - success: false, - error: 'No active session found', - }; - } - if (!session.isActive) { - return { - success: false, - error: 'Session is not active', - }; - } - // Get the validator for this game - const validator = (0, validation_1.getValidator)(session.currentGame); - console.log('[SessionManager] About to validate move:', { - moveType: move.type, - playerId: move.playerId, - gameStateCurrentPlayer: session.gameState?.currentPlayer, - gameStateActivePlayers: session.gameState?.activePlayers, - gameStatePhase: session.gameState?.gamePhase, - }); - // Fetch player ownership for authorization checks (room-based games) - let playerOwnership; - let internalUserId; - if (session.roomId) { - try { - // Convert guestId to internal userId for ownership comparison - internalUserId = await getUserIdFromGuestId(userId); - if (!internalUserId) { - console.error('[SessionManager] Failed to convert guestId to userId:', userId); - return { - success: false, - error: 'User not found', - }; - } - const players = await db_1.db.query.players.findMany({ - columns: { - id: true, - userId: true, - }, - }); - playerOwnership = Object.fromEntries(players.map((p) => [p.id, p.userId])); - console.log('[SessionManager] Player ownership map:', playerOwnership); - console.log('[SessionManager] Internal userId for authorization:', internalUserId); - } - catch (error) { - console.error('[SessionManager] Failed to fetch player ownership:', error); - } - } - // Validate the move with authorization context (use internal userId, not guestId) - const validationResult = validator.validateMove(session.gameState, move, { - userId: internalUserId || userId, // Use internal userId for room-based games - playerOwnership, - }); - console.log('[SessionManager] Validation result:', { - valid: validationResult.valid, - error: validationResult.error, - }); - if (!validationResult.valid) { - return { - success: false, - error: validationResult.error || 'Invalid move', - }; - } - // Update the session with new state (using optimistic locking) - const now = new Date(); - const expiresAt = new Date(now.getTime() + TTL_HOURS * 60 * 60 * 1000); - try { - const [updatedSession] = await db_1.db - .update(db_1.schema.arcadeSessions) - .set({ - gameState: validationResult.newState, - lastActivityAt: now, - expiresAt, - version: session.version + 1, - }) - .where((0, drizzle_orm_1.eq)(db_1.schema.arcadeSessions.userId, session.userId) // Use the userId from the session we just fetched - ) - // Version check for optimistic locking would go here - // SQLite doesn't support WHERE clauses in UPDATE with RETURNING easily - // We'll handle this by checking the version after - .returning(); - if (!updatedSession) { - return { - success: false, - error: 'Failed to update session', - }; - } - return { - success: true, - session: updatedSession, - }; - } - catch (error) { - console.error('Error updating session:', error); - return { - success: false, - error: 'Database error', - }; - } -} -/** - * Delete an arcade session - * @param guestId - The guest ID from the cookie (not the database user.id) - */ -async function deleteArcadeSession(guestId) { - const userId = await getUserIdFromGuestId(guestId); - if (!userId) - return; - await db_1.db.delete(db_1.schema.arcadeSessions).where((0, drizzle_orm_1.eq)(db_1.schema.arcadeSessions.userId, userId)); -} -/** - * Update session activity timestamp (keep-alive) - * @param guestId - The guest ID from the cookie (not the database user.id) - */ -async function updateSessionActivity(guestId) { - const userId = await getUserIdFromGuestId(guestId); - if (!userId) - return; - const now = new Date(); - const expiresAt = new Date(now.getTime() + TTL_HOURS * 60 * 60 * 1000); - await db_1.db - .update(db_1.schema.arcadeSessions) - .set({ - lastActivityAt: now, - expiresAt, - }) - .where((0, drizzle_orm_1.eq)(db_1.schema.arcadeSessions.userId, userId)); -} -/** - * Clean up expired sessions (should be called periodically) - */ -async function cleanupExpiredSessions() { - const now = new Date(); - const result = await db_1.db - .delete(db_1.schema.arcadeSessions) - .where((0, drizzle_orm_1.eq)(db_1.schema.arcadeSessions.expiresAt, now)) - .returning(); - return result.length; -} diff --git a/apps/web/src/lib/arcade/validation/MatchingGameValidator.js b/apps/web/src/lib/arcade/validation/MatchingGameValidator.js deleted file mode 100644 index 49718695..00000000 --- a/apps/web/src/lib/arcade/validation/MatchingGameValidator.js +++ /dev/null @@ -1,469 +0,0 @@ -"use strict"; -/** - * Server-side validator for matching game - * Validates all game moves and state transitions - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.matchingGameValidator = exports.MatchingGameValidator = void 0; -const cardGeneration_1 = require("../../../app/games/matching/utils/cardGeneration"); -const matchValidation_1 = require("../../../app/games/matching/utils/matchValidation"); -class MatchingGameValidator { - validateMove(state, move, context) { - switch (move.type) { - case 'FLIP_CARD': - return this.validateFlipCard(state, move.data.cardId, move.playerId, context); - case 'START_GAME': - return this.validateStartGame(state, move.data.activePlayers, move.data.cards, move.data.playerMetadata); - case 'CLEAR_MISMATCH': - return this.validateClearMismatch(state); - case 'GO_TO_SETUP': - return this.validateGoToSetup(state); - case 'SET_CONFIG': - return this.validateSetConfig(state, move.data.field, move.data.value); - case 'RESUME_GAME': - return this.validateResumeGame(state); - case 'HOVER_CARD': - return this.validateHoverCard(state, move.data.cardId, move.playerId); - default: - return { - valid: false, - error: `Unknown move type: ${move.type}`, - }; - } - } - validateFlipCard(state, cardId, playerId, context) { - // Game must be in playing phase - if (state.gamePhase !== 'playing') { - return { - valid: false, - error: 'Cannot flip cards outside of playing phase', - }; - } - // Check if it's the player's turn (in multiplayer) - if (state.activePlayers.length > 1 && state.currentPlayer !== playerId) { - console.log('[Validator] Turn check failed:', { - activePlayers: state.activePlayers, - currentPlayer: state.currentPlayer, - currentPlayerType: typeof state.currentPlayer, - playerId, - playerIdType: typeof playerId, - matches: state.currentPlayer === playerId, - }); - return { - valid: false, - error: 'Not your turn', - }; - } - // Check player ownership authorization (if context provided) - if (context?.userId && context?.playerOwnership) { - const playerOwner = context.playerOwnership[playerId]; - if (playerOwner && playerOwner !== context.userId) { - console.log('[Validator] Player ownership check failed:', { - playerId, - playerOwner, - requestingUserId: context.userId, - }); - return { - valid: false, - error: 'You can only move your own players', - }; - } - } - // Find the card - const card = state.gameCards.find((c) => c.id === cardId); - if (!card) { - return { - valid: false, - error: 'Card not found', - }; - } - // Validate using existing game logic - if (!(0, matchValidation_1.canFlipCard)(card, state.flippedCards, state.isProcessingMove)) { - return { - valid: false, - error: 'Cannot flip this card', - }; - } - // Calculate new state - const newFlippedCards = [...state.flippedCards, card]; - let newState = { - ...state, - flippedCards: newFlippedCards, - isProcessingMove: newFlippedCards.length === 2, - // Clear mismatch feedback when player flips a new card - showMismatchFeedback: false, - }; - // If two cards are flipped, check for match - if (newFlippedCards.length === 2) { - const [card1, card2] = newFlippedCards; - const matchResult = (0, matchValidation_1.validateMatch)(card1, card2); - if (matchResult.isValid) { - // Match found - update cards - newState = { - ...newState, - gameCards: newState.gameCards.map((c) => c.id === card1.id || c.id === card2.id - ? { ...c, matched: true, matchedBy: state.currentPlayer } - : c), - matchedPairs: state.matchedPairs + 1, - scores: { - ...state.scores, - [state.currentPlayer]: (state.scores[state.currentPlayer] || 0) + 1, - }, - consecutiveMatches: { - ...state.consecutiveMatches, - [state.currentPlayer]: (state.consecutiveMatches[state.currentPlayer] || 0) + 1, - }, - moves: state.moves + 1, - flippedCards: [], - isProcessingMove: false, - }; - // Check if game is complete - if (newState.matchedPairs === newState.totalPairs) { - newState = { - ...newState, - gamePhase: 'results', - gameEndTime: Date.now(), - }; - } - } - else { - // Match failed - keep cards flipped briefly so player can see them - // Client will handle clearing them after a delay - const shouldSwitchPlayer = state.activePlayers.length > 1; - const nextPlayerIndex = shouldSwitchPlayer - ? (state.activePlayers.indexOf(state.currentPlayer) + 1) % state.activePlayers.length - : 0; - const nextPlayer = shouldSwitchPlayer - ? state.activePlayers[nextPlayerIndex] - : state.currentPlayer; - newState = { - ...newState, - currentPlayer: nextPlayer, - consecutiveMatches: { - ...state.consecutiveMatches, - [state.currentPlayer]: 0, - }, - moves: state.moves + 1, - // Keep flippedCards so player can see both cards - flippedCards: newFlippedCards, - isProcessingMove: true, // Keep processing state so no more cards can be flipped - showMismatchFeedback: true, - }; - } - } - return { - valid: true, - newState, - }; - } - validateStartGame(state, activePlayers, cards, playerMetadata) { - // Allow starting a new game from any phase (for "New Game" button) - // Must have at least one player - if (!activePlayers || activePlayers.length === 0) { - return { - valid: false, - error: 'Must have at least one player', - }; - } - // Use provided cards or generate new ones - const gameCards = cards || (0, cardGeneration_1.generateGameCards)(state.gameType, state.difficulty); - const newState = { - ...state, - gameCards, - cards: gameCards, - activePlayers, - playerMetadata: playerMetadata || {}, // Store player metadata for cross-user visibility - gamePhase: 'playing', - gameStartTime: Date.now(), - currentPlayer: activePlayers[0], - flippedCards: [], - matchedPairs: 0, - moves: 0, - scores: activePlayers.reduce((acc, p) => ({ ...acc, [p]: 0 }), {}), - consecutiveMatches: activePlayers.reduce((acc, p) => ({ ...acc, [p]: 0 }), {}), - // PAUSE/RESUME: Save original config so we can detect changes - originalConfig: { - gameType: state.gameType, - difficulty: state.difficulty, - turnTimer: state.turnTimer, - }, - // Clear any paused game state (starting fresh) - pausedGamePhase: undefined, - pausedGameState: undefined, - }; - return { - valid: true, - newState, - }; - } - validateClearMismatch(state) { - // Only clear if there's actually a mismatch showing - // This prevents race conditions where CLEAR_MISMATCH arrives after cards have already been cleared - if (!state.showMismatchFeedback || state.flippedCards.length === 0) { - // Nothing to clear - return current state unchanged - return { - valid: true, - newState: state, - }; - } - // Clear mismatched cards and feedback - return { - valid: true, - newState: { - ...state, - flippedCards: [], - showMismatchFeedback: false, - isProcessingMove: false, - }, - }; - } - /** - * STANDARD ARCADE PATTERN: GO_TO_SETUP - * - * Transitions the game back to setup phase, allowing players to reconfigure - * the game. This is synchronized across all room members. - * - * Can be called from any phase (setup, playing, results). - * - * PAUSE/RESUME: If called from 'playing' or 'results', saves game state - * to allow resuming later (if config unchanged). - * - * Pattern for all arcade games: - * - Validates the move is allowed - * - Sets gamePhase to 'setup' - * - Preserves current configuration (gameType, difficulty, etc.) - * - Saves game state for resume if coming from active game - * - Resets game progression state (scores, cards, etc.) - */ - validateGoToSetup(state) { - // Determine if we're pausing an active game (for Resume functionality) - const isPausingGame = state.gamePhase === 'playing' || state.gamePhase === 'results'; - return { - valid: true, - newState: { - ...state, - gamePhase: 'setup', - // Pause/Resume: Save game state if pausing from active game - pausedGamePhase: isPausingGame ? state.gamePhase : undefined, - pausedGameState: isPausingGame - ? { - gameCards: state.gameCards, - currentPlayer: state.currentPlayer, - matchedPairs: state.matchedPairs, - moves: state.moves, - scores: state.scores, - activePlayers: state.activePlayers, - playerMetadata: state.playerMetadata, - consecutiveMatches: state.consecutiveMatches, - gameStartTime: state.gameStartTime, - } - : undefined, - // Keep originalConfig if it exists (was set when game started) - // This allows detecting if config changed while paused - // Reset visible game progression - gameCards: [], - cards: [], - flippedCards: [], - currentPlayer: '', - matchedPairs: 0, - moves: 0, - scores: {}, - activePlayers: [], - playerMetadata: {}, - consecutiveMatches: {}, - gameStartTime: null, - gameEndTime: null, - currentMoveStartTime: null, - celebrationAnimations: [], - isProcessingMove: false, - showMismatchFeedback: false, - lastMatchedPair: null, - // Preserve configuration - players can modify in setup - // gameType, difficulty, turnTimer stay as-is - }, - }; - } - /** - * STANDARD ARCADE PATTERN: SET_CONFIG - * - * Updates a configuration field during setup phase. This is synchronized - * across all room members in real-time, allowing collaborative setup. - * - * Pattern for all arcade games: - * - Only allowed during setup phase - * - Validates field name and value - * - Updates the configuration field - * - Other room members see the change immediately (optimistic + server validation) - * - * @param state Current game state - * @param field Configuration field name - * @param value New value for the field - */ - validateSetConfig(state, field, value) { - // Can only change config during setup phase - if (state.gamePhase !== 'setup') { - return { - valid: false, - error: 'Cannot change configuration outside of setup phase', - }; - } - // Validate field-specific values - switch (field) { - case 'gameType': - if (value !== 'abacus-numeral' && value !== 'complement-pairs') { - return { valid: false, error: `Invalid gameType: ${value}` }; - } - break; - case 'difficulty': - if (![6, 8, 12, 15].includes(value)) { - return { valid: false, error: `Invalid difficulty: ${value}` }; - } - break; - case 'turnTimer': - if (typeof value !== 'number' || value < 5 || value > 300) { - return { valid: false, error: `Invalid turnTimer: ${value}` }; - } - break; - default: - return { valid: false, error: `Unknown config field: ${field}` }; - } - // PAUSE/RESUME: If there's a paused game and config is changing, - // clear the paused game state (can't resume anymore) - const clearPausedGame = !!state.pausedGamePhase; - // Apply the configuration change - return { - valid: true, - newState: { - ...state, - [field]: value, - // Update totalPairs if difficulty changes - ...(field === 'difficulty' ? { totalPairs: value } : {}), - // Clear paused game if config changed - ...(clearPausedGame - ? { pausedGamePhase: undefined, pausedGameState: undefined, originalConfig: undefined } - : {}), - }, - }; - } - /** - * STANDARD ARCADE PATTERN: RESUME_GAME - * - * Resumes a paused game if configuration hasn't changed. - * Restores the saved game state from when GO_TO_SETUP was called. - * - * Pattern for all arcade games: - * - Validates there's a paused game - * - Validates config hasn't changed since pause - * - Restores game state and phase - * - Clears paused game state - */ - validateResumeGame(state) { - // Must be in setup phase - if (state.gamePhase !== 'setup') { - return { - valid: false, - error: 'Can only resume from setup phase', - }; - } - // Must have a paused game - if (!state.pausedGamePhase || !state.pausedGameState) { - return { - valid: false, - error: 'No paused game to resume', - }; - } - // Config must match original (no changes while paused) - if (state.originalConfig) { - const configChanged = state.gameType !== state.originalConfig.gameType || - state.difficulty !== state.originalConfig.difficulty || - state.turnTimer !== state.originalConfig.turnTimer; - if (configChanged) { - return { - valid: false, - error: 'Cannot resume - configuration has changed', - }; - } - } - // Restore the paused game - return { - valid: true, - newState: { - ...state, - gamePhase: state.pausedGamePhase, - gameCards: state.pausedGameState.gameCards, - cards: state.pausedGameState.gameCards, - currentPlayer: state.pausedGameState.currentPlayer, - matchedPairs: state.pausedGameState.matchedPairs, - moves: state.pausedGameState.moves, - scores: state.pausedGameState.scores, - activePlayers: state.pausedGameState.activePlayers, - playerMetadata: state.pausedGameState.playerMetadata, - consecutiveMatches: state.pausedGameState.consecutiveMatches, - gameStartTime: state.pausedGameState.gameStartTime, - // Clear paused state - pausedGamePhase: undefined, - pausedGameState: undefined, - // Keep originalConfig for potential future pauses - }, - }; - } - /** - * Validate hover state update for networked presence - * - * Hover moves are lightweight and always valid - they just update - * which card a player is hovering over for UI feedback to other players. - */ - validateHoverCard(state, cardId, playerId) { - // Hover is always valid - it's just UI state for networked presence - // Update the player's hover state - return { - valid: true, - newState: { - ...state, - playerHovers: { - ...state.playerHovers, - [playerId]: cardId, - }, - }, - }; - } - isGameComplete(state) { - return state.gamePhase === 'results' || state.matchedPairs === state.totalPairs; - } - getInitialState(config) { - return { - cards: [], - gameCards: [], - flippedCards: [], - gameType: config.gameType, - difficulty: config.difficulty, - turnTimer: config.turnTimer, - gamePhase: 'setup', - currentPlayer: '', - matchedPairs: 0, - totalPairs: config.difficulty, - moves: 0, - scores: {}, - activePlayers: [], - playerMetadata: {}, // Initialize empty player metadata - consecutiveMatches: {}, - gameStartTime: null, - gameEndTime: null, - currentMoveStartTime: null, - timerInterval: null, - celebrationAnimations: [], - isProcessingMove: false, - showMismatchFeedback: false, - lastMatchedPair: null, - // PAUSE/RESUME: Initialize paused game fields - originalConfig: undefined, - pausedGamePhase: undefined, - pausedGameState: undefined, - // HOVER: Initialize hover state - playerHovers: {}, - }; - } -} -exports.MatchingGameValidator = MatchingGameValidator; -// Singleton instance -exports.matchingGameValidator = new MatchingGameValidator(); diff --git a/apps/web/src/lib/arcade/validation/index.js b/apps/web/src/lib/arcade/validation/index.js deleted file mode 100644 index 479f9d03..00000000 --- a/apps/web/src/lib/arcade/validation/index.js +++ /dev/null @@ -1,37 +0,0 @@ -"use strict"; -/** - * Game validator registry - * Maps game names to their validators - */ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.matchingGameValidator = void 0; -exports.getValidator = getValidator; -const MatchingGameValidator_1 = require("./MatchingGameValidator"); -const validators = new Map([ - ['matching', MatchingGameValidator_1.matchingGameValidator], - // Add other game validators here as they're implemented -]); -function getValidator(gameName) { - const validator = validators.get(gameName); - if (!validator) { - throw new Error(`No validator found for game: ${gameName}`); - } - return validator; -} -var MatchingGameValidator_2 = require("./MatchingGameValidator"); -Object.defineProperty(exports, "matchingGameValidator", { enumerable: true, get: function () { return MatchingGameValidator_2.matchingGameValidator; } }); -__exportStar(require("./types"), exports); diff --git a/apps/web/src/lib/arcade/validation/types.js b/apps/web/src/lib/arcade/validation/types.js deleted file mode 100644 index 82917344..00000000 --- a/apps/web/src/lib/arcade/validation/types.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -/** - * Isomorphic game validation types - * Used on both client and server for arcade session validation - */ -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/apps/web/src/lib/typst-test.js b/apps/web/src/lib/typst-test.js deleted file mode 100644 index c17ec5a8..00000000 --- a/apps/web/src/lib/typst-test.js +++ /dev/null @@ -1,154 +0,0 @@ -// Test file for typst.ts integration -// This will test if we can render our existing Typst templates using typst.ts - -import { $typst } from '@myriaddreamin/typst.ts/dist/esm/contrib/snippet.mjs' -import fs from 'fs' - -async function testBasicTypst() { - console.log('๐Ÿงช Testing basic typst.ts functionality...') - - try { - // Test basic rendering - const result = await $typst.svg({ mainContent: 'Hello, typst!' }) - console.log('โœ… Basic typst.ts working!') - console.log('๐Ÿ“ SVG length:', result.length) - return true - } catch (error) { - console.error('โŒ Basic typst.ts failed:', error) - return false - } -} - -async function testSorobanTemplate() { - console.log('๐Ÿงฎ Testing soroban template rendering...') - - try { - // Read our existing flashcards.typ template - const { FLASHCARDS_TEMPLATE, SINGLE_CARD_TEMPLATE } = require('@soroban/templates') - const flashcardsTemplate = fs.readFileSync(FLASHCARDS_TEMPLATE, 'utf-8') - const singleCardTemplate = fs.readFileSync(SINGLE_CARD_TEMPLATE, 'utf-8') - - console.log('๐Ÿ“ Templates loaded successfully') - console.log('๐Ÿ“ flashcards.typ length:', flashcardsTemplate.length) - console.log('๐Ÿ“ single-card.typ length:', singleCardTemplate.length) - - // Create a simple test document that uses our templates - const testContent = ` -${flashcardsTemplate} - -// Test drawing a simple soroban for number 5 -#draw-soroban(5, columns: auto, show-empty: false, hide-inactive: false, bead-shape: "diamond", color-scheme: "place-value", base-size: 1.0) -` - - console.log('๐ŸŽฏ Attempting to render soroban for number 5...') - - const result = await $typst.svg({ mainContent: testContent }) - - console.log('โœ… Soroban template rendering successful!') - console.log('๐Ÿ“ Generated SVG length:', result.length) - console.log('๐Ÿ” SVG preview:', `${result.substring(0, 200)}...`) - - // Save the result for inspection - fs.writeFileSync('/tmp/soroban-test.svg', result) - console.log('๐Ÿ’พ Saved test SVG to /tmp/soroban-test.svg') - - return result - } catch (error) { - console.error('โŒ Soroban template rendering failed:', error) - console.error('๐Ÿ“‹ Error details:', error.message) - return null - } -} - -async function testSingleCard() { - console.log('๐Ÿƒ Testing single card template...') - - try { - // Read templates - const { FLASHCARDS_TEMPLATE, SINGLE_CARD_TEMPLATE } = require('@soroban/templates') - const flashcardsTemplate = fs.readFileSync(FLASHCARDS_TEMPLATE, 'utf-8') - const singleCardTemplate = fs.readFileSync(SINGLE_CARD_TEMPLATE, 'utf-8') - - // Extract just the functions we need from single-card.typ and inline them - // Remove the import line and create an inlined version - const singleCardInlined = singleCardTemplate.replace( - '#import "flashcards.typ": draw-soroban', - '// Inlined draw-soroban from flashcards.typ' - ) - - // Create test content using inlined single-card template - const testContent = ` -${flashcardsTemplate} -${singleCardInlined} - -#set page( - width: 120pt, - height: 160pt, - margin: 0pt, - fill: white -) - -#set text(font: "DejaVu Sans", size: 48pt, fallback: true) - -#align(center + horizon)[ - #box( - width: 120pt - 2 * (120pt * 0.05), - height: 160pt - 2 * (160pt * 0.05) - )[ - #align(center + horizon)[ - #scale(x: 100%, y: 100%)[ - #draw-soroban( - 23, - columns: auto, - show-empty: false, - hide-inactive: false, - bead-shape: "diamond", - color-scheme: "place-value", - color-palette: "default", - base-size: 1.0 - ) - ] - ] - ] -] -` - - console.log('๐ŸŽฏ Attempting to render single card for number 23...') - - const result = await $typst.svg({ mainContent: testContent }) - - console.log('โœ… Single card rendering successful!') - console.log('๐Ÿ“ Generated SVG length:', result.length) - - // Save the result - fs.writeFileSync('/tmp/single-card-test.svg', result) - console.log('๐Ÿ’พ Saved single card test SVG to /tmp/single-card-test.svg') - - return result - } catch (error) { - console.error('โŒ Single card rendering failed:', error) - console.error('๐Ÿ“‹ Error details:', error.message) - return null - } -} - -// Run the tests -async function runTests() { - console.log('๐Ÿš€ Starting typst.ts integration tests...\n') - - const basicTest = await testBasicTypst() - if (!basicTest) { - console.log('โŒ Basic test failed, aborting further tests') - return - } - - console.log('\n') - await testSorobanTemplate() - - console.log('\n') - await testSingleCard() - - console.log('\n๐Ÿ Tests completed!') -} - -runTests().catch(console.error) diff --git a/apps/web/tsconfig.server.json b/apps/web/tsconfig.server.json index 8502d83b..660da281 100644 --- a/apps/web/tsconfig.server.json +++ b/apps/web/tsconfig.server.json @@ -3,8 +3,8 @@ "compilerOptions": { "module": "commonjs", "target": "es2020", - "outDir": ".", - "rootDir": ".", + "outDir": "./dist", + "rootDir": "./src", "noEmit": false, "incremental": false, "skipLibCheck": true,