/** * Maze Generator * Creates algorithmically generated, guaranteed-solvable mazes */ /** * Generate a maze using recursive backtracking algorithm * Returns a 2D grid where true = wall, false = path */ function generateMazeGrid(rows, cols, startEdge, endEdge) { // Initialize grid with all walls const grid = Array(rows * 2 + 1).fill(null).map(() => Array(cols * 2 + 1).fill(true) ); // Prim's algorithm to carve paths const visited = Array(rows).fill(null).map(() => Array(cols).fill(false)); function addWalls(r, c, walls) { const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]; for (const [dr, dc] of directions) { const nr = r + dr; const nc = c + dc; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && !visited[nr][nc]) { walls.push({ row: r, col: c, dr, dc }); } } } // Determine starting cell for carving based on entrance position let startRow = 0, startCol = 0; switch (startEdge) { case 'top': startRow = 0; startCol = 0; break; case 'bottom': startRow = rows - 1; startCol = cols - 1; break; case 'left': startRow = 0; startCol = 0; break; case 'right': startRow = 0; startCol = cols - 1; break; } // Start Prim's algorithm from position near entrance visited[startRow][startCol] = true; grid[startRow * 2 + 1][startCol * 2 + 1] = false; const walls = []; addWalls(startRow, startCol, walls); while (walls.length > 0) { // Pick a random wall const wallIndex = Math.floor(Math.random() * walls.length); const { row, col, dr, dc } = walls[wallIndex]; // Remove the wall walls.splice(wallIndex, 1); const newRow = row + dr; const newCol = col + dc; // If the cell on the other side isn't visited yet if (!visited[newRow][newCol]) { // Carve the cell visited[newRow][newCol] = true; grid[newRow * 2 + 1][newCol * 2 + 1] = false; // Carve the wall between them grid[row * 2 + 1 + dr][col * 2 + 1 + dc] = false; // Add the new cell's walls addWalls(newRow, newCol, walls); } } // Track entrance and exit positions for marker placement let entranceGridPos = { row: 0, col: 0 }; let exitGridPos = { row: 0, col: 0 }; // Create entrance based on startEdge - opening in the outer wall // Place in first third of edge for variety switch (startEdge) { case 'top': const topEntranceCol = 1 + Math.floor(Math.random() * Math.max(1, cols / 3)) * 2; grid[0][topEntranceCol] = false; entranceGridPos = { row: 0, col: topEntranceCol }; break; case 'bottom': const bottomEntranceCol = 1 + Math.floor(Math.random() * Math.max(1, cols / 3)) * 2; grid[rows * 2][bottomEntranceCol] = false; entranceGridPos = { row: rows * 2, col: bottomEntranceCol }; break; case 'left': const leftEntranceRow = 1 + Math.floor(Math.random() * Math.max(1, rows / 3)) * 2; grid[leftEntranceRow][0] = false; entranceGridPos = { row: leftEntranceRow, col: 0 }; break; case 'right': const rightEntranceRow = 1 + Math.floor(Math.random() * Math.max(1, rows / 3)) * 2; grid[rightEntranceRow][cols * 2] = false; entranceGridPos = { row: rightEntranceRow, col: cols * 2 }; break; } // Create exit based on endEdge - opening in the outer wall // Place in last third of edge to be far from entrance switch (endEdge) { case 'top': const topExitCol = cols * 2 - 1 - Math.floor(Math.random() * Math.max(1, cols / 3)) * 2; grid[0][topExitCol] = false; exitGridPos = { row: 0, col: topExitCol }; break; case 'bottom': const bottomExitCol = cols * 2 - 1 - Math.floor(Math.random() * Math.max(1, cols / 3)) * 2; grid[rows * 2][bottomExitCol] = false; exitGridPos = { row: rows * 2, col: bottomExitCol }; break; case 'left': const leftExitRow = rows * 2 - 1 - Math.floor(Math.random() * Math.max(1, rows / 3)) * 2; grid[leftExitRow][0] = false; exitGridPos = { row: leftExitRow, col: 0 }; break; case 'right': const rightExitRow = rows * 2 - 1 - Math.floor(Math.random() * Math.max(1, rows / 3)) * 2; grid[rightExitRow][cols * 2] = false; exitGridPos = { row: rightExitRow, col: cols * 2 }; break; } return { grid, entranceGridPos, exitGridPos }; } /** * Convert maze grid to SVG */ function mazeGridToSVG(grid, startX, startY, cellSize) { let svg = ''; const wallColor = '#333333'; for (let row = 0; row < grid.length; row++) { for (let col = 0; col < grid[row].length; col++) { if (grid[row][col]) { const x = startX + col * cellSize; const y = startY + row * cellSize; svg += ``; } } } return svg; } /** * Generate START and GOAL markers */ function generateMarkers(startX, startY, endX, endY, theme, cellSize) { // Smaller icon size to fit in path cells const iconSize = Math.min(cellSize * 0.6, 20); const radius = iconSize / 2; return ` START GOAL `; } /** * Generate complete maze worksheet * @param {Object} params * @param {string} params.theme - Theme for decorations * @param {number} params.difficulty - 1-3 (affects grid size) * @param {number} params.age - Child's age */ export function generateMazeWorksheet({ theme, difficulty = 1, age }) { // Grid size based on difficulty and age let rows, cols; if (age <= 4 || difficulty === 1) { rows = 4; cols = 4; } else if (age <= 5 || difficulty === 2) { rows = 5; cols = 5; } else if (age <= 7) { rows = 6; cols = 6; } else if (age <= 9) { rows = 7; cols = 7; } else { rows = 8; cols = 8; } // Randomly select start and end edges - favor opposite edges for better gameplay const edges = ['top', 'bottom', 'left', 'right']; const startEdge = edges[Math.floor(Math.random() * edges.length)]; // 80% chance to pick opposite edge, 20% chance for any different edge let endEdge; const oppositeEdges = { 'top': 'bottom', 'bottom': 'top', 'left': 'right', 'right': 'left' }; if (Math.random() < 0.8) { // Pick opposite edge endEdge = oppositeEdges[startEdge]; } else { // Pick any different edge const remainingEdges = edges.filter(e => e !== startEdge); endEdge = remainingEdges[Math.floor(Math.random() * remainingEdges.length)]; } console.log(`Maze edges: START=${startEdge}, END=${endEdge}`); const { grid, entranceGridPos, exitGridPos } = generateMazeGrid(rows, cols, startEdge, endEdge); // Calculate positioning to fill the space const gridWidth = cols * 2 + 1; const gridHeight = rows * 2 + 1; const cellSize = Math.min(480 / gridWidth, 480 / gridHeight); const mazeWidth = gridWidth * cellSize; const mazeHeight = gridHeight * cellSize; const startX = (500 - mazeWidth) / 2; const startY = (500 - mazeHeight) / 2; // Calculate marker positions based on actual entrance/exit grid positions // Add half cellSize to center in the path cell (not on the edge) const mazeStartX = startX + entranceGridPos.col * cellSize + cellSize / 2; const mazeStartY = startY + entranceGridPos.row * cellSize + cellSize / 2; const mazeEndX = startX + exitGridPos.col * cellSize + cellSize / 2; const mazeEndY = startY + exitGridPos.row * cellSize + cellSize / 2; const svg = ` ${mazeGridToSVG(grid, startX, startY, cellSize)} ${generateMarkers(mazeStartX, mazeStartY, mazeEndX, mazeEndY, theme, cellSize)} `; return { title: theme ? `${theme} Maze Adventure` : 'Maze Adventure', subtitle: 'Find the path from start to finish!', sections: [{ id: 'find-the-way-main', type: 'find-the-way', content_svg: svg }] }; } export default { generateMazeWorksheet };