From c182756b80be23d3f54e9ff377fe8a372797224c Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Tue, 13 Jan 2026 10:48:51 -0600 Subject: [PATCH] feat(seeding): add game results generation to test student profiles - Add gameHistory configs to all 21 test student profiles - Implement generateGameResults function to create scoreboard data - Each profile has appropriate game history matching their characteristics: - Struggling students get low scores (35-45) - Developing students get medium scores (55-75) - Strong students get high scores (78-95) - Add accuracyMultiplier to TuningAdjustment interface (fixes TS error) Co-Authored-By: Claude Opus 4.5 --- apps/web/scripts/seedTestStudents.ts | 545 +++++++++++++++++++++++++++ 1 file changed, 545 insertions(+) diff --git a/apps/web/scripts/seedTestStudents.ts b/apps/web/scripts/seedTestStudents.ts index 60aa5cbb..650a6e3b 100644 --- a/apps/web/scripts/seedTestStudents.ts +++ b/apps/web/scripts/seedTestStudents.ts @@ -126,6 +126,7 @@ import { type GeneratedProblem as GenProblem, } from '../src/utils/problemGenerator' import { createEmptySkillSet, type SkillSet } from '../src/types/tutorial' +import type { GameResultsReport } from '../src/lib/arcade/game-sdk/types' // ============================================================================= // BKT Simulation Utilities @@ -557,12 +558,34 @@ interface SuccessCriteria { interface TuningAdjustment { /** Skill ID to adjust (or 'all' for all skills) */ skillId: string | 'all' + /** Multiply accuracy by this factor */ + accuracyMultiplier?: number /** Add this many problems */ problemsAdd?: number /** Multiply problems by this factor */ problemsMultiplier?: number } +/** + * Configuration for seeding game results (scoreboard data) + */ +interface GameResultConfig { + /** Which game: 'matching', 'card-sorting', 'complement-race', etc. */ + gameName: string + /** Human-readable display name */ + displayName: string + /** Game icon emoji */ + icon: string + /** Category for leaderboard grouping */ + category: 'puzzle' | 'memory' | 'speed' | 'strategy' | 'geography' + /** Target score range (0-100), actual will vary within ±5 */ + targetScore: number + /** Number of games to seed */ + gameCount: number + /** Days ago spread (games will be distributed over this period) */ + spreadDays?: number +} + /** Profile category for CLI filtering */ type ProfileCategory = 'bkt' | 'session' | 'edge' @@ -606,6 +629,11 @@ interface TestStudentProfile { * Default: 30 */ sessionSpreadDays?: number + /** + * Game results to seed for scoreboard testing. + * Each entry creates multiple game result records. + */ + gameHistory?: GameResultConfig[] } // ============================================================================= @@ -692,6 +720,18 @@ Use this student to test how the UI handles intervention alerts for foundational problems: 12, }, ], + // Game history: Struggling student - low scores, few games + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 35, // Struggling + gameCount: 3, + spreadDays: 14, + }, + ], // Tuning: Need at least 2 weak skills successCriteria: { minWeak: 2 }, tuningAdjustments: [{ skillId: 'all', problemsAdd: 10 }], @@ -750,6 +790,27 @@ Use this student to test targeted intervention recommendations.`, problems: 18, }, ], + // Game history: Mixed results - good at some games, struggling with blocker skill + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 65, // Decent but not great + gameCount: 5, + spreadDays: 21, + }, + { + gameName: 'card-sorting', + displayName: 'Card Sorting', + icon: '🔢', + category: 'puzzle', + targetScore: 70, // Better at puzzles + gameCount: 4, + spreadDays: 21, + }, + ], }, { name: '🟢 Progressing Nicely', @@ -808,6 +869,27 @@ Use this student to verify: ], // Success criteria: Need at least 1 developing to prove the system works successCriteria: { minDeveloping: 1 }, + // Game history: Healthy player - good scores, regular game play + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 75, // Good scores + gameCount: 8, + spreadDays: 30, + }, + { + gameName: 'card-sorting', + displayName: 'Card Sorting', + icon: '🔢', + category: 'puzzle', + targetScore: 72, + gameCount: 6, + spreadDays: 30, + }, + ], }, { name: '⭐ Ready to Level Up', @@ -872,6 +954,36 @@ Use this student to test: problems: 18, }, ], + // Game history: Excellent player - high scores, ready to advance + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 88, // Excellent scores + gameCount: 12, + spreadDays: 45, + }, + { + gameName: 'card-sorting', + displayName: 'Card Sorting', + icon: '🔢', + category: 'puzzle', + targetScore: 85, + gameCount: 10, + spreadDays: 45, + }, + { + gameName: 'complement-race', + displayName: 'Complement Race', + icon: '🏁', + category: 'speed', + targetScore: 82, + gameCount: 8, + spreadDays: 45, + }, + ], }, { name: '🚀 Overdue for Promotion', @@ -996,6 +1108,45 @@ Use this student to test: problems: 18, }, ], + // Game history: Top-tier player - highest scores, extensive game history + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 95, // Near-perfect + gameCount: 25, + spreadDays: 90, + }, + { + gameName: 'card-sorting', + displayName: 'Card Sorting', + icon: '🔢', + category: 'puzzle', + targetScore: 92, + gameCount: 20, + spreadDays: 90, + }, + { + gameName: 'complement-race', + displayName: 'Complement Race', + icon: '🏁', + category: 'speed', + targetScore: 90, + gameCount: 18, + spreadDays: 90, + }, + { + gameName: 'memory-quiz', + displayName: 'Memory Quiz', + icon: '🧠', + category: 'memory', + targetScore: 88, + gameCount: 15, + spreadDays: 90, + }, + ], }, // ============================================================================= @@ -1063,6 +1214,18 @@ Use this to test the remediation UI in dashboard and modal.`, problems: 18, }, ], + // Game history: Struggling student - low scores, few games + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 40, // Struggling + gameCount: 3, + spreadDays: 14, + }, + ], }, { name: '📚 Progression Tutorial Test', @@ -1126,6 +1289,27 @@ Use this to test the progression UI and tutorial gate flow.`, problems: 20, }, ], + // Game history: Good player learning new skills - solid scores + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 78, + gameCount: 6, + spreadDays: 21, + }, + { + gameName: 'card-sorting', + displayName: 'Card Sorting', + icon: '🔢', + category: 'puzzle', + targetScore: 75, + gameCount: 5, + spreadDays: 21, + }, + ], }, { name: '🚀 Progression Ready Test', @@ -1189,6 +1373,27 @@ Use this to test the progression UI when tutorial is already satisfied.`, problems: 20, }, ], + // Game history: Strong player ready for more - consistent scores + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 80, + gameCount: 7, + spreadDays: 28, + }, + { + gameName: 'card-sorting', + displayName: 'Card Sorting', + icon: '🔢', + category: 'puzzle', + targetScore: 78, + gameCount: 5, + spreadDays: 28, + }, + ], }, { name: '🏆 Maintenance Test', @@ -1276,6 +1481,36 @@ Use this to test the maintenance mode UI in dashboard and modal.`, problems: 20, }, ], + // Game history: Excellent all-around player - high scores across many games + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 90, + gameCount: 15, + spreadDays: 60, + }, + { + gameName: 'card-sorting', + displayName: 'Card Sorting', + icon: '🔢', + category: 'puzzle', + targetScore: 88, + gameCount: 12, + spreadDays: 60, + }, + { + gameName: 'complement-race', + displayName: 'Complement Race', + icon: '🏁', + category: 'speed', + targetScore: 85, + gameCount: 10, + spreadDays: 60, + }, + ], }, // ============================================================================= @@ -1303,6 +1538,7 @@ This tests the empty state handling in the dashboard. Use this to verify the dashboard handles zero practicing skills gracefully.`, skillHistory: [], // No history at all + // NO gameHistory - intentionally empty for testing empty states }, { name: '🔢 Single Skill Only', @@ -1330,6 +1566,18 @@ Use this to verify the dashboard handles single-skill students correctly.`, problems: 12, }, ], + // Game history: Just getting started - few games, developing scores + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 55, // Still learning + gameCount: 2, + spreadDays: 7, + }, + ], }, { name: '📊 High Volume Learner', @@ -1424,6 +1672,45 @@ Use this to verify: problems: 12, }, ], + // Game history: Lots of gameplay - many games across all types + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 82, + gameCount: 30, + spreadDays: 90, + }, + { + gameName: 'card-sorting', + displayName: 'Card Sorting', + icon: '🔢', + category: 'puzzle', + targetScore: 80, + gameCount: 25, + spreadDays: 90, + }, + { + gameName: 'complement-race', + displayName: 'Complement Race', + icon: '🏁', + category: 'speed', + targetScore: 78, + gameCount: 20, + spreadDays: 90, + }, + { + gameName: 'memory-quiz', + displayName: 'Memory Quiz', + icon: '🧠', + category: 'memory', + targetScore: 75, + gameCount: 15, + spreadDays: 90, + }, + ], }, { name: '⚖️ Multi-Weak Remediation', @@ -1500,6 +1787,27 @@ Complements 🔴 Multi-Skill Deficient (which has only 2 weak).`, ], // Need at least 2 weak for remediation testing successCriteria: { minWeak: 2 }, + // Game history: Struggling student - low scores across games + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 42, + gameCount: 5, + spreadDays: 30, + }, + { + gameName: 'card-sorting', + displayName: 'Card Sorting', + icon: '🔢', + category: 'puzzle', + targetScore: 38, + gameCount: 4, + spreadDays: 30, + }, + ], }, { name: '🕰️ Stale Skills Test', @@ -1583,6 +1891,18 @@ Use this to test: ageDays: 45, }, ], + // Game history: Some old games to match staleness theme + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 70, + gameCount: 8, + spreadDays: 60, // Games spread over 60 days (some stale) + }, + ], }, { name: '💥 NaN Stress Test', @@ -1679,6 +1999,18 @@ Use this profile to verify: simulateLegacyData: true, }, ], + // Game history: Random mix for stress testing + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 60, // Middle-of-the-road + gameCount: 6, + spreadDays: 45, + }, + ], }, { name: '🧊 Forgotten Weaknesses', @@ -1772,6 +2104,18 @@ are both - the forgotten weaknesses that need urgent attention.`, ], // Need at least 3 weak for this profile successCriteria: { minWeak: 3 }, + // Game history: Low scores from struggling + old games + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 45, + gameCount: 4, + spreadDays: 45, // Some games are old + }, + ], }, // ============================================================================= @@ -1828,6 +2172,18 @@ Use this to verify the chart gracefully handles the minimum history case.`, problems: 3, }, ], + // Game history: Minimal - just started + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 50, + gameCount: 1, + spreadDays: 1, + }, + ], }, { name: '📊 Chart: 2 Sessions (Min)', @@ -1878,6 +2234,18 @@ Use this to verify the chart renders correctly at the minimum viable history.`, problems: 5, }, ], + // Game history: 2 games to match 2 sessions + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 55, + gameCount: 2, + spreadDays: 7, + }, + ], }, { name: '📈 Chart: 25 Sessions', @@ -1945,6 +2313,27 @@ Use this to verify: problems: 20, }, ], + // Game history: Moderate amount of games over 2 months + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 75, + gameCount: 15, + spreadDays: 60, + }, + { + gameName: 'card-sorting', + displayName: 'Card Sorting', + icon: '🔢', + category: 'puzzle', + targetScore: 72, + gameCount: 10, + spreadDays: 60, + }, + ], }, { name: '🏋️ Chart: 150 Sessions', @@ -2069,6 +2458,45 @@ Use this to verify: problems: 35, }, ], + // Game history: LOTS of games over 6 months to match 150 sessions + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 88, + gameCount: 75, + spreadDays: 180, + }, + { + gameName: 'card-sorting', + displayName: 'Card Sorting', + icon: '🔢', + category: 'puzzle', + targetScore: 85, + gameCount: 50, + spreadDays: 180, + }, + { + gameName: 'complement-race', + displayName: 'Complement Race', + icon: '🏁', + category: 'speed', + targetScore: 82, + gameCount: 40, + spreadDays: 180, + }, + { + gameName: 'memory-quiz', + displayName: 'Memory Quiz', + icon: '🧠', + category: 'memory', + targetScore: 80, + gameCount: 30, + spreadDays: 180, + }, + ], }, { name: '🌈 Chart: Dramatic Progress', @@ -2136,6 +2564,27 @@ Use this to verify: problems: 10, }, ], + // Game history: Showing improvement - scores getting better over time + gameHistory: [ + { + gameName: 'matching', + displayName: 'Matching Pairs', + icon: '⚔️', + category: 'memory', + targetScore: 78, // Good but still improving + gameCount: 12, + spreadDays: 45, + }, + { + gameName: 'card-sorting', + displayName: 'Card Sorting', + icon: '🔢', + category: 'puzzle', + targetScore: 75, + gameCount: 8, + spreadDays: 45, + }, + ], }, ] @@ -2769,6 +3218,96 @@ async function createTestStudent( return { playerId, classifications, bktResult } } +/** + * Generate game results for scoreboard testing. + * Creates realistic game history based on the profile's gameHistory configs. + */ +async function generateGameResults(playerId: string, profile: TestStudentProfile): Promise { + if (!profile.gameHistory || profile.gameHistory.length === 0) { + return 0 + } + + let totalGames = 0 + const now = Date.now() + + for (const gameConfig of profile.gameHistory) { + const spreadMs = (gameConfig.spreadDays ?? 30) * 24 * 60 * 60 * 1000 + + for (let i = 0; i < gameConfig.gameCount; i++) { + // Spread games evenly over the time period + const gameAgeMs = (spreadMs * i) / Math.max(1, gameConfig.gameCount - 1) + const playedAt = new Date(now - spreadMs + gameAgeMs) + + // Add some variation to scores (±5 from target) + const scoreVariation = (Math.random() - 0.5) * 10 + const normalizedScore = Math.max(0, Math.min(100, gameConfig.targetScore + scoreVariation)) + + // Calculate accuracy based on score (roughly correlate) + const accuracy = normalizedScore * (0.8 + Math.random() * 0.2) + + // Determine difficulty based on score + let difficulty: 'easy' | 'medium' | 'hard' | 'expert' + if (normalizedScore >= 85) difficulty = 'hard' + else if (normalizedScore >= 70) difficulty = 'medium' + else difficulty = 'easy' + + // Generate duration (2-10 minutes) + const durationMs = (120 + Math.random() * 480) * 1000 + + // Create a minimal fullReport for display + const fullReport: GameResultsReport = { + gameName: gameConfig.gameName, + gameDisplayName: gameConfig.displayName, + gameIcon: gameConfig.icon, + durationMs, + completedNormally: true, + startedAt: playedAt.getTime() - durationMs, + endedAt: playedAt.getTime(), + gameMode: 'single-player', + playerCount: 1, + playerResults: [ + { + playerId, + playerName: profile.name.replace(/^[^\s]+\s*/, ''), // Remove emoji prefix + playerEmoji: profile.emoji, + userId: '', + score: Math.round(normalizedScore), + rank: 1, + }, + ], + leaderboardEntry: { + normalizedScore, + category: gameConfig.category, + difficulty, + }, + headline: + normalizedScore >= 90 ? 'Excellent!' : normalizedScore >= 70 ? 'Great Job!' : 'Good Try!', + resultTheme: normalizedScore >= 90 ? 'success' : normalizedScore >= 70 ? 'good' : 'neutral', + } + + await db.insert(schema.gameResults).values({ + playerId, + gameName: gameConfig.gameName, + gameDisplayName: gameConfig.displayName, + gameIcon: gameConfig.icon, + sessionType: 'practice-break', + normalizedScore, + rawScore: Math.round(normalizedScore), + accuracy, + category: gameConfig.category, + difficulty, + durationMs: Math.round(durationMs), + playedAt, + fullReport, + }) + + totalGames++ + } + } + + return totalGames +} + /** * Create a test student with iterative tuning (up to maxRounds) */ @@ -2841,6 +3380,12 @@ async function createTestStudentWithTuning( .set({ notes: fullNotes }) .where(eq(schema.players.id, result!.playerId)) + // Generate game results for this student + const gameCount = await generateGameResults(result!.playerId, profile) + if (gameCount > 0) { + console.log(` Generated ${gameCount} game results`) + } + return { playerId: result!.playerId, classifications: result!.classifications,