14 KiB
14 KiB
Complexity Budget System
Overview
The complexity budget system controls problem difficulty by measuring the cognitive cost of each term in a problem. This allows us to:
- Cap difficulty for beginners (max budget) - don't overwhelm with too many hard skills per term
- Require difficulty for challenge problems (min budget) - ensure every term exercises real skills
- Personalize difficulty based on student mastery - same problem is "harder" for students still learning
Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ SESSION PLANNER │
│ ┌─────────────────────┐ ┌─────────────────────────────────────────┐ │
│ │ PlayerSkillMastery │───▶│ buildStudentSkillHistory() │ │
│ │ (from DB) │ │ ↓ │ │
│ └─────────────────────┘ │ StudentSkillHistory │ │
│ │ ↓ │ │
│ │ createSkillCostCalculator() │ │
│ │ ↓ │ │
│ │ SkillCostCalculator │──┐
│ └─────────────────────────────────────────┘ │ │
│ │ │
│ ┌─────────────────────┐ ┌─────────────────────────────────────────┐ │ │
│ │ purposeComplexity │───▶│ getComplexityBoundsForSlot() │ │ │
│ │ Bounds (config) │ │ ↓ │ │ │
│ └─────────────────────┘ │ { min?: number, max?: number } │──┼─┐
│ └─────────────────────────────────────────┘ │ │ │
└──────────────────────────────────────────────────────────────────────────┘ │ │
│ │
┌─────────────────────────────────────────────────────────────────────────┐ │ │
│ PROBLEM GENERATOR │ │ │
│ │ │ │
│ generateProblemFromConstraints(constraints, costCalculator) ◀───────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ For each candidate term: │ │
│ │ termCost = costCalculator.calculateTermCost(stepSkills) │◀─┘
│ │ │
│ │ if (termCost > maxBudget) continue // Too hard │
│ │ if (termCost < minBudget) continue // Too easy │
│ │ │
│ │ candidates.push({ term, skillsUsed, complexityCost: termCost }) │
│ └─────────────────────────────────────────────────────────────────────┘
│ │
│ ▼
│ ┌─────────────────────────────────────────────────────────────────────┐
│ │ GenerationTrace (output) │
│ │ - steps[].complexityCost │
│ │ - totalComplexityCost │
│ │ - minBudgetConstraint / budgetConstraint │
│ │ - skillMasteryContext (per-skill mastery for display) │
│ └─────────────────────────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────────────────────┘
Cost Calculation
Base Skill Complexity (Intrinsic)
| Skill Category | Base Cost | Rationale |
|---|---|---|
basic.* (direct moves) |
0 | Trivial bead movements |
fiveComplements.* |
1 | Single mental substitution |
tenComplements.* |
2 | Cross-column operation |
advanced.cascading* |
3 | Multi-column propagation |
Mastery Multipliers (Student-Specific)
| Mastery State | Multiplier | Description |
|---|---|---|
effortless |
1× | Automatic, no thought required |
fluent |
2× | Solid but needs some attention |
practicing |
3× | Currently working on, needs focus |
learning |
4× | Just introduced, maximum effort |
Effective Cost Formula
effectiveCost = baseCost × masteryMultiplier
termCost = Σ(effectiveCost for each skill in term)
Example: 5 + 9 = 14 requires tenComplements.9=10-1
- For a beginner (learning):
2 × 4 = 8 - For an expert (effortless):
2 × 1 = 2
Same problem, different cognitive load.
Configuration
Purpose-Specific Complexity Bounds
purposeComplexityBounds: {
focus: {
abacus: { min: null, max: null }, // Full range
visualization: { min: null, max: 3 }, // Cap for mental math
linear: { min: null, max: null },
},
reinforce: {
abacus: { min: null, max: null },
visualization: { min: null, max: 3 },
linear: { min: null, max: null },
},
review: {
abacus: { min: null, max: null },
visualization: { min: null, max: 3 },
linear: { min: null, max: null },
},
challenge: {
abacus: { min: 1, max: null }, // Require complement skills
visualization: { min: 1, max: null }, // No cap, require min
linear: { min: 1, max: null },
},
}
What the Bounds Mean
min: null- Any term is acceptable, including trivial+1direct additionsmin: 1- Every term must use at least one non-trivial skill (five-complement or higher)max: 3- No term can exceed cost 3 (prevents overwhelming visualization)max: null- No upper limit
Data Flow
1. Session Planning
// session-planner.ts
const skillMastery = await getAllSkillMastery(playerId);
// Build student-aware calculator
const studentHistory = buildStudentSkillHistory(skillMastery);
const costCalculator = createSkillCostCalculator(studentHistory);
// For each slot
const bounds = getComplexityBoundsForSlot(purpose, partType, config);
const slot = createSlot(index, purpose, constraints, partType, config);
slot.complexityBounds = bounds;
// Generate problem with calculator
slot.problem = generateProblemFromConstraints(slot.constraints, costCalculator);
2. Problem Generation
// problem-generator.ts
function generateProblemFromConstraints(
constraints: ProblemConstraints,
costCalculator?: SkillCostCalculator,
): GeneratedProblem {
// Pass through to generator
const problem = generateSingleProblem({
constraints: {
...generatorConstraints,
minComplexityBudgetPerTerm: constraints.minComplexityBudgetPerTerm,
maxComplexityBudgetPerTerm: constraints.maxComplexityBudgetPerTerm,
},
allowedSkills,
costCalculator,
});
}
3. Term Filtering
// problemGenerator.ts - findValidNextTermWithTrace
const termCost = costCalculator?.calculateTermCost(stepSkills);
if (termCost !== undefined) {
if (maxBudget !== undefined && termCost > maxBudget) continue;
if (minBudget !== undefined && termCost < minBudget) continue;
}
candidates.push({ term, skillsUsed, complexityCost: termCost });
4. Trace Capture
// Captured in GenerationTrace
{
steps: [
{ termAdded: 4, skillsUsed: ['fiveComplements.4=5-1'], complexityCost: 2 },
{ termAdded: 9, skillsUsed: ['tenComplements.9=10-1'], complexityCost: 4 },
],
totalComplexityCost: 6,
minBudgetConstraint: 1,
budgetConstraint: null,
skillMasteryContext: {
'fiveComplements.4=5-1': { masteryLevel: 'fluent', baseCost: 1, effectiveCost: 2 },
'tenComplements.9=10-1': { masteryLevel: 'practicing', baseCost: 2, effectiveCost: 6 },
}
}
UI Display
Purpose Tooltip (Enhanced)
The purpose badge tooltip shows complexity information:
⭐ Challenge
Harder problems - every term requires complement techniques.
┌─────────────────────────────────────────┐
│ Complexity │
│ ─────────────────────────────────────── │
│ Required: ≥1 per term Actual: 2 avg │
│ │
│ +4 (5-comp) cost: 2 [fluent] │
│ +9 (10-comp) cost: 4 [practicing] │
│ │
│ Total: 6 │
└─────────────────────────────────────────┘
Future Extensions
Mastery Recency (Not Implemented Yet)
The architecture supports adding recency-based mastery states:
Scenarios to support:
- Mastered + continuously practiced →
effortless(1×) - Mastered + not practiced recently →
rusty(2.5×) - NEW STATE - Recently mastered →
fluent(2×)
Implementation path:
-
Track
masteredAttimestamp inplayer_skill_masterytable -
Add
rustystate toMasteryStatetype and multipliers:export type MasteryState = | "effortless" | "fluent" | "rusty" | "practicing" | "learning"; export const MASTERY_MULTIPLIERS: Record<MasteryState, number> = { effortless: 1, fluent: 2, rusty: 2.5, // NEW practicing: 3, learning: 4, }; -
Enhance
dbMasteryToStateconversion:export function dbMasteryToState( dbLevel: "learning" | "practicing" | "mastered", daysSinceLastPractice?: number, daysSinceMastery?: number, ): MasteryState { if (dbLevel === "learning") return "learning"; if (dbLevel === "practicing") return "practicing"; // Mastered - but how rusty? if (daysSinceLastPractice !== undefined && daysSinceLastPractice > 14) { return "rusty"; // Mastered but neglected } if (daysSinceMastery !== undefined && daysSinceMastery > 30) { return "effortless"; // Long-term mastery + recent practice } return "fluent"; // Recently mastered }
Why this is straightforward:
SkillCostCalculatoris an interface - can swap implementationsdbMasteryToStateis the single conversion point - all recency logic goes hereStudentSkillStateinterface already has documented extension points- UI captures
skillMasteryContextin trace - automatically displays new states
Other Future Extensions
- Accuracy-based multipliers: Students with <70% accuracy on a skill get higher multiplier
- Time-based decay: Multiplier increases gradually based on days since practice
- Per-skill complexity overrides: Some skills are harder for specific students
Files Reference
| File | Purpose |
|---|---|
src/utils/skillComplexity.ts |
Base costs, mastery states, calculator factory |
src/utils/problemGenerator.ts |
Term filtering with budget enforcement |
src/lib/curriculum/problem-generator.ts |
Wrapper that passes calculator through |
src/lib/curriculum/session-planner.ts |
Builds calculator, sets purpose bounds |
src/db/schema/session-plans.ts |
Type definitions, config defaults |
src/components/practice/ActiveSession.tsx |
UI display of complexity data |
Testing
Verify Budget Enforcement
// Existing test file: src/utils/__tests__/problemGenerator.budget.test.ts
describe('complexity budget', () => {
it('rejects terms exceeding max budget', () => { ... })
it('rejects terms below min budget', () => { ... }) // NEW
it('uses student mastery to calculate cost', () => { ... })
})
Verify UI Display
Check Storybook stories for PurposeBadge with complexity data visible.