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 <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2026-01-13 10:48:51 -06:00
parent dda041e014
commit c182756b80
1 changed files with 545 additions and 0 deletions

View File

@ -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<number> {
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,