Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | 'use client' import { createContext, useContext, useMemo } from 'react' import type { ReactNode } from 'react' import { CONSTANT_IDS } from '@/components/toys/number-line/talkToNumber/explorationRegistry' import { useGameMode } from '@/contexts/GameModeContext' import { usePlayerGameHistory } from '@/hooks/useGameResults' /** All constant IDs that have demos + narration available */ const DEMO_CONSTANT_IDS = [...CONSTANT_IDS] function pickRandomConstant(): string { return DEMO_CONSTANT_IDS[Math.floor(Math.random() * DEMO_CONSTANT_IDS.length)] } /** * Pick a constant the student has watched least. * Counts plays per constant from game history, then picks randomly * from the set with the lowest count (including 0 for never-watched). */ function pickBalancedConstant( history: Array<{ fullReport?: { leaderboardEntry?: { difficulty?: string } } | null }> | undefined ): string { // Count how many times each constant has been watched const counts = new Map<string, number>() for (const id of DEMO_CONSTANT_IDS) { counts.set(id, 0) } if (history) { for (const entry of history) { const constantId = entry.fullReport?.leaderboardEntry?.difficulty if (constantId && counts.has(constantId)) { counts.set(constantId, (counts.get(constantId) ?? 0) + 1) } } } // Find the minimum count let minCount = Infinity for (const count of counts.values()) { if (count < minCount) minCount = count } // Collect all constants with that minimum count const leastWatched: string[] = [] for (const [id, count] of counts) { if (count === minCount) leastWatched.push(id) } // Pick randomly from the least-watched set return leastWatched[Math.floor(Math.random() * leastWatched.length)] } function resolveConstantId( raw: unknown, history: Array<{ fullReport?: { leaderboardEntry?: { difficulty?: string } } | null }> | undefined ): string { if (typeof raw === 'string') { if (raw === 'balance') return pickBalancedConstant(history) if (raw !== 'random' && CONSTANT_IDS.has(raw)) return raw } return pickRandomConstant() } const ConstantExplorerContext = createContext<{ constantId: string }>({ constantId: 'pi', }) export function useConstantExplorerConfig() { return useContext(ConstantExplorerContext) } /** * Provider for the constant-explorer "game". * * Reads constantId from roomData.gameConfig (set by the teacher in the * start-practice modal), resolving 'random'/'balance' to a concrete constant. * For 'balance', queries the player's game history to find least-watched constants. */ export function ConstantExplorerProvider({ children }: { children: ReactNode }) { const { getActivePlayers, roomData } = useGameMode() // Get player ID for history query const playerId = useMemo(() => { const players = getActivePlayers() return players[0]?.id ?? null }, [getActivePlayers]) // Query this player's constant-explorer history (for balance mode) const { data: historyData } = usePlayerGameHistory(playerId, { gameName: 'constant-explorer', limit: 200, // Enough to cover many sessions }) const value = useMemo(() => { const gameConfig = roomData?.gameConfig as Record<string, unknown> | null | undefined const explorerConfig = gameConfig?.['constant-explorer'] as Record<string, unknown> | undefined const constantId = resolveConstantId( explorerConfig?.constantId, historyData?.history as | Array<{ fullReport?: { leaderboardEntry?: { difficulty?: string } } | null }> | undefined ) return { constantId } // eslint-disable-next-line react-hooks/exhaustive-deps -- resolve once when history loads }, [roomData?.gameConfig, historyData]) return ( <ConstantExplorerContext.Provider value={value}>{children}</ConstantExplorerContext.Provider> ) } |