248 lines
8.8 KiB
JavaScript
248 lines
8.8 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 = cols - 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 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 = `<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 };
|