242 lines
8.7 KiB
JavaScript

/**
* 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)
);
// Recursive backtracking to carve paths
const visited = Array(rows).fill(null).map(() => Array(cols).fill(false));
function carve(row, col) {
visited[row][col] = true;
grid[row * 2 + 1][col * 2 + 1] = false; // Carve cell
// Get neighbors in random order
const directions = [
[-1, 0], [1, 0], [0, -1], [0, 1]
].sort(() => Math.random() - 0.5);
for (const [dr, dc] of directions) {
const newRow = row + dr;
const newCol = col + dc;
if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols && !visited[newRow][newCol]) {
// Carve wall between cells
grid[row * 2 + 1 + dr][col * 2 + 1 + dc] = false;
carve(newRow, newCol);
}
}
}
// 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 = rows - 1;
break;
case 'left':
startRow = 0; startCol = 0;
break;
case 'right':
startRow = 0; startCol = cols - 1;
break;
}
// Start carving from position near entrance
carve(startRow, startCol);
// 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 += `<rect x='${x}' y='${y}' width='${cellSize}' height='${cellSize}' fill='${wallColor}'/>`;
}
}
}
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 marker: Black circle in white path -->
<circle cx="${startX}" cy="${startY}" r="${radius}" fill="#000000" stroke="#FFFFFF" stroke-width="2"/>
<text x="${startX}" y="${startY + radius + 12}" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" font-weight="bold" fill="#000000">START</text>
<!-- GOAL marker: White circle with flag in white path -->
<circle cx="${endX}" cy="${endY}" r="${radius}" fill="#FFFFFF" stroke="#000000" stroke-width="2"/>
<text x="${endX}" y="${endY + radius + 12}" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" font-weight="bold" fill="#000000">GOAL</text>
`;
}
/**
* 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 {
rows = 6;
cols = 6;
}
// 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 = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 500 500'>
<!-- Maze background -->
<rect x='${startX - 3}' y='${startY - 3}' width='${mazeWidth + 6}' height='${mazeHeight + 6}' fill='#FFFFFF' stroke='#333333' stroke-width='2' rx='3'/>
<!-- Maze walls -->
${mazeGridToSVG(grid, startX, startY, cellSize)}
<!-- Start/End markers -->
${generateMarkers(mazeStartX, mazeStartY, mazeEndX, mazeEndY, theme, cellSize)}
</svg>`;
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 };