docs: comprehensive problem generation documentation

Created three documentation files covering the problem generation system:

**1. PROBLEM_GENERATION_ARCHITECTURE.md** (Technical Deep Dive)
- Complete architecture overview for developers
- Two generation strategies: generate-all + shuffle vs retry-based
- Problem space estimation formulas (exact and heuristic)
- Edge cases: single-digit 100% regrouping (45 problems), mixed mastery, subtraction borrowing
- Performance considerations and trade-offs
- Logging patterns and debugging
- Testing checklist
- Future improvements

**2. .claude/PROBLEM_GENERATION.md** (Quick Reference for AI)
- Fast lookup for Claude Code and developers
- File locations and key functions
- Strategy selection logic with code examples
- Critical edge cases with explanations
- Debugging commands
- Common modifications guide
- Problem space estimation formulas
- Testing checklist
- Quick Q&A section

**3. USER_WARNING_IMPROVEMENTS.md** (UX Enhancement Plan)
- Current warning system review
- 7 recommended improvements with designs:
  1. Config panel live indicator (HIGH PRIORITY)
  2. Slider visual feedback (MEDIUM)
  3. Smart Mode suggestion (MEDIUM)
  4. Download confirmation (LOW)
  5. Regrouping tooltip (LOW)
  6. Digit range recommendations (MEDIUM)
  7. Mixed mastery validation (LOW)
- Component structure suggestions
- Implementation phases
- Testing plan
- Analytics recommendations

**Enhanced Inline Comments:**
- Added comprehensive docstrings to `generateProblems()`
- Documented strategy selection logic with examples
- Explained cycling behavior for both modes
- Added section headers for generate-all and retry-based branches
- Documented progressive vs constant difficulty modes
- Added comments for mastery mixed mode differences

**Key Insights Documented:**
- Only 45 unique 1-digit 100% regrouping problems exist
- Cycling maintains order (non-interpolate) or clears seen set (interpolate)
- Retry limit reduced from 3000 to 100 for performance
- Mixed mastery skips validation due to separate configs
- Subtraction 2-digit can't have 2+ borrows (mathematical impossibility)
- Borrowing across zeros counts each zero as additional borrow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-11-12 09:19:09 -06:00
parent 1e3fa58bc3
commit 5304e4da4e
4 changed files with 1521 additions and 25 deletions

View File

@@ -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).

View File

@@ -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)

View File

@@ -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
<Slider
value={formState.pages}
max={10} // Hard limit
onChange={handlePagesChange}
zones={[
{ end: recommendedMaxPages, color: 'green' },
{ end: recommendedMaxPages * 1.6, color: 'yellow' },
{ end: 10, color: 'red' }
]}
/>
```
---
### 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 && (
<SmartModeSuggestion onSwitch={() => 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
<Slider
label="Regrouping Probability"
value={formState.pAnyStart}
tooltip={showRegroupingWarning ? (
<RegroupingConstraintTooltip digitRange={formState.digitRange} />
) : 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 && (
<DigitRangeRecommendation
onApply={() => 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

View File

@@ -333,14 +333,30 @@ function uniquePush(list: AdditionProblem[], a: number, b: number, seen: Set<str
}
/**
* Generate a complete set of problems based on difficulty parameters
* Generate a complete set of addition problems with configurable difficulty
*
* STRATEGY SELECTION:
* - Small problem spaces (< 10,000 unique): Generate-all + shuffle for zero retries
* - Large problem spaces (≥ 10,000): Retry-based generation allows some duplicates
*
* CYCLING BEHAVIOR (when total > 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<string>()
@@ -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,