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 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 | 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 30x 30x 30x 30x 30x 30x 30x 30x 30x 194464x 194464x 194464x 30x 30x 30x 30x 30x 30x 30x 30x 30x 187472x 187472x 187472x 187472x 187472x 184034x 184034x 3438x 3438x 3438x 187472x 30x 30x 30x 187472x 187472x 187472x 187472x 30x 30x 152224x 180480x 152224x 152224x 30x 30x 6992x 6992x 30x 30x 30x 30x 30x 30x 30x 30x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 3x 3x 3x 3x 3x 3x 2x 2x 2x 2x 2x 2x 2x 2x | /**
* Skill Complexity Budget System
*
* Cost = baseCost × rotationMultiplier
*
* This architecture separates:
* 1. BASE_SKILL_COMPLEXITY - intrinsic mechanical complexity (constant)
* 2. SkillCostCalculator - student-aware cost calculation (pluggable)
*
* TUNING: All tunable constants are in @/lib/curriculum/config/
*/
// Import tunable constants from centralized config
import {
BASE_SKILL_COMPLEXITY,
calculateBktMultiplier,
DEFAULT_COMPLEXITY_BUDGETS,
getBaseComplexity,
isBktConfident,
ROTATION_MULTIPLIERS,
type ProblemGenerationMode,
} from '@/lib/curriculum/config'
import type { SkillBktResult } from '@/lib/curriculum/bkt'
// Re-export for backwards compatibility
export {
BASE_SKILL_COMPLEXITY,
DEFAULT_COMPLEXITY_BUDGETS,
getBaseComplexity,
ROTATION_MULTIPLIERS,
}
/**
* Information about a student's relationship with a skill
*/
export interface StudentSkillState {
skillId: string
/** Whether skill is in the student's active practice rotation */
isPracticing: boolean
/** Practice level: 'none' | 'abacus' | 'visual' */
practiceLevel: import('@/db/schema/player-skill-mastery').PracticeLevel
}
/**
* Student skill history - all skills and their states
*/
export interface StudentSkillHistory {
skills: Record<string, StudentSkillState>
}
/**
* Options for creating a skill cost calculator
*/
export interface SkillCostCalculatorOptions {
/**
* BKT results keyed by skillId.
* Used for skill targeting in all adaptive modes.
* Used for cost calculation only in 'adaptive-bkt' mode.
*/
bktResults?: Map<string, SkillBktResult>
/**
* Problem generation mode:
* - 'classic': No BKT targeting, discrete cost multipliers (inRotation/outOfRotation)
* - 'adaptive': BKT skill targeting, discrete cost multipliers
* - 'adaptive-bkt': BKT skill targeting, BKT-based continuous multipliers (default)
*/
mode?: ProblemGenerationMode
}
/**
* Interface for calculating skill cost for a student
* This abstraction allows swapping implementations later
*/
export interface SkillCostCalculator {
/**
* Calculate the effective cost of a skill for this student
*/
calculateSkillCost(skillId: string): number
/**
* Calculate total cost for a set of skills (a term)
*/
calculateTermCost(skillIds: string[]): number
/**
* Check if skill is in student's practice rotation (useful for debug UI)
*/
getIsPracticing(skillId: string): boolean
/**
* Get the raw multiplier used for a skill (useful for debug UI)
*/
getMultiplier(skillId: string): number
/**
* Get BKT result for a skill if available (for transparency)
*/
getBktResult(skillId: string): SkillBktResult | undefined
/**
* Get the mode being used for this calculator
*/
getMode(): ProblemGenerationMode
}
// =============================================================================
// Base Complexity (imported from config)
// =============================================================================
// See @/lib/curriculum/config/skill-costs.ts for BASE_SKILL_COMPLEXITY and getBaseComplexity
// =============================================================================
// Default Implementation: Practice Rotation Based Calculator
// =============================================================================
/**
* Creates a skill cost calculator based on student's skill history.
*
* Cost calculation depends on mode:
* - 'classic' / 'adaptive': Use discrete multipliers (inRotation=3, outOfRotation=4)
* - 'adaptive-bkt': Use BKT P(known) for continuous multipliers (default)
*
* Note: In 'adaptive' mode, BKT is used for skill TARGETING but not cost.
* In 'adaptive-bkt' mode, BKT is used for both targeting AND cost.
*
* @param studentHistory - Student's skill history for discrete multipliers fallback
* @param options - Optional BKT results (for targeting/cost) and mode selection
*/
export function createSkillCostCalculator(
studentHistory: StudentSkillHistory,
options: SkillCostCalculatorOptions = {}
): SkillCostCalculator {
const { bktResults, mode = 'adaptive' } = options
/**
* Check if a skill is in the student's practice rotation.
*/
function checkIsPracticing(skillId: string): boolean {
const skillState = studentHistory.skills[skillId]
return skillState?.isPracticing ?? false
}
/**
* Get multiplier for a skill.
*
* Uses BKT P(known) for continuous multipliers when confident.
* Falls back to discrete multipliers based on isPracticing status
* when BKT confidence is insufficient.
*/
function getMultiplierForSkill(skillId: string): number {
// Use BKT for cost calculation if confident
if (bktResults) {
const bktResult = bktResults.get(skillId)
if (bktResult && isBktConfident(bktResult.confidence)) {
return calculateBktMultiplier(bktResult.pKnown)
}
}
// Fallback: use discrete multiplier based on isPracticing status
if (!checkIsPracticing(skillId)) {
return ROTATION_MULTIPLIERS.outOfRotation
}
// For practicing skills without confident BKT, use inRotation multiplier (3.0)
// This represents "developing/learning" - a reasonable default
return ROTATION_MULTIPLIERS.inRotation
}
return {
calculateSkillCost(skillId: string): number {
const baseCost = getBaseComplexity(skillId)
const multiplier = getMultiplierForSkill(skillId)
return baseCost * multiplier
},
calculateTermCost(skillIds: string[]): number {
return skillIds.reduce((total, skillId) => {
return total + this.calculateSkillCost(skillId)
}, 0)
},
getIsPracticing(skillId: string): boolean {
return checkIsPracticing(skillId)
},
getMultiplier(skillId: string): number {
return getMultiplierForSkill(skillId)
},
getBktResult(skillId: string): SkillBktResult | undefined {
return bktResults?.get(skillId)
},
getMode(): ProblemGenerationMode {
return mode
},
}
}
/**
* Calculate the maximum effective skill cost for a student.
*
* This is used to set dynamic budgets - e.g., visualization max budget
* should be at least as high as the student's most expensive skill,
* so that skill can appear in visualization practice.
*
* @param calculator - The student's cost calculator
* @param skillIds - List of skill IDs to check (e.g., all mastered skills)
* @returns The maximum effective cost across all provided skills
*/
export function calculateMaxSkillCost(calculator: SkillCostCalculator, skillIds: string[]): number {
if (skillIds.length === 0) {
return 0
}
return Math.max(...skillIds.map((id) => calculator.calculateSkillCost(id)))
}
// =============================================================================
// Utility Functions
// =============================================================================
/**
* Database record shape for skill practice status
*
* NOTE: attempts/correct/consecutiveCorrect were removed.
* Stats are now computed on-the-fly from session results.
*/
export interface DbSkillRecord {
skillId: string
isPracticing: boolean
practiceLevel: import('@/db/schema/player-skill-mastery').PracticeLevel
lastPracticedAt?: Date | null
}
/**
* Build StudentSkillHistory from database records
*/
export function buildStudentSkillHistoryFromRecords(
dbRecords: DbSkillRecord[]
): StudentSkillHistory {
const skills: Record<string, StudentSkillState> = {}
for (const record of dbRecords) {
skills[record.skillId] = {
skillId: record.skillId,
isPracticing: record.isPracticing,
practiceLevel: record.practiceLevel,
}
}
return { skills }
}
// =============================================================================
// Budget Defaults (imported from config)
// =============================================================================
// See @/lib/curriculum/config/complexity-budgets.ts for DEFAULT_COMPLEXITY_BUDGETS
|