/** * Counting Worksheet Generator * Generates randomized counting exercises with themed objects */ // Simple themed object SVG shapes const themedObjects = { star: ``, heart: ``, circle: ``, apple: ``, fish: ``, flower: ``, butterfly: ``, ball: ``, moon: ``, cloud: `` }; const objectNames = Object.keys(themedObjects); /** * Pluralize object names correctly */ function pluralize(word) { const irregulars = { 'fish': 'Fish', 'butterfly': 'Butterflies', 'moon': 'Moons' }; if (irregulars[word]) return irregulars[word]; return word.charAt(0).toUpperCase() + word.slice(1) + 's'; } /** * Generate random quantities that are all different */ function generateRandomQuantities(count, maxValue) { const quantities = []; const used = new Set(); while (quantities.length < count) { const num = Math.floor(Math.random() * maxValue) + 1; if (!used.has(num)) { used.add(num); quantities.push(num); } } // Shuffle the array return quantities.sort(() => Math.random() - 0.5); } /** * Pick object type based on theme keywords with randomization */ function pickObjectForTheme(theme, llmSuggestion) { // Try to use LLM suggestion first if valid if (llmSuggestion && themedObjects[llmSuggestion]) { return llmSuggestion; } if (!theme) { return objectNames[Math.floor(Math.random() * objectNames.length)]; } const themeLower = theme.toLowerCase(); // Direct matches for (const name of objectNames) { if (themeLower.includes(name)) { return name; } } // Build list of ALL matching shapes, then pick randomly const associations = { star: ['space', 'night', 'sky', 'twinkle', 'magic', 'galaxy', 'astronaut', 'king', 'crown', 'royal'], heart: ['love', 'valentine', 'romance', 'caring', 'friendship', 'kind', 'help'], circle: ['round', 'bubble', 'dot'], apple: ['fruit', 'food', 'tree', 'garden', 'healthy', 'snack', 'farm', 'autumn', 'fall'], fish: ['ocean', 'sea', 'water', 'aquarium', 'swim', 'underwater', 'beach', 'marine', 'animal'], flower: ['garden', 'plant', 'nature', 'spring', 'rose', 'bloom', 'petal', 'beautiful'], butterfly: ['insect', 'bug', 'garden', 'spring', 'colorful', 'wings', 'animal', 'fly'], ball: ['sport', 'play', 'game', 'soccer', 'football', 'basketball', 'tennis', 'cricket', 'kick'], moon: ['night', 'sleep', 'bedtime', 'dream', 'space', 'lunar', 'dark'], cloud: ['sky', 'weather', 'rain', 'sunny', 'day', 'fluffy', 'soft'] }; // Collect ALL matching shapes const matchingShapes = []; for (const [objectType, keywords] of Object.entries(associations)) { if (keywords.some(keyword => themeLower.includes(keyword))) { matchingShapes.push(objectType); } } // If we found matches, pick one randomly for variety if (matchingShapes.length > 0) { return matchingShapes[Math.floor(Math.random() * matchingShapes.length)]; } // Random fallback return objectNames[Math.floor(Math.random() * objectNames.length)]; } /** * Generate a group of objects at a position */ function generateObjectGroup(objectType, count, x, y, groupWidth) { const objectSvg = themedObjects[objectType] || themedObjects.star; const objectSize = 22; const maxPerRow = 5; let svg = ''; if (count <= maxPerRow) { // Single row const spacing = Math.min(28, (groupWidth - count * objectSize) / (count + 1)); let startX = x + spacing; for (let i = 0; i < count; i++) { svg += `${objectSvg}`; } } else { // Two rows for larger counts const topRowCount = Math.ceil(count / 2); const bottomRowCount = count - topRowCount; const spacing = Math.min(26, (groupWidth - topRowCount * objectSize) / (topRowCount + 1)); // Top row let startX = x + spacing; for (let i = 0; i < topRowCount; i++) { svg += `${objectSvg}`; } // Bottom row startX = x + spacing + (topRowCount - bottomRowCount) * (objectSize + spacing) / 2; for (let i = 0; i < bottomRowCount; i++) { svg += `${objectSvg}`; } } return svg; } /** * Generate an empty answer box */ function generateAnswerBox(x, y, size = 40) { return ``; } /** * Generate complete counting worksheet * @param {Object} params * @param {string} params.theme - Theme name (optional) * @param {string} params.shape - Shape chosen by LLM (e.g. 'fish', 'star') * @param {number[]} params.quantities - Array of quantities from LLM * @param {number} params.age - Child's age */ export function generateCountingWorksheet({ theme, shape, quantities, age }) { // Use hybrid approach: LLM suggestion + programmatic randomization from matching shapes const objectType = pickObjectForTheme(theme, shape); // ALWAYS generate random quantities for variety (LLM is deterministic) let maxValue; if (age <= 4) { maxValue = 5; } else if (age <= 6) { maxValue = 10; } else if (age <= 8) { maxValue = 15; } else { maxValue = 20; } const rowCount = 8; const finalQuantities = generateRandomQuantities(rowCount, maxValue); const rowHeight = 62; // 500 / 8 = 62.5, fill entire height const startY = 0; // Start at absolute top const objectAreaWidth = 320; const answerBoxX = 440; const answerBoxSize = 42; let objectRows = ''; for (let i = 0; i < rowCount; i++) { const rowY = startY + i * rowHeight; const quantity = finalQuantities[i]; // Dotted separator line between rows (except first row) if (i > 0) { objectRows += ``; } // Row number indicator - vertically centered in row objectRows += `${i + 1}.`; // Objects to count - positioned in middle of row objectRows += generateObjectGroup(objectType, quantity, 50, rowY + 20, objectAreaWidth); // Equals sign objectRows += `=`; // Answer box objectRows += generateAnswerBox(answerBoxX, rowY + 10, answerBoxSize); } const svg = ` ${objectRows} `; return { title: `Count the ${pluralize(objectType)}!`, subtitle: 'Count each group and write the number', sections: [{ id: 'counting-main', type: 'counting', content_svg: svg }] }; } export default { generateCountingWorksheet };