/**
* 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 = ``;
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 };