feat: add unified step generator for consistent pedagogical decomposition
Create comprehensive unified instruction generator that computes all tutorial aspects simultaneously to guarantee consistency between pedagogical decomposition, English instructions, expected states, and bead mappings. Key features: - Generates pedagogical decomposition like "3 + 14 = 3 + 10 + (5 - 1) = 17" - 1:1 mapping between mathematical terms and bead movements - Simultaneous computation prevents reconstruction inconsistencies - Validates step consistency and expected states - Handles complex operations like ten complements and place value transitions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
437
apps/web/src/utils/unifiedStepGenerator.ts
Normal file
437
apps/web/src/utils/unifiedStepGenerator.ts
Normal file
@@ -0,0 +1,437 @@
|
||||
// Unified step generator that computes all tutorial step information simultaneously
|
||||
// to guarantee consistency between pedagogical decomposition, English instructions,
|
||||
// expected states, and bead mappings.
|
||||
|
||||
import { ValidPlaceValues } from '@soroban/abacus-react'
|
||||
import {
|
||||
BeadState,
|
||||
AbacusState,
|
||||
BeadHighlight,
|
||||
StepBeadHighlight,
|
||||
numberToAbacusState,
|
||||
calculateBeadChanges
|
||||
} from './abacusInstructionGenerator'
|
||||
|
||||
export interface UnifiedStepData {
|
||||
stepIndex: number
|
||||
|
||||
// Pedagogical decomposition - the math term for this step
|
||||
mathematicalTerm: string // e.g., "10", "(5 - 1)", "-6"
|
||||
|
||||
// English instruction - what the user should do
|
||||
englishInstruction: string // e.g., "Click earth bead 1 in tens column"
|
||||
|
||||
// Expected ending state/value after this step
|
||||
expectedValue: number // e.g., 13, 17, 11
|
||||
expectedState: AbacusState
|
||||
|
||||
// Bead movements for this step (for arrows/highlights)
|
||||
beadMovements: StepBeadHighlight[]
|
||||
|
||||
// Validation
|
||||
isValid: boolean
|
||||
validationIssues?: string[]
|
||||
}
|
||||
|
||||
export interface UnifiedInstructionSequence {
|
||||
// Overall pedagogical decomposition
|
||||
fullDecomposition: string // e.g., "3 + 14 = 3 + 10 + (5 - 1) = 17"
|
||||
|
||||
// Step-by-step breakdown
|
||||
steps: UnifiedStepData[]
|
||||
|
||||
// Summary
|
||||
startValue: number
|
||||
targetValue: number
|
||||
totalSteps: number
|
||||
}
|
||||
|
||||
/**
|
||||
* THE UNIFIED FUNCTION: Generates all tutorial step information simultaneously
|
||||
* to ensure perfect consistency between math, instructions, states, and bead mappings.
|
||||
*/
|
||||
export function generateUnifiedInstructionSequence(
|
||||
startValue: number,
|
||||
targetValue: number
|
||||
): UnifiedInstructionSequence {
|
||||
|
||||
const difference = targetValue - startValue
|
||||
|
||||
// Step 1: Calculate actual bead movements
|
||||
const startState = numberToAbacusState(startValue)
|
||||
const targetState = numberToAbacusState(targetValue)
|
||||
const { additions, removals } = calculateBeadChanges(startState, targetState)
|
||||
|
||||
// Step 2: Generate pedagogical decomposition terms based on actual bead movements
|
||||
const decompositionTerms = generateDecompositionTerms(startValue, targetValue, additions, removals)
|
||||
|
||||
// Step 3: Generate unified steps - each step computes ALL aspects simultaneously
|
||||
const steps: UnifiedStepData[] = []
|
||||
let currentValue = startValue
|
||||
let currentState = { ...startState }
|
||||
|
||||
for (let stepIndex = 0; stepIndex < decompositionTerms.length; stepIndex++) {
|
||||
const term = decompositionTerms[stepIndex]
|
||||
|
||||
// Calculate what this step should accomplish
|
||||
const stepResult = calculateStepResult(currentValue, term)
|
||||
const newValue = stepResult.newValue
|
||||
const newState = numberToAbacusState(newValue)
|
||||
|
||||
// Find the bead movements for this specific step
|
||||
const stepBeadMovements = calculateStepBeadMovements(
|
||||
currentState,
|
||||
newState,
|
||||
stepIndex
|
||||
)
|
||||
|
||||
// Generate English instruction based on the actual bead movements
|
||||
const englishInstruction = generateStepInstruction(
|
||||
stepBeadMovements,
|
||||
term,
|
||||
stepResult
|
||||
)
|
||||
|
||||
// Validate that everything is consistent
|
||||
const validation = validateStepConsistency(
|
||||
term,
|
||||
englishInstruction,
|
||||
currentValue,
|
||||
newValue,
|
||||
stepBeadMovements
|
||||
)
|
||||
|
||||
// Create the unified step data
|
||||
const stepData: UnifiedStepData = {
|
||||
stepIndex,
|
||||
mathematicalTerm: term,
|
||||
englishInstruction,
|
||||
expectedValue: newValue,
|
||||
expectedState: newState,
|
||||
beadMovements: stepBeadMovements,
|
||||
isValid: validation.isValid,
|
||||
validationIssues: validation.issues
|
||||
}
|
||||
|
||||
steps.push(stepData)
|
||||
|
||||
// Move to next step
|
||||
currentValue = newValue
|
||||
currentState = { ...newState }
|
||||
}
|
||||
|
||||
// Step 4: Build full decomposition string
|
||||
const fullDecomposition = buildFullDecomposition(startValue, targetValue, decompositionTerms)
|
||||
|
||||
return {
|
||||
fullDecomposition,
|
||||
steps,
|
||||
startValue,
|
||||
targetValue,
|
||||
totalSteps: steps.length
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate decomposition terms based on actual bead movements
|
||||
*/
|
||||
function generateDecompositionTerms(
|
||||
startValue: number,
|
||||
targetValue: number,
|
||||
additions: BeadHighlight[],
|
||||
removals: BeadHighlight[]
|
||||
): string[] {
|
||||
|
||||
const terms: string[] = []
|
||||
|
||||
// Group movements by place value
|
||||
const movementsByPlace: { [place: number]: { adds: BeadHighlight[], removes: BeadHighlight[] } } = {}
|
||||
|
||||
additions.forEach(bead => {
|
||||
if (!movementsByPlace[bead.placeValue]) {
|
||||
movementsByPlace[bead.placeValue] = { adds: [], removes: [] }
|
||||
}
|
||||
movementsByPlace[bead.placeValue].adds.push(bead)
|
||||
})
|
||||
|
||||
removals.forEach(bead => {
|
||||
if (!movementsByPlace[bead.placeValue]) {
|
||||
movementsByPlace[bead.placeValue] = { adds: [], removes: [] }
|
||||
}
|
||||
movementsByPlace[bead.placeValue].removes.push(bead)
|
||||
})
|
||||
|
||||
// Process places in pedagogical order (highest first)
|
||||
const places = Object.keys(movementsByPlace)
|
||||
.map(p => parseInt(p))
|
||||
.sort((a, b) => b - a)
|
||||
|
||||
for (const place of places) {
|
||||
const movements = movementsByPlace[place]
|
||||
|
||||
// Calculate the net effect of this place
|
||||
const addValue = movements.adds.reduce((sum, bead) => {
|
||||
return sum + (bead.beadType === 'heaven' ? 5 * Math.pow(10, place) : Math.pow(10, place))
|
||||
}, 0)
|
||||
|
||||
const removeValue = movements.removes.reduce((sum, bead) => {
|
||||
return sum + (bead.beadType === 'heaven' ? 5 * Math.pow(10, place) : Math.pow(10, place))
|
||||
}, 0)
|
||||
|
||||
// Generate appropriate term
|
||||
if (addValue > 0 && removeValue > 0) {
|
||||
// Complement operation
|
||||
terms.push(`(${addValue} - ${removeValue})`)
|
||||
} else if (addValue > 0) {
|
||||
// Pure addition
|
||||
terms.push(`${addValue}`)
|
||||
} else if (removeValue > 0) {
|
||||
// Pure subtraction
|
||||
terms.push(`-${removeValue}`)
|
||||
}
|
||||
}
|
||||
|
||||
return terms
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate what a mathematical term should accomplish
|
||||
*/
|
||||
function calculateStepResult(currentValue: number, term: string): {
|
||||
newValue: number
|
||||
operation: 'add' | 'subtract' | 'complement'
|
||||
addAmount?: number
|
||||
subtractAmount?: number
|
||||
} {
|
||||
|
||||
// Parse the term to understand the operation
|
||||
if (term.startsWith('(') && term.includes(' - ')) {
|
||||
// Complement operation like "(10 - 6)"
|
||||
const match = term.match(/\((\d+) - (\d+)\)/)
|
||||
if (match) {
|
||||
const addAmount = parseInt(match[1])
|
||||
const subtractAmount = parseInt(match[2])
|
||||
return {
|
||||
newValue: currentValue + addAmount - subtractAmount,
|
||||
operation: 'complement',
|
||||
addAmount,
|
||||
subtractAmount
|
||||
}
|
||||
}
|
||||
} else if (term.startsWith('-')) {
|
||||
// Pure subtraction like "-6"
|
||||
const amount = parseInt(term.substring(1))
|
||||
return {
|
||||
newValue: currentValue - amount,
|
||||
operation: 'subtract',
|
||||
subtractAmount: amount
|
||||
}
|
||||
} else {
|
||||
// Pure addition like "10"
|
||||
const amount = parseInt(term)
|
||||
return {
|
||||
newValue: currentValue + amount,
|
||||
operation: 'add',
|
||||
addAmount: amount
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return {
|
||||
newValue: currentValue,
|
||||
operation: 'add'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bead movements for a specific step
|
||||
*/
|
||||
function calculateStepBeadMovements(
|
||||
fromState: AbacusState,
|
||||
toState: AbacusState,
|
||||
stepIndex: number
|
||||
): StepBeadHighlight[] {
|
||||
|
||||
const { additions, removals } = calculateBeadChanges(fromState, toState)
|
||||
const movements: StepBeadHighlight[] = []
|
||||
|
||||
// Convert additions to step bead movements
|
||||
additions.forEach((bead, index) => {
|
||||
movements.push({
|
||||
...bead,
|
||||
stepIndex,
|
||||
direction: 'activate',
|
||||
order: index
|
||||
})
|
||||
})
|
||||
|
||||
// Convert removals to step bead movements
|
||||
removals.forEach((bead, index) => {
|
||||
movements.push({
|
||||
...bead,
|
||||
stepIndex,
|
||||
direction: 'deactivate',
|
||||
order: additions.length + index
|
||||
})
|
||||
})
|
||||
|
||||
return movements
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate English instruction based on actual bead movements
|
||||
*/
|
||||
function generateStepInstruction(
|
||||
beadMovements: StepBeadHighlight[],
|
||||
mathematicalTerm: string,
|
||||
stepResult: any
|
||||
): string {
|
||||
|
||||
if (beadMovements.length === 0) {
|
||||
return 'No bead movements required'
|
||||
}
|
||||
|
||||
// Group by place and direction
|
||||
const byPlace: { [place: number]: { adds: StepBeadHighlight[], removes: StepBeadHighlight[] } } = {}
|
||||
|
||||
beadMovements.forEach(bead => {
|
||||
if (!byPlace[bead.placeValue]) {
|
||||
byPlace[bead.placeValue] = { adds: [], removes: [] }
|
||||
}
|
||||
|
||||
if (bead.direction === 'activate') {
|
||||
byPlace[bead.placeValue].adds.push(bead)
|
||||
} else {
|
||||
byPlace[bead.placeValue].removes.push(bead)
|
||||
}
|
||||
})
|
||||
|
||||
// Generate instruction for each place
|
||||
const instructions: string[] = []
|
||||
|
||||
Object.keys(byPlace)
|
||||
.map(p => parseInt(p))
|
||||
.sort((a, b) => b - a) // Pedagogical order: highest place first
|
||||
.forEach(place => {
|
||||
|
||||
const placeName = place === 0 ? 'ones' :
|
||||
place === 1 ? 'tens' :
|
||||
place === 2 ? 'hundreds' : `place ${place}`
|
||||
|
||||
const placeData = byPlace[place]
|
||||
|
||||
// Handle additions
|
||||
if (placeData.adds.length > 0) {
|
||||
const instruction = generatePlaceInstruction(placeData.adds, 'add', placeName)
|
||||
instructions.push(instruction)
|
||||
}
|
||||
|
||||
// Handle removals
|
||||
if (placeData.removes.length > 0) {
|
||||
const instruction = generatePlaceInstruction(placeData.removes, 'remove', placeName)
|
||||
instructions.push(instruction)
|
||||
}
|
||||
})
|
||||
|
||||
return instructions.join(', then ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate instruction for a specific place
|
||||
*/
|
||||
function generatePlaceInstruction(
|
||||
beads: StepBeadHighlight[],
|
||||
action: 'add' | 'remove',
|
||||
placeName: string
|
||||
): string {
|
||||
|
||||
const heavenBeads = beads.filter(b => b.beadType === 'heaven')
|
||||
const earthBeads = beads.filter(b => b.beadType === 'earth')
|
||||
|
||||
const parts: string[] = []
|
||||
|
||||
if (heavenBeads.length > 0) {
|
||||
const verb = action === 'add' ? 'add' : 'remove'
|
||||
parts.push(`${verb} heaven bead in ${placeName} column`)
|
||||
}
|
||||
|
||||
if (earthBeads.length > 0) {
|
||||
const verb = action === 'add' ? 'add' : 'remove'
|
||||
const count = earthBeads.length
|
||||
const beadText = count === 1 ? 'earth bead' : `${count} earth beads`
|
||||
parts.push(`${verb} ${beadText} in ${placeName} column`)
|
||||
}
|
||||
|
||||
return parts.join(' and ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that all aspects of a step are consistent
|
||||
*/
|
||||
function validateStepConsistency(
|
||||
mathematicalTerm: string,
|
||||
englishInstruction: string,
|
||||
startValue: number,
|
||||
expectedValue: number,
|
||||
beadMovements: StepBeadHighlight[]
|
||||
): { isValid: boolean; issues: string[] } {
|
||||
|
||||
const issues: string[] = []
|
||||
|
||||
// Validate that bead movements produce the expected value
|
||||
const startState = numberToAbacusState(startValue)
|
||||
const expectedState = numberToAbacusState(expectedValue)
|
||||
|
||||
// Apply bead movements to start state
|
||||
let simulatedState = { ...startState }
|
||||
beadMovements.forEach(movement => {
|
||||
if (movement.direction === 'activate') {
|
||||
if (movement.beadType === 'heaven') {
|
||||
simulatedState[movement.placeValue].heavenActive = true
|
||||
} else {
|
||||
simulatedState[movement.placeValue].earthActive++
|
||||
}
|
||||
} else {
|
||||
if (movement.beadType === 'heaven') {
|
||||
simulatedState[movement.placeValue].heavenActive = false
|
||||
} else {
|
||||
simulatedState[movement.placeValue].earthActive--
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Check if simulated state matches expected state
|
||||
for (const place in expectedState) {
|
||||
const placeNum = parseInt(place)
|
||||
const expected = expectedState[placeNum]
|
||||
const simulated = simulatedState[placeNum]
|
||||
|
||||
if (expected.heavenActive !== simulated.heavenActive) {
|
||||
issues.push(`Place ${place}: heaven bead mismatch`)
|
||||
}
|
||||
|
||||
if (expected.earthActive !== simulated.earthActive) {
|
||||
issues.push(`Place ${place}: earth bead count mismatch`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: issues.length === 0,
|
||||
issues
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full pedagogical decomposition string
|
||||
*/
|
||||
function buildFullDecomposition(
|
||||
startValue: number,
|
||||
targetValue: number,
|
||||
terms: string[]
|
||||
): string {
|
||||
|
||||
const difference = targetValue - startValue
|
||||
const termString = terms.join(' + ').replace('+ -', '- ')
|
||||
|
||||
return `${startValue} + ${difference} = ${startValue} + ${termString} = ${targetValue}`
|
||||
}
|
||||
Reference in New Issue
Block a user