diff --git a/apps/web/.claude/PROBLEM_GENERATION.md b/apps/web/.claude/PROBLEM_GENERATION.md new file mode 100644 index 00000000..02b65d6d --- /dev/null +++ b/apps/web/.claude/PROBLEM_GENERATION.md @@ -0,0 +1,353 @@ +# Problem Generation System - Claude Code Reference + +## Quick Reference for AI Development + +This document provides quick-reference information about the worksheet problem generation system for Claude Code and developers working on this codebase. + +--- + +## File Locations + +### Core Logic +- **`src/app/create/worksheets/problemGenerator.ts`** - All generation algorithms (addition, subtraction, mixed) +- **`src/app/create/worksheets/utils/validateProblemSpace.ts`** - Space estimation and validation +- **`src/app/create/worksheets/PROBLEM_GENERATION_ARCHITECTURE.md`** - Complete technical documentation + +### UI Components +- **`components/worksheet-preview/WorksheetPreviewContext.tsx`** - Validation triggering and state +- **`components/worksheet-preview/DuplicateWarningBanner.tsx`** - Warning display UI + +--- + +## Two Generation Strategies + +### When to Use Each + +```typescript +const estimatedSpace = estimateUniqueProblemSpace(digitRange, pAnyStart, operator) + +if (estimatedSpace < 10000) { + // STRATEGY 1: Generate-All + Shuffle + // Zero retries, guaranteed coverage, deterministic +} else { + // STRATEGY 2: Retry-Based + // Random generation, allows some duplicates after 100 retries +} +``` + +### Strategy 1: Generate-All (Small Spaces) + +**Examples:** +- 1-digit 100% regrouping: 45 unique problems +- 2-digit mixed regrouping: ~4,000 unique problems + +**Key behavior:** +```typescript +// Non-interpolate: Shuffle and cycle +problems[0-44] = first shuffle +problems[45-89] = second shuffle (same order) +problems[90+] = third shuffle... + +// Interpolate: Sort by difficulty, then cycle maintaining progression +seen.clear() when exhausted // Start new cycle +``` + +**Location:** `problemGenerator.ts:381-503` + +### Strategy 2: Retry-Based (Large Spaces) + +**Examples:** +- 3-digit problems: ~400,000 unique problems +- 4-5 digit problems: millions of unique problems + +**Key behavior:** +```typescript +let tries = 0 +while (tries++ < 100 && !unique) { + problem = generate() + if (!seen.has(key)) { + seen.add(key) + break + } +} +// Allow duplicate if still not unique after 100 tries +``` + +**Location:** `problemGenerator.ts:506-595` + +--- + +## Critical Edge Cases + +### 1. Single-Digit 100% Regrouping + +**Problem:** Only 45 unique problems exist! + +```typescript +// 1-digit addition where a + b >= 10 +// a=0: none, a=1: 9 (1+9), a=2: 8 (2+8,2+9), ..., a=9: 1 (9+1) +// Total: 0+9+8+7+6+5+4+3+2+1 = 45 +``` + +**User impact:** +- Requesting 100 problems → 55 duplicates guaranteed +- Warning banner shown: "Single-digit problems (1-9) with 100% regrouping have very few unique combinations!" + +**Code:** `validateProblemSpace.ts:56-64` (exact count), `validateProblemSpace.ts:175-179` (warning) + +### 2. Mixed Mode with Mastery + +**Problem:** Cannot validate combined space with separate configs + +```typescript +// Addition skill: {digitRange: {min:2, max:2}, pAnyStart: 0.3} +// Subtraction skill: {digitRange: {min:1, max:2}, pAnyStart: 0.7} +// Combined estimation is complex and potentially misleading +``` + +**Solution:** Skip validation entirely + +```typescript +// WorksheetPreviewContext.tsx:53-56 +if (mode === 'mastery' && operator === 'mixed') { + setWarnings([]) + return +} +``` + +### 3. Subtraction Multiple Borrowing Impossibility + +**Mathematical fact:** 1-2 digit subtraction cannot have 2+ borrows + +```typescript +// generateBothBorrow() - problemGenerator.ts:802-804 +if (maxDigits <= 2) { + return generateOnesOnlyBorrow(rand, minDigits, maxDigits) // Fallback +} +``` + +### 4. Borrowing Across Zeros + +**Example:** `1000 - 1` requires 3 borrow operations + +``` +ones: 0 < 1, borrow from tens +tens: 0 (zero!), borrow from hundreds (+1 for crossing zero) +hundreds: 0, borrow from thousands (+1 for crossing zero) +thousands: 1, decrement +Total: 3 borrows +``` + +**Code:** `countBorrows()` - `problemGenerator.ts:740-782` + +--- + +## Debugging Commands + +### Check Server Logs for Generation + +```bash +# Look for these log patterns: +[ADD GEN] Starting: 100 problems, digitRange: 1-1, pAnyStart: 1, pAllStart: 0 +[ADD GEN] Estimated unique problem space: 45 (requesting 100) +[ADD GEN] Using generate-all + shuffle (space < 10000, interpolate=true) +[ADD GEN] Exhausted all 45 unique problems at position 45. Starting cycle 2. +[ADD GEN] Complete: 100 problems in 8ms (0 retries, generate-all with progressive difficulty, 1 cycles) +``` + +### Test Problem Space Estimation + +```typescript +import { estimateUniqueProblemSpace } from './utils/validateProblemSpace' + +// 1-digit 100% regrouping +const space1 = estimateUniqueProblemSpace({min: 1, max: 1}, 1.0, 'addition') +console.log(space1) // Expected: 45 + +// 2-digit mixed +const space2 = estimateUniqueProblemSpace({min: 2, max: 2}, 0.5, 'addition') +console.log(space2) // Expected: ~4000 +``` + +### Verify Cycling Behavior + +```typescript +// Generate 100 problems from 45-problem space +const problems = generateProblems(100, 1.0, 0, false, 12345, {min: 1, max: 1}) + +// Check that problems 0-44 and 45-89 are identical +const cycle1 = problems.slice(0, 45) +const cycle2 = problems.slice(45, 90) +console.log('Cycles match:', + cycle1.every((p, i) => p.a === cycle2[i].a && p.b === cycle2[i].b) +) // Expected: true +``` + +--- + +## Common Modifications + +### Adding a New Difficulty Category + +**Current categories:** `non`, `onesOnly`, `both` + +**To add a new category:** + +1. Add category type to `ProblemCategory` in `types.ts` +2. Create generator function (e.g., `generateThreePlus()`) +3. Update probability sampling in retry-based strategy +4. Update `countRegroupingOperations()` or `countBorrows()` for difficulty scoring +5. Update `generateAllAdditionProblems()` or `generateAllSubtractionProblems()` filtering + +### Changing Strategy Threshold + +**Current:** 10,000 unique problems + +```typescript +const THRESHOLD = 10000 +if (estimatedSpace < THRESHOLD) { + // Generate-all +} else { + // Retry-based +} +``` + +**To change:** +- Increase for better uniqueness guarantees (more generate-all usage) +- Decrease for better performance on larger spaces (more retry-based usage) + +**Trade-off:** Generate-all is O(n²) enumeration, slow for large spaces + +### Adjusting Retry Limit + +**Current:** 100 retries per problem + +```typescript +let tries = 0 +while (tries++ < 100 && !ok) { + // Generate and check uniqueness +} +``` + +**To change:** +- Increase for better uniqueness (slower generation) +- Decrease for faster generation (more duplicates) + +**Historical note:** Was 3000 retries, reduced to 100 for performance +- 100 problems × 3000 retries = 300,000 iterations (seconds) +- 100 problems × 100 retries = 10,000 iterations (milliseconds) + +--- + +## Problem Space Estimation Formulas + +### Exact Counting (1-Digit) + +```typescript +// Addition regrouping (a + b >= 10) +for (let a = 0; a <= 9; a++) { + for (let b = 0; b <= 9; b++) { + if (a + b >= 10) count++ + } +} +// Result: 45 + +// Addition non-regrouping +// Result: 100 - 45 = 55 +``` + +### Heuristic Estimation (2+ Digits) + +```typescript +numbersPerDigitCount = digits === 1 ? 10 : 9 * 10^(digits-1) + +// Addition +pairsForDigits = numbersPerDigitCount * numbersPerDigitCount +regroupFactor = pAnyStart > 0.8 ? 0.45 : pAnyStart > 0.5 ? 0.5 : 0.7 +totalSpace += pairsForDigits * regroupFactor + +// Subtraction (only minuend >= subtrahend valid) +pairsForDigits = (numbersPerDigitCount * numbersPerDigitCount) / 2 +borrowFactor = pAnyStart > 0.8 ? 0.35 : pAnyStart > 0.5 ? 0.5 : 0.7 +totalSpace += pairsForDigits * borrowFactor +``` + +**Why these factors?** +- High regrouping requirement → Fewer valid problems +- Medium regrouping → About half +- Low/mixed → Most problems valid + +--- + +## User Warning Levels + +```typescript +const ratio = requestedProblems / estimatedSpace + +if (ratio < 0.3) { + duplicateRisk = 'none' // No warning shown +} else if (ratio < 0.5) { + duplicateRisk = 'low' // "Some duplicates may occur" +} else if (ratio < 0.8) { + duplicateRisk = 'medium' // "Expect moderate duplicates" + suggestions +} else if (ratio < 1.5) { + duplicateRisk = 'high' // "High duplicate risk!" + recommendations +} else { + duplicateRisk = 'extreme' // "Mostly duplicate problems" + strong warnings +} +``` + +**Location:** `validateProblemSpace.ts:130-172` + +--- + +## Testing Checklist + +When modifying problem generation: + +- [ ] Test 1-digit 100% regrouping (45 unique) +- [ ] Test 2-digit mixed (should use generate-all) +- [ ] Test 3-digit (should use retry-based) +- [ ] Test cycling: Request 100 from 45-problem space +- [ ] Test progressive difficulty (interpolate=true) +- [ ] Test constant difficulty (interpolate=false) +- [ ] Test mixed mode (manual) +- [ ] Test mixed mode (mastery with separate configs) +- [ ] Test subtraction borrowing across zeros +- [ ] Test subtraction 2-digit multiple borrowing (should fallback) +- [ ] Verify server logs show correct strategy selection +- [ ] Verify warnings appear at correct thresholds + +--- + +## Related Documentation + +- **`PROBLEM_GENERATION_ARCHITECTURE.md`** - Complete technical documentation (read this first for deep understanding) +- **`CONFIG_SCHEMA_GUIDE.md`** - Worksheet configuration schema +- **`SMART_DIFFICULTY_SPEC.md`** - Smart mode difficulty progression +- **`SUBTRACTION_AND_OPERATOR_PLAN.md`** - Subtraction implementation plan + +--- + +## Quick Answers + +**Q: Why am I seeing duplicate problems?** +A: Check estimated space vs requested problems. If ratio > 1.0, duplicates are inevitable. + +**Q: Why is generation slow?** +A: If using retry-based with constrained space (e.g., 2-digit 100% regrouping), switch to generate-all by increasing THRESHOLD. + +**Q: Why does interpolate mode show different problems on second cycle?** +A: By design! It clears the "seen" set to allow re-sampling while maintaining difficulty progression. + +**Q: Why is mastery+mixed mode not showing warnings?** +A: Validation is skipped because addition and subtraction have separate configs, making combined estimation complex. + +**Q: Can I have 3+ borrows in 2-digit subtraction?** +A: No, mathematically impossible. `generateBothBorrow()` falls back to ones-only for maxDigits ≤ 2. + +**Q: How do I test if a configuration will have many duplicates?** +A: Use `validateProblemSpace()` - it returns `duplicateRisk` level and `warnings` array. + +**Q: What's the difference between addition and subtraction uniqueness?** +A: Addition is commutative (2+3 = 3+2, same problem). Subtraction is not (5-3 ≠ 3-5, different problems). diff --git a/apps/web/src/app/create/worksheets/PROBLEM_GENERATION_ARCHITECTURE.md b/apps/web/src/app/create/worksheets/PROBLEM_GENERATION_ARCHITECTURE.md new file mode 100644 index 00000000..4cbc5616 --- /dev/null +++ b/apps/web/src/app/create/worksheets/PROBLEM_GENERATION_ARCHITECTURE.md @@ -0,0 +1,556 @@ +# Problem Generation Architecture + +## Overview + +This document describes the complete architecture for generating addition, subtraction, and mixed worksheets with configurable difficulty and uniqueness constraints. + +## Table of Contents + +1. [Core Concepts](#core-concepts) +2. [Generation Strategies](#generation-strategies) +3. [Problem Space Estimation](#problem-space-estimation) +4. [Edge Cases](#edge-cases) +5. [Performance Considerations](#performance-considerations) +6. [User-Facing Warnings](#user-facing-warnings) + +--- + +## Core Concepts + +### Problem Types + +**Addition Problems** +- Two addends: `a + b = ?` +- Regrouping ("carrying"): When column sum ≥ 10 +- Categories: + - **Non-regrouping**: No carries in any column (e.g., 23 + 45) + - **Ones-only**: Carry from ones to tens only (e.g., 27 + 58) + - **Multiple regrouping**: Carries in 2+ columns (e.g., 67 + 48) + +**Subtraction Problems** +- Minuend - Subtrahend: `a - b = ?` where `a >= b` +- Borrowing ("regrouping"): When minuend digit < subtrahend digit +- Categories: + - **Non-borrowing**: No borrows needed (e.g., 89 - 34) + - **Ones-only**: Borrow in ones place only (e.g., 52 - 17) + - **Multiple borrowing**: Borrows in 2+ positions (e.g., 534 - 178) + +**Mixed Problems** +- Two modes: + - **Manual mode**: 50/50 random mix using same constraints for both operators + - **Mastery mode**: Separate skill-based configs for addition and subtraction + +### Difficulty Parameters + +**pAnyStart** (0.0 - 1.0) +- Probability that ANY regrouping/borrowing occurs +- `0.0` = No regrouping/borrowing allowed +- `1.0` = ALL problems must have regrouping/borrowing +- `0.5` = Mixed (about half the problems will have regrouping/borrowing) + +**pAllStart** (0.0 - 1.0) +- Probability of MULTIPLE regrouping/borrowing operations +- Must be ≤ pAnyStart +- Only meaningful when `pAnyStart > 0` + +**interpolate** (boolean) +- `false` = All problems at same difficulty (uses pAnyStart as constant) +- `true` = Progressive difficulty from easy to hard + - Early problems: `pAny = pAnyStart × 0.0` + - Late problems: `pAny = pAnyStart × 1.0` + +**digitRange** (`{min: 1-5, max: 1-5}`) +- Range of digits for each operand +- `{min: 1, max: 1}` = Single-digit problems (0-9 for addition, 1-9 for subtraction) +- `{min: 2, max: 2}` = Two-digit problems (10-99) +- `{min: 1, max: 3}` = Mixed 1-3 digit problems + +--- + +## Generation Strategies + +The system uses **two different strategies** based on problem space size: + +### Strategy 1: Generate-All + Shuffle (Small Spaces) + +**When used:** Estimated unique problems < 10,000 + +**Process:** +1. **Enumerate all valid problems** within constraints + - For addition: All pairs `(a, b)` in digit range matching regrouping requirements + - For subtraction: All pairs `(m, s)` where `m ≥ s` matching borrowing requirements +2. **Filter by difficulty constraints** + - `pAnyStart = 1.0` → Keep only regrouping/borrowing problems + - `pAnyStart = 0.0` → Keep only non-regrouping/non-borrowing problems + - `0.0 < pAnyStart < 1.0` → Keep all problems (will sample later) +3. **Handle interpolation** + - **If interpolate = false**: Shuffle deterministically, take first N problems + - **If interpolate = true**: Sort by difficulty (carry/borrow count), sample based on difficulty curve + +**Cycling behavior** (when requesting > available): +- **Non-interpolate**: Repeat shuffled sequence indefinitely + - Problems 0-44: First shuffle + - Problems 45-89: Second shuffle (same order) + - Problems 90+: Third shuffle, etc. +- **Interpolate**: Clear "seen" set after exhausting all unique problems, maintain difficulty curve + - Each cycle: Easy→Medium→Hard progression + - Cycle boundary logged: `"Exhausted all 45 unique problems at position 45. Starting cycle 2"` + +**Advantages:** +- **Zero retries** - No random generation needed +- **Guaranteed coverage** - Every unique problem appears once before repeating +- **Deterministic** - Same seed always produces same worksheet + +**Code location:** `problemGenerator.ts:365-470` + +### Strategy 2: Retry-Based Generation (Large Spaces) + +**When used:** Estimated unique problems ≥ 10,000 + +**Process:** +1. **For each problem position** (i = 0 to total-1): + - Calculate difficulty fraction: `frac = i / (total - 1)` (0.0 → 1.0) + - Interpolate difficulty: `pAny = pAnyStart × frac` (if interpolate=true) + - Sample problem category based on probabilities + - Generate random problem in that category + - Retry up to 100 times if duplicate + - If still duplicate after 100 tries, allow it (prevents infinite loops) + +**Uniqueness tracking:** +- Addition: Key = `"min(a,b)+max(a,b)"` (commutative) +- Subtraction: Key = `"minuend-subtrahend"` (not commutative) + +**Retry limits:** +- Old limit: 3000 retries per problem (millions of iterations for large worksheets!) +- New limit: 100 retries per problem (allow some duplicates to prevent performance issues) + +**Why allow duplicates?** +- For constrained spaces (e.g., 100 problems from 2-digit 100% regrouping), uniqueness is impossible +- Better to have a few duplicates than hang for minutes generating one worksheet +- Duplicates are rare in practice for large spaces + +**Code location:** `problemGenerator.ts:473-557` + +--- + +## Problem Space Estimation + +### Purpose +Estimate how many unique problems exist given constraints to: +1. Choose generation strategy (enumerate vs retry) +2. Warn users about duplicate risk +3. Display problem space in UI + +### Exact Counting (Small Spaces) + +**1-digit addition (0-9):** +- Total pairs: 10 × 10 = 100 +- Regrouping pairs: 45 (where a + b ≥ 10) + - Calculation: `sum from a=0 to 9 of (9-a+1 if a+b≥10 else 0)` + - a=0: none, a=1: 9, a=2: 8, ..., a=9: 1 + - Total: 0+9+8+7+6+5+4+3+2+1 = 45 +- Non-regrouping: 100 - 45 = 55 + +**1-digit subtraction (0-9):** +- Valid pairs: 55 (where m ≥ s, including 0-0) +- Borrowing: ~36 (where m < s at ones place) +- Non-borrowing: ~19 + +**2-digit addition (10-99):** +- Total pairs: 90 × 90 = 8,100 +- Use generate-all for exact count based on pAnyStart + +### Heuristic Estimation (Large Spaces) + +For digit ranges ≥ 2-digit or mixed ranges: + +```typescript +numbersPerDigitCount = digits === 1 ? 10 : 9 × 10^(digits-1) + +// Addition +pairsForDigits = numbersPerDigitCount × numbersPerDigitCount +regroupFactor = pAnyStart > 0.8 ? 0.45 : pAnyStart > 0.5 ? 0.5 : 0.7 +totalSpace += pairsForDigits × regroupFactor + +// Subtraction +pairsForDigits = (numbersPerDigitCount × numbersPerDigitCount) / 2 // Only m ≥ s +borrowFactor = pAnyStart > 0.8 ? 0.35 : pAnyStart > 0.5 ? 0.5 : 0.7 +totalSpace += pairsForDigits × borrowFactor +``` + +**Why these factors?** +- High regrouping requirement (pAnyStart > 0.8) → Fewer valid problems +- Medium regrouping (pAnyStart > 0.5) → About half the space +- Low regrouping → Most problems are valid + +**Code location:** `utils/validateProblemSpace.ts:18-104` + +--- + +## Edge Cases + +### 1. Single-Digit High Regrouping (CRITICAL) + +**Problem:** 1-digit addition with 100% regrouping +**Space size:** Only 45 unique problems! + +**User impact:** +- Requesting 100 problems → 55 duplicates guaranteed +- Warning banner: "Single-digit problems (1-9) with 100% regrouping have very few unique combinations!" + +**Mitigation:** +- Clear warning in UI +- Suggest increasing digit range to 2 +- Suggestion: Reduce to 1 page (20 problems) or lower regrouping to 50% + +**Code:** `validateProblemSpace.ts:175-179` + +### 2. Mixed Mode with Mastery + +**Problem:** Addition and subtraction use different skill configs +**Space estimation:** Cannot easily estimate combined space + +**Solution:** +- Skip validation entirely for `mode=mastery && operator=mixed` +- Code: `WorksheetPreviewContext.tsx:53-56` + +**Reason:** Each operator has its own digitRange, pAnyStart, making combined estimation complex and potentially misleading + +### 3. Subtraction Multiple Borrowing Impossibility + +**Problem:** 1-2 digit subtraction cannot have 2+ borrows + +**Mathematical proof:** +- 1-digit: Only 1 column, max 1 borrow +- 2-digit: Max minuend = 99, if ones requires borrow, tens can only have 0-1 borrows + +**Solution:** +- `generateBothBorrow()` falls back to `generateOnesOnlyBorrow()` when `maxDigits ≤ 2` +- Code: `problemGenerator.ts:802-804` + +### 4. Borrowing Across Zeros + +**Problem:** `1000 - 1` requires 3 borrows (hundreds→tens→ones) + +**Counting logic:** +```typescript +100 - 1: + ones: 0 < 1, borrow from tens + tens: 0 (zero!), borrow from hundreds // +1 borrow for crossing zero + hundreds: 1, decrement to 0 + Total: 2 borrows (ones + crossing zero) + +1000 - 1: + ones: 0 < 1, borrow from tens + tens: 0, borrow from hundreds // +1 borrow + hundreds: 0, borrow from thousands // +1 borrow + thousands: 1, decrement to 0 + Total: 3 borrows +``` + +**Code:** `problemGenerator.ts:740-782` (`countBorrows` function) + +### 5. Duplicate Detection + +**Addition is commutative:** +- `23 + 45` = `45 + 23` → Same problem +- Key: `"min(a,b)+max(a,b)"` = `"23+45"` + +**Subtraction is NOT:** +- `45 - 23` ≠ `23 - 45` +- Key: `"45-23"` (order matters) + +**Mixed mode:** +- `23 + 45` and `45 - 23` are considered different (different operators) + +--- + +## Performance Considerations + +### Strategy Selection Threshold + +**Why 10,000?** +- Generate-all is O(n²) where n = numbers in range +- For 2-digit: 90² = 8,100 pairs (under threshold) → Use generate-all +- For 3-digit: 900² = 810,000 pairs (over threshold) → Use retry-based +- Enumeration is fast for < 10K, slow for > 100K + +### Retry Limit Reduction + +**Old:** 3000 retries per problem +**New:** 100 retries per problem + +**Why reduced?** +- 100-problem worksheet × 3000 retries = 300,000 iterations (seconds to generate) +- 100-problem worksheet × 100 retries = 10,000 iterations (milliseconds to generate) +- For large problem spaces, duplicates are rare anyway +- For small problem spaces, we use generate-all (zero retries) + +**When duplicates occur:** +- Constrained space + retry strategy = Some duplicates allowed +- Example: 2-digit 100% regrouping, 500 problems requested + - Unique space: ~3,700 problems + - After 3,700 unique: Retries start failing, duplicates appear + - Alternative: Use generate-all + cycle (better!) + +### Logging + +**Addition generation:** +``` +[ADD GEN] Starting: 100 problems, digitRange: 1-1, pAnyStart: 1, pAllStart: 0 +[ADD GEN] Estimated unique problem space: 45 (requesting 100) +[ADD GEN] Using generate-all + shuffle (space < 10000, interpolate=true) +[ADD GEN] Generated 45 unique problems +[ADD GEN] Sorting problems by difficulty for progressive difficulty +[ADD GEN] Exhausted all 45 unique problems at position 45. Starting cycle 2. +[ADD GEN] Complete: 100 problems in 8ms (0 retries, generate-all with progressive difficulty, 1 cycles) +``` + +**Subtraction generation:** +``` +[SUB GEN] Starting: 50 problems, digitRange: 2-2, pAnyBorrow: 0.5, pAllBorrow: 0 +[SUB GEN] Estimated unique problem space: 4050 (requesting 50) +[SUB GEN] Using generate-all + shuffle (space < 10000) +[SUB GEN] Generated 4050 unique problems +[SUB GEN] Complete: 50 problems in 112ms (0 retries, generate-all method) +``` + +**Mixed mastery generation:** +``` +[MASTERY MIXED] Generating 100 mixed problems (50/50 split)... +[MASTERY MIXED] Step 1: Generating 50 addition problems... +[ADD GEN] Starting: 50 problems, digitRange: 2-2, pAnyStart: 0.3, pAllStart: 0 +[MASTERY MIXED] Step 1: ✓ Generated 50 addition problems in 45ms +[MASTERY MIXED] Step 2: Generating 50 subtraction problems... +[SUB GEN] Starting: 50 problems, digitRange: 1-2, pAnyBorrow: 0.7, pAllBorrow: 0 +[MASTERY MIXED] Step 2: ✓ Generated 50 subtraction problems in 23ms +[MASTERY MIXED] Step 3: Shuffling 100 problems... +[MASTERY MIXED] Step 3: ✓ Shuffled in 0ms +``` + +--- + +## User-Facing Warnings + +### Current Implementation + +**Where shown:** `DuplicateWarningBanner` component (preview pane, centered) + +**Trigger conditions:** +- `duplicateRisk !== 'none'` (ratio ≥ 0.3) +- Not dismissed by user +- Not in mastery + mixed mode + +**Warning levels:** + +**Low risk** (0.3 ≤ ratio < 0.5): +``` +You're requesting 50 problems, but only ~45 unique problems are possible +with these constraints. Some duplicates may occur. +``` + +**Medium risk** (0.5 ≤ ratio < 0.8): +``` +Warning: Only ~45 unique problems possible, but you're requesting 100. +Expect moderate duplicates. + +Suggestion: Reduce pages to 1 or increase digit range to 3 +``` + +**High risk** (0.8 ≤ ratio < 1.5): +``` +High duplicate risk! Only ~45 unique problems possible for 100 requested. + +Recommendations: + • Reduce to 1 pages (50% of available space) + • Increase digit range to 2-2 + • Lower regrouping probability from 100% to 50% +``` + +**Extreme risk** (ratio ≥ 1.5): +``` +Extreme duplicate risk! Requesting 200 problems but only ~45 unique problems exist. + +This configuration will produce mostly duplicate problems. + +Strong recommendations: + • Reduce to 1 pages maximum + • OR increase digit range from 1-1 to 1-2 + • OR reduce regrouping requirement from 100% +``` + +**Special case - Single digit:** +``` +Single-digit problems (1-9) with 100% regrouping have very few unique combinations! +``` + +### Suggested Improvements + +**1. Show in Config Panel (Proactive)** +- Display estimated problem space next to pages/problemsPerPage sliders +- Live update as user adjusts settings +- Color-coded indicator: Green (plenty), Yellow (tight), Red (insufficient) + +**2. Smart Mode Suggestions** +- When user selects high pages + constrained digit range: + - "This configuration has limited unique problems. Consider using Smart Mode for auto-scaled difficulty." + +**3. Download-Time Warning (Last Chance)** +- If user dismisses warning and clicks Download with extreme risk: + - Modal: "Are you sure? This will produce mostly duplicates. Continue anyway?" + +**4. Mixed Mode Validation** +- Currently skipped for mastery+mixed +- Could estimate: `additionSpace / 2 + subtractionSpace / 2` (rough approximation) +- Or: "Mixed mastery mode - problem space not validated" + +**5. Tooltip on Regrouping Slider** +- "100% regrouping with 1-digit problems severely limits unique combinations (only 45 possible)" +- Show when `digitRange.max === 1 && pAnyStart > 0.8` + +--- + +## Code Organization + +### Main Files + +**`problemGenerator.ts`** (1,130 lines) +- All problem generation logic (addition, subtraction, mixed) +- Generate-all vs retry strategy selection +- Regrouping/borrowing counting +- Deterministic PRNG (Mulberry32) +- Mixed mode (manual and mastery) + +**`utils/validateProblemSpace.ts`** (200 lines) +- Problem space estimation +- Duplicate risk calculation +- Warning message generation +- Validation logic + +**`components/worksheet-preview/WorksheetPreviewContext.tsx`** (85 lines) +- Validation triggering on config changes +- Warning state management +- Dismiss state tracking + +**`components/worksheet-preview/DuplicateWarningBanner.tsx`** (147 lines) +- Warning UI display +- Collapsible details +- Dismiss button + +### Key Functions + +**Addition:** +- `generateProblems()` - Main entry point +- `generateAllAdditionProblems()` - Enumerate all valid problems +- `generateNonRegroup()` - No carries +- `generateOnesOnly()` - Carry in ones only +- `generateBoth()` - Multiple carries +- `countRegroupingOperations()` - Difficulty scoring + +**Subtraction:** +- `generateSubtractionProblems()` - Main entry point +- `generateAllSubtractionProblems()` - Enumerate all valid problems +- `generateNonBorrow()` - No borrows +- `generateOnesOnlyBorrow()` - Borrow in ones only +- `generateBothBorrow()` - Multiple borrows +- `countBorrows()` - Simulate subtraction algorithm + +**Mixed:** +- `generateMixedProblems()` - Manual mode 50/50 split +- `generateMasteryMixedProblems()` - Separate skill-based configs + +**Validation:** +- `estimateUniqueProblemSpace()` - Exact or heuristic estimation +- `validateProblemSpace()` - Duplicate risk analysis + +--- + +## Testing Considerations + +### Unit Tests Needed + +**Problem space estimation:** +- 1-digit addition: Exactly 45 regrouping, 55 non-regrouping +- 1-digit subtraction: Validate borrow counts +- Mixed mode: Validate combined space estimation + +**Generation strategy selection:** +- Verify generate-all used for spaces < 10K +- Verify retry-based used for spaces ≥ 10K + +**Cycling behavior:** +- Request 100 problems from 45-problem space +- Verify first 45 are unique +- Verify next 45 repeat the sequence (non-interpolate) +- Verify progressive difficulty maintained across cycles (interpolate) + +**Edge cases:** +- Single-digit 100% regrouping +- Subtraction multiple borrows with 2-digit max +- Borrowing across zeros (1000 - 1) +- Mixed mastery mode (skip validation) + +### Integration Tests + +**End-to-end worksheet generation:** +1. Configure: 1-digit, 100% regrouping, 100 problems +2. Generate worksheet +3. Verify warning shown +4. Verify 45 unique problems + 55 repeats +5. Download PDF, verify renders correctly + +--- + +## Future Improvements + +### 1. Better Duplicate Handling for Interpolate Mode + +**Current:** Clears "seen" set and restarts cycle +**Better:** Shuffle the sorted array between cycles to get different difficulty ordering + +### 2. Subtraction Generate-All Missing Interpolate + +**Current:** Subtraction doesn't support generate-all + interpolate +**Code:** `subtractionGenerator.ts:863` has `&& !interpolate` check +**Fix:** Implement same difficulty sorting as addition + +### 3. More Granular Difficulty Levels + +**Current:** 3 categories (non, onesOnly, both) +**Better:** Score by exact carry/borrow count (0, 1, 2, 3+) for finer difficulty curve + +### 4. Problem Space Caching + +**Current:** Re-estimates on every config change +**Better:** Cache estimated spaces for common configurations + +### 5. User Education + +**Current:** Technical warnings with math jargon +**Better:** Simpler language, visual indicators, examples + +--- + +## Glossary + +**Regrouping (Addition):** Carrying a value to the next place value when column sum ≥ 10 + +**Borrowing (Subtraction):** Taking from a higher place value when minuend digit < subtrahend digit + +**Problem Space:** The set of all unique problems possible given constraints + +**Generate-All:** Strategy that enumerates all valid problems upfront, then shuffles + +**Retry-Based:** Strategy that randomly generates problems and retries on duplicates + +**Interpolation:** Gradual difficulty increase from start to end of worksheet + +**Cycle:** Repetition of the entire problem set when requesting more problems than exist + +**pAnyStart:** Target probability that any regrouping/borrowing occurs + +**pAllStart:** Target probability of multiple regrouping/borrowing operations + +**Commutative:** Order doesn't matter (addition: 2+3 = 3+2) + +**Non-commutative:** Order matters (subtraction: 5-3 ≠ 3-5) diff --git a/apps/web/src/app/create/worksheets/USER_WARNING_IMPROVEMENTS.md b/apps/web/src/app/create/worksheets/USER_WARNING_IMPROVEMENTS.md new file mode 100644 index 00000000..3f8a4691 --- /dev/null +++ b/apps/web/src/app/create/worksheets/USER_WARNING_IMPROVEMENTS.md @@ -0,0 +1,535 @@ +# User Warning Improvements for Problem Space Constraints + +## Current Implementation + +### Warning Banner (Implemented) + +**Location:** Preview pane, centered overlay +**Component:** `DuplicateWarningBanner.tsx` +**Trigger:** When `duplicateRisk !== 'none'` (ratio ≥ 0.3) + +**Strengths:** +- ✅ Visible and prominent +- ✅ Dismissable +- ✅ Shows in preview (where user sees the actual worksheet) +- ✅ Provides actionable recommendations +- ✅ Collapsible details for advanced users + +**Weaknesses:** +- ❌ Reactive (shown after user has configured) +- ❌ Can be dismissed and forgotten +- ❌ Not shown in mastery+mixed mode +- ❌ No visual feedback in config panel +- ❌ Requires user to generate preview first + +--- + +## Recommended Improvements + +### 1. Proactive Config Panel Indicator (HIGH PRIORITY) + +**Where:** Next to pages/problemsPerPage sliders in ConfigPanel +**When:** Live update as user adjusts settings +**Why:** Prevents users from creating invalid configs in the first place + +#### Design + +```typescript +interface ProblemSpaceIndicator { + estimatedSpace: number + requestedProblems: number + status: 'plenty' | 'tight' | 'insufficient' + color: 'green' | 'yellow' | 'red' +} +``` + +**Visual appearance:** + +``` +┌─ Problems Per Page ────────────────┐ +│ [──────●──────] 20 │ +│ │ +│ Problem Space: ~4,050 available │ ← Green text +│ ✓ Plenty of unique problems │ +└────────────────────────────────────┘ + +┌─ Problems Per Page ────────────────┐ +│ [──────────●──────] 50 │ +│ │ +│ Problem Space: ~45 available │ ← Yellow text +│ ⚠ Limited unique problems │ +└────────────────────────────────────┘ + +┌─ Problems Per Page ────────────────┐ +│ [────────────●──] 100 │ +│ │ +│ Problem Space: ~45 available │ ← Red text +│ ✕ Insufficient - duplicates likely │ +│ → Try increasing digit range │ +└────────────────────────────────────┘ +``` + +**Implementation:** + +```typescript +// In ConfigPanel component +const estimatedSpace = useMemo(() => { + return estimateUniqueProblemSpace( + formState.digitRange, + formState.pAnyStart, + formState.operator + ) +}, [formState.digitRange, formState.pAnyStart, formState.operator]) + +const requestedProblems = formState.problemsPerPage * formState.pages +const ratio = requestedProblems / estimatedSpace + +const spaceIndicator: ProblemSpaceIndicator = { + estimatedSpace, + requestedProblems, + status: ratio < 0.5 ? 'plenty' : ratio < 0.8 ? 'tight' : 'insufficient', + color: ratio < 0.5 ? 'green' : ratio < 0.8 ? 'yellow' : 'red' +} +``` + +**Files to modify:** +- `components/config-panel/ConfigPanel.tsx` (or respective sections) +- Possibly create `components/config-panel/ProblemSpaceIndicator.tsx` + +--- + +### 2. Slider Constraints with Visual Feedback (MEDIUM PRIORITY) + +**Where:** Pages and problemsPerPage sliders +**When:** User drags slider past recommended limits +**Why:** Prevents invalid configurations while allowing override + +#### Design + +**Visual feedback:** +- Green track: Safe range (0-50% of space) +- Yellow track: Caution range (50-80% of space) +- Red track: Over limit (80%+ of space) + +**Dynamic max values:** +- Suggest max pages based on current settings +- Show "soft limit" vs "hard limit" +- Allow override with confirmation + +**Example:** + +``` +┌─ Pages ────────────────────────────┐ +│ [──●──|───────] 2 pages │ ← Slider in yellow zone +│ ↑ │ +│ Recommended max: 2 │ +│ (45 unique problems available) │ +│ │ +│ [Continue anyway] [Reduce to 1] │ +└────────────────────────────────────┘ +``` + +**Implementation:** + +```typescript +const recommendedMaxPages = Math.floor((estimatedSpace * 0.5) / problemsPerPage) + +// Slider shows visual zones + +``` + +--- + +### 3. Smart Mode Suggestion (MEDIUM PRIORITY) + +**Where:** Config panel when user selects constrained settings +**When:** High pages + constrained digit range + manual mode +**Why:** Educate users about Smart Mode's auto-scaling benefits + +#### Design + +``` +┌─ Mode Selection ──────────────────────────────────┐ +│ ○ Smart Mode (Recommended for varied difficulty) │ +│ ● Manual Mode │ +│ │ +│ 💡 Tip: Smart Mode automatically scales │ +│ difficulty and maximizes problem variety │ +│ [Switch to Smart Mode] │ +└───────────────────────────────────────────────────┘ +``` + +**Trigger conditions:** +- Manual mode selected +- Pages > 2 +- Digit range narrow (min === max) +- High regrouping probability (pAnyStart > 0.8) +- Duplicate risk >= medium + +**Implementation:** + +```typescript +const shouldSuggestSmartMode = + formState.mode === 'manual' && + formState.pages > 2 && + formState.digitRange.min === formState.digitRange.max && + formState.pAnyStart > 0.8 && + duplicateRisk >= 'medium' + +{shouldSuggestSmartMode && ( + setMode('smart')} /> +)} +``` + +--- + +### 4. Download-Time Confirmation (LOW PRIORITY) + +**Where:** Modal before generating PDF +**When:** User dismissed warning AND extreme duplicate risk +**Why:** Last chance to prevent user frustration + +#### Design + +``` +┌─ Confirm Download ─────────────────────────────┐ +│ │ +│ ⚠️ Warning: Duplicate Problems Detected │ +│ │ +│ Your configuration will produce: │ +│ • 200 requested problems │ +│ • Only 45 unique problems available │ +│ • ~155 duplicates (78% of worksheet) │ +│ │ +│ This may not provide enough practice variety. │ +│ │ +│ Recommendations: │ +│ • Reduce to 1-2 pages │ +│ • Increase digit range from 1 to 2 │ +│ • Lower regrouping to 50% │ +│ │ +│ [Go Back] [Download Anyway] │ +└─────────────────────────────────────────────────┘ +``` + +**Trigger conditions:** +- User clicked Download +- Duplicate risk is extreme (ratio >= 1.5) +- Warning was previously dismissed (or never shown) + +**Implementation:** + +```typescript +// In PreviewCenter.tsx handleGenerate +const handleGenerate = async () => { + if (duplicateRisk === 'extreme' && (isDismissed || !warningsShown)) { + setShowDownloadConfirmModal(true) + return + } + + await onGenerate() +} +``` + +--- + +### 5. Tooltip on Regrouping Slider (LOW PRIORITY) + +**Where:** Regrouping probability slider +**When:** Hover or focus +**Why:** Contextual education about regrouping constraints + +#### Design + +``` +┌─ Regrouping Probability ──────────────────────┐ +│ [────────────────●] 100% ⓘ │ ← Hover for tooltip +└────────────────────────────────────────────────┘ + +Tooltip appears: +┌────────────────────────────────────────────┐ +│ 100% Regrouping with 1-Digit Problems │ +│ │ +│ This limits unique problems to only 45. │ +│ Consider: │ +│ • Reducing to 50% regrouping │ +│ • Increasing to 2-digit problems │ +└────────────────────────────────────────────┘ +``` + +**Conditional display:** +- Only show warning tooltip when: + - `digitRange.max === 1` + - `pAnyStart > 0.8` + +**Implementation:** + +```typescript +const showRegroupingWarning = + formState.digitRange.max === 1 && formState.pAnyStart > 0.8 + + + ) : undefined} +/> +``` + +--- + +### 6. Digit Range Recommendations (MEDIUM PRIORITY) + +**Where:** Digit range selector +**When:** User selects 1-digit with high pages count +**Why:** Proactive suggestion before problem space constraint hits + +#### Design + +``` +┌─ Digit Range ─────────────────────────────────┐ +│ Min: [1▼] Max: [1▼] │ +│ │ +│ ℹ️ 1-digit problems have limited variety │ +│ For 5+ pages, consider: │ +│ • Min: 1, Max: 2 (mixed 1-2 digit) │ +│ • Min: 2, Max: 2 (all 2-digit) │ +│ │ +│ [Quick Apply: 1-2 digits] │ +└────────────────────────────────────────────────┘ +``` + +**Trigger conditions:** +- `digitRange.max === 1` +- `pages >= 5` + +**Implementation:** + +```typescript +const shouldSuggestDigitRangeIncrease = + formState.digitRange.max === 1 && formState.pages >= 5 + +{shouldSuggestDigitRangeIncrease && ( + setDigitRange({ min: 1, max: 2 })} + /> +)} +``` + +--- + +### 7. Mixed Mode Mastery Validation (LOW PRIORITY) + +**Where:** Preview banner or config panel +**When:** Mastery + mixed mode selected +**Why:** Currently shows no validation, which could be confusing + +#### Design + +**Option A: Simple info message** +``` +ℹ️ Mixed Mastery Mode +Problem space not validated (uses separate skill configs for +/−) +``` + +**Option B: Rough estimation** +``` +ℹ️ Mixed Mastery Mode +~2,025 addition problems + ~550 subtraction problems available +(Separate configs - validation approximate) +``` + +**Trigger conditions:** +- `mode === 'mastery'` +- `operator === 'mixed'` + +**Implementation:** + +Currently skipped in `WorksheetPreviewContext.tsx:53-56`. + +Two approaches: + +**Approach 1 - Info Only:** +```typescript +if (mode === 'mastery' && operator === 'mixed') { + setWarnings([ + 'ℹ️ Mixed Mastery Mode uses separate skill-based configs for addition and subtraction. Problem space validation is disabled.' + ]) + return +} +``` + +**Approach 2 - Rough Estimation:** +```typescript +if (mode === 'mastery' && operator === 'mixed') { + // Get separate estimates (need to access skill configs) + const addSpace = estimateUniqueProblemSpace( + additionSkill.digitRange, + additionSkill.pAnyStart, + 'addition' + ) + const subSpace = estimateUniqueProblemSpace( + subtractionSkill.digitRange, + subtractionSkill.pAnyStart, + 'subtraction' + ) + + const total = addSpace + subSpace + const requested = problemsPerPage * pages + + if (requested > total * 0.8) { + setWarnings([ + `Mixed Mastery Mode: ~${addSpace} addition + ~${subSpace} subtraction problems available. Validation is approximate.` + ]) + } + return +} +``` + +--- + +## Implementation Priority + +### Phase 1 - High Impact, Low Effort +1. **Config Panel Indicator** - Shows live problem space estimate +2. **Digit Range Recommendations** - Suggests 2-digit when user selects many 1-digit pages + +### Phase 2 - Medium Impact, Medium Effort +3. **Slider Visual Feedback** - Color-coded zones for safe/caution/danger +4. **Smart Mode Suggestion** - Educates about Smart Mode benefits +5. **Tooltip on Regrouping Slider** - Contextual help for 1-digit + 100% regrouping + +### Phase 3 - Nice to Have +6. **Download Confirmation** - Last-chance warning for extreme cases +7. **Mixed Mastery Validation** - Rough estimation or info message + +--- + +## Component Structure + +Suggested new components to create: + +``` +components/config-panel/ +├── ProblemSpaceIndicator.tsx # Live space estimate with color coding +├── SmartModeSuggestion.tsx # Suggests switching to Smart Mode +├── DigitRangeRecommendation.tsx # Suggests increasing digit range +└── RegroupingConstraintTooltip.tsx # Warning tooltip for constrained settings + +components/modals/ +└── DownloadConfirmModal.tsx # Pre-download warning for extreme risk +``` + +--- + +## User Education Opportunities + +### Tooltips and Help Text + +**Regrouping Probability:** +``` +"The percentage of problems that involve carrying (addition) or borrowing +(subtraction). Higher percentages with limited digit ranges may result in +fewer unique problems." +``` + +**Digit Range:** +``` +"1-digit: 0-9 (very limited variety) +2-digit: 10-99 (good variety) +3-digit: 100-999 (excellent variety) + +For worksheets with many problems, use 2+ digits." +``` + +**Pages:** +``` +"Each page contains {problemsPerPage} problems. +{estimatedSpace} unique problems available with current settings." +``` + +### Onboarding/Tutorial + +Add a brief tutorial or info modal explaining: +- What "problem space" means +- Why digit range matters +- How regrouping probability affects uniqueness +- When to use Smart Mode vs Manual Mode + +--- + +## Testing Plan + +For each improvement: + +1. **Visual regression:** Screenshot before/after +2. **Interaction testing:** Verify all states (plenty/tight/insufficient) +3. **Edge case testing:** + - 1-digit 100% regrouping (45 problems) + - 2-digit 100% regrouping (~3,700 problems) + - Mixed mode with mastery +4. **Accessibility:** Keyboard navigation, screen reader labels +5. **Mobile responsive:** Touch-friendly, readable on small screens + +--- + +## Analytics (Future Consideration) + +Track user behavior to measure effectiveness: + +```typescript +analytics.track('Warning Shown', { + duplicateRisk: 'high', + estimatedSpace: 45, + requestedProblems: 100, + digitRange: { min: 1, max: 1 }, + pAnyStart: 1.0 +}) + +analytics.track('Warning Dismissed', { + duplicateRisk: 'high' +}) + +analytics.track('Config Adjusted After Warning', { + change: 'increased_digit_range', + from: { min: 1, max: 1 }, + to: { min: 1, max: 2 } +}) + +analytics.track('Downloaded Despite Warning', { + duplicateRisk: 'extreme' +}) +``` + +Use this data to: +- Identify most common problematic configurations +- Measure warning effectiveness +- Improve recommendation accuracy + +--- + +## Summary + +**Current state:** Reactive warning in preview pane (good, but not enough) + +**Ideal state:** Multi-layered approach +1. **Proactive** - Config panel shows live feedback +2. **Preventive** - Visual slider constraints guide users +3. **Educational** - Tooltips and suggestions explain why +4. **Protective** - Last-chance confirmation for extreme cases + +**Impact:** +- Fewer confused users ("why so many duplicates?") +- Better worksheet quality +- Reduced support requests +- Improved user confidence in the tool diff --git a/apps/web/src/app/create/worksheets/problemGenerator.ts b/apps/web/src/app/create/worksheets/problemGenerator.ts index d9755472..9abe3092 100644 --- a/apps/web/src/app/create/worksheets/problemGenerator.ts +++ b/apps/web/src/app/create/worksheets/problemGenerator.ts @@ -333,14 +333,30 @@ function uniquePush(list: AdditionProblem[], a: number, b: number, seen: Set unique problems available): + * - Non-interpolate mode: Repeats entire shuffled sequence (problems 0-44, 45-89, 90+...) + * - Interpolate mode: Clears "seen" set after exhausting problems, maintains difficulty curve + * + * EXAMPLES: + * - 1-digit 100% regrouping: Only 45 unique problems exist (will cycle after problem 44) + * - 2-digit mixed regrouping: ~4,000 unique problems (generate-all used) + * - 3-digit any regrouping: ~400,000 unique problems (retry-based used) * * @param total Total number of problems to generate - * @param pAnyStart Starting probability of any regrouping (0-1) - * @param pAllStart Starting probability of multiple regrouping (0-1) - * @param interpolate If true, difficulty increases from start to end - * @param seed Random seed for reproducible generation - * @param digitRange Digit range for problem numbers (V4+) + * @param pAnyStart Target probability of ANY regrouping occurring (0-1) + * 0.0 = no regrouping, 1.0 = all problems must have regrouping + * @param pAllStart Target probability of MULTIPLE regrouping (0-1), must be ≤ pAnyStart + * @param interpolate If true, difficulty increases progressively from easy→hard + * If false, all problems at constant difficulty + * @param seed Random seed for deterministic reproducible generation + * @param digitRange Digit range for problem operands (1-5 digits) + * @returns Array of addition problems matching constraints */ export function generateProblems( total: number, @@ -362,7 +378,14 @@ export function generateProblems( const estimatedSpace = estimateUniqueProblemSpace(digitRange, pAnyStart, 'addition') console.log(`[ADD GEN] Estimated unique problem space: ${estimatedSpace} (requesting ${total})`) - // For small problem spaces, use generate-all + shuffle approach (even with interpolate) + // ======================================================================== + // STRATEGY 1: Generate-All + Shuffle (Small Problem Spaces) + // ======================================================================== + // Used when estimated unique problems < 10,000 + // Advantages: + // - Zero retries (no random generation needed) + // - Guaranteed coverage (all unique problems appear before repeating) + // - Deterministic (same seed = same worksheet) const THRESHOLD = 10000 if (estimatedSpace < THRESHOLD) { console.log( @@ -372,8 +395,9 @@ export function generateProblems( console.log(`[ADD GEN] Generated ${allProblems.length} unique problems`) if (interpolate) { - // Sort problems by difficulty (number of regrouping operations) - // This allows us to sample from easier problems early, harder problems later + // ===== PROGRESSIVE DIFFICULTY MODE ===== + // Sort all problems by difficulty (carry count), then sample based on + // position in worksheet to create easy→medium→hard progression console.log(`[ADD GEN] Sorting problems by difficulty for progressive difficulty`) const sortedByDifficulty = [...allProblems].sort((a, b) => { const diffA = countRegroupingOperations(a.a, a.b) @@ -387,21 +411,23 @@ export function generateProblems( let cycleCount = 0 // Track how many times we've cycled through all problems for (let i = 0; i < total; i++) { + // Calculate difficulty fraction (0.0 = easy, 1.0 = hard) const frac = total <= 1 ? 0 : i / (total - 1) // Map frac (0 to 1) to index in sorted array // frac=0 (start) → sample from easy problems (low index) // frac=1 (end) → sample from hard problems (high index) const targetIndex = Math.floor(frac * (sortedByDifficulty.length - 1)) - // Try to get a problem near the target difficulty that we haven't used + // Try to get a problem near the target difficulty that we haven't used yet let problem = sortedByDifficulty[targetIndex] const key = `${problem.a},${problem.b}` - // If already used, search nearby for unused problem + // If already used, search nearby (forward then backward) for unused problem + // This maintains approximate difficulty while avoiding duplicates if (seen.has(key)) { let found = false for (let offset = 1; offset < sortedByDifficulty.length; offset++) { - // Try forward first, then backward + // Try forward first (slightly harder), then backward (slightly easier) for (const direction of [1, -1]) { const idx = targetIndex + direction * offset if (idx >= 0 && idx < sortedByDifficulty.length) { @@ -416,15 +442,16 @@ export function generateProblems( } if (found) break } - // If still not found, we've exhausted all unique problems - // Reset the seen set and start a new cycle + // If still not found, we've exhausted ALL unique problems + // Clear the "seen" set and start a new cycle through the sorted array + // This maintains the difficulty progression across cycles if (!found) { cycleCount++ console.log( `[ADD GEN] Exhausted all ${sortedByDifficulty.length} unique problems at position ${i}. Starting cycle ${cycleCount + 1}.` ) seen.clear() - // Use the target problem for this position + // Use the target problem for this position (beginning of new cycle) seen.add(key) } } else { @@ -440,16 +467,22 @@ export function generateProblems( ) return result } else { - // No interpolation - just shuffle and take first N + // ===== CONSTANT DIFFICULTY MODE ===== + // Shuffle all problems randomly, no sorting by difficulty const shuffled = shuffleArray(allProblems, rand) // If we need more problems than available, cycle through the shuffled array + // Example: 45 unique problems, requesting 100 + // - Problems 0-44: First complete shuffle + // - Problems 45-89: Second complete shuffle (same order as first) + // - Problems 90-99: First 10 from third shuffle if (total > shuffled.length) { const cyclesNeeded = Math.ceil(total / shuffled.length) console.warn( `[ADD GEN] Warning: Requested ${total} problems but only ${shuffled.length} unique problems exist. Will cycle ${cyclesNeeded} times.` ) // Build result by repeating the entire shuffled array as many times as needed + // Using modulo ensures we cycle through: problem[0], problem[1], ..., problem[N-1], problem[0], ... const result: AdditionProblem[] = [] for (let i = 0; i < total; i++) { result.push(shuffled[i % shuffled.length]) @@ -461,7 +494,7 @@ export function generateProblems( return result } - // Take first N problems from shuffled array + // Take first N problems from shuffled array (typical case: enough unique problems) const elapsed = Date.now() - startTime console.log( `[ADD GEN] Complete: ${total} problems in ${elapsed}ms (0 retries, generate-all method)` @@ -470,7 +503,12 @@ export function generateProblems( } } - // For large problem spaces, use retry-based approach + // ======================================================================== + // STRATEGY 2: Retry-Based Generation (Large Problem Spaces) + // ======================================================================== + // Used when estimated unique problems ≥ 10,000 + // Randomly generates problems and retries on duplicates + // Allows some duplicates after 100 retries to prevent infinite loops console.log(`[ADD GEN] Using retry-based approach (space >= ${THRESHOLD})`) const problems: AdditionProblem[] = [] const seen = new Set() @@ -998,14 +1036,28 @@ export function generateSubtractionProblems( } /** - * Generate mixed addition and subtraction problems for mastery mode - * Uses separate configs for addition and subtraction based on current skill levels + * Generate mixed addition and subtraction problems for MASTERY MODE * - * @param count Number of problems to generate - * @param additionConfig Configuration for addition problems (from current addition skill) - * @param subtractionConfig Configuration for subtraction problems (from current subtraction skill) - * @param seed Random seed - * @returns Array of mixed problems, shuffled randomly + * KEY DIFFERENCES from manual mixed mode: + * - Uses SEPARATE skill-based configs for addition vs subtraction + * - Addition problems based on addition skill (e.g., 2-digit 30% regrouping) + * - Subtraction problems based on subtraction skill (e.g., 1-2 digit 70% borrowing) + * - Problems can have different difficulties within same worksheet + * + * PROBLEM SPACE VALIDATION: + * - Validation is SKIPPED for mastery+mixed mode (too complex with separate configs) + * - See WorksheetPreviewContext.tsx:53-56 + * + * EXAMPLE: + * - Addition skill: Level 5 → {digitRange: {min:2, max:2}, pAnyStart: 0.3} + * - Subtraction skill: Level 3 → {digitRange: {min:1, max:2}, pAnyStart: 0.7} + * - Result: 50 easy 2-digit additions + 50 harder 1-2 digit subtractions, shuffled + * + * @param count Total number of problems to generate (split 50/50 between operators) + * @param additionConfig Difficulty config from current addition skill level + * @param subtractionConfig Difficulty config from current subtraction skill level + * @param seed Random seed for deterministic generation and shuffling + * @returns Array of mixed problems shuffled randomly (no interpolation in mastery mode) */ export function generateMasteryMixedProblems( count: number,