javaScript 生成随机迷宫
HTML代码
<svg
viewBox="0 0 1000 1000"
width="1000"
height="1000"
role="img"
>
<title>
A square maze.
</title>
<style>
g {
--cell-size: 50px;
--stroke-width: 10px;
}
.grid-line {
fill: none;
stroke: var(--maze-stroke-color);
stroke-width: var(--stroke-width);
stroke-linecap: square;
}
.maze-path {
fill: none;
stroke: red;
stroke-width: 10;
stroke: white;
stroke-width: calc(var(--cell-size) - var(--stroke-width));
stroke-linecap: square;
}
</style>
<g class="grid"></g>
<g class="pattern">
<!--
Our graphics code goes here
-->
</g>
</svg>
<div class="buttons">
<button class="randomize">绘制路径</button>
</div>
javaScript代码
// 引入随机数帮助函数,用于生成随机整数、从数组中随机选项,以及随机概率判断
import { randomInt, randomItemInArray, randomChance } from 'https://unpkg.com/randomness-helpers@0.0.1/dist/index.js';
// 获取 SVG 元素、迷宫图案元素和网格元素
const svgEl = document.querySelector('svg');
const patternEl = document.querySelector('.pattern');
const gridEl = document.querySelector('.grid');
// 网格宽度和高度(单位:格子数)
const gridWidth = 20;
const gridHeight = 20;
// 每个格子的缩放比例
const scale = 50;
// 随机分支路径的生成概率
const splittingChance = 0.1;
// 动画播放的速度,单位:毫秒
const animationSpeed = 20;
// 失败重试的最大次数
const retryLimit = 30;
let interval; // 存储动画间隔
let failedCount = 0; // 记录失败的次数
let mainPathPoints = []; // 存储主路径的点
let otherPaths = []; // 存储其他分支路径
// 初始化网格数据
let gridData = buildFreshGrid();
// 绘制网格函数
function drawGrid() {
let gridMarkup = '';
// 绘制水平网格线
for (let y = 0; y <= gridHeight; y++) {
gridMarkup += `
<line
class="grid-line"
x1="0"
x2="${gridWidth * scale}"
y1="${y * scale}"
y2="${y * scale}"
/>
`;
}
// 绘制垂直网格线
for (let x = 0; x <= gridWidth; x++) {
gridMarkup += `
<line
class="grid-line"
y1="0"
y2="${gridHeight * scale}"
x1="${x * scale}"
x2="${x * scale}"
/>
`;
}
return gridMarkup;
}
// 构建一个全新的空网格,初始值为 0
function buildFreshGrid() {
return new Array(gridHeight).fill().map(
() => new Array(gridWidth).fill(0)
);
}
// 调整迷宫点的位置,使其适应缩放比例
function adjustMazePoint(point) {
return {
x: scale / 2 + point.x * scale,
y: scale / 2 + point.y * scale
}
}
// 将路径点转换为路径数据(SVG 的路径命令)
function buildPathData(points) {
points = points.map(adjustMazePoint);
const firstPoint = points.shift(); // 获取第一个点
let commands = [`M ${firstPoint.x}, ${firstPoint.y}`]; // 起始点命令
// 遍历后续的点,生成路径命令
points.forEach(point => {
commands.push(`L ${point.x}, ${point.y}`); // 连接命令
});
return commands.join(' '); // 返回完整的路径命令
}
// 绘制路径(接受点和可选的类名)
function drawLine(points, className = '') {
return `
<path
class="maze-path ${className}"
d="${buildPathData(points)}"
/>
`;
}
// 生成主路径的起始点
function mainPathStartPoints() {
const yStart = randomInt(0, gridHeight - 1); // 随机选择起始行
return [
{ x: -1, y: yStart }, // 起始点在左侧外面
{ x: 0, y: yStart } // 入口点位于网格内
]
}
// 标记网格中的一个点为已占用
function markPointAsTaken(point, value = 1) {
gridData[point.y][point.x] = value;
}
// 寻找当前点的下一个可行点(四个方向)
function findNextPoint(point) {
const potentialPoints = [];
// 检查上方
if (gridData[point.y - 1]?.[point.x] === 0) {
potentialPoints.push({ y: point.y - 1, x: point.x });
}
// 检查下方
if (gridData[point.y + 1]?.[point.x] === 0) {
potentialPoints.push({ y: point.y + 1, x: point.x });
}
// 检查左方
if (gridData[point.y]?.[point.x - 1] === 0) {
potentialPoints.push({ y: point.y, x: point.x - 1 });
}
// 检查右方
if (gridData[point.y]?.[point.x + 1] === 0) {
potentialPoints.push({ y: point.y, x: point.x + 1 });
}
// 如果没有可行点,返回 undefined
if (potentialPoints.length === 0) {
return undefined;
}
// 随机选择一个方向
return randomItemInArray(potentialPoints);
}
// 重置迷宫状态
function refreshState() {
mainPathPoints = mainPathStartPoints(); // 重新设置主路径起点
gridData = buildFreshGrid(); // 重建空网格
markPointAsTaken(mainPathPoints.at(-1)); // 标记起点为占用
otherPaths = []; // 清空其他路径
failedCount = 0; // 重置失败计数
}
// 绘制路径
function drawLines() {
let markup = '';
// 绘制分支路径
markup += otherPaths.map(drawLine).join('');
// 绘制主路径
markup += drawLine(mainPathPoints, 'main');
patternEl.innerHTML = markup; // 将路径渲染到页面
}
// 构建主路径
function buildMainPath() {
refreshState(); // 重置迷宫状态
drawLines(); // 绘制初始路径
// 启动一个定时器来逐步绘制路径
interval = setInterval(() => {
const nextPoint = findNextPoint(mainPathPoints.at(-1)); // 找到下一个路径点
// 如果找不到路径点,判断是否失败次数超过限制
if (!nextPoint) {
if (failedCount > retryLimit) {
refreshState(); // 超过限制,重置迷宫
} else {
failedCount++; // 增加失败次数
for (let i = 0; i < failedCount; i++) {
markPointAsTaken(mainPathPoints.pop(), 0); // 回溯并标记点为未占用
}
}
} else {
mainPathPoints.push(nextPoint); // 添加新的路径点
markPointAsTaken(nextPoint); // 标记新点为已占用
// 如果到达右边缘,结束主路径绘制
if (nextPoint.x === gridWidth - 1) {
mainPathPoints.push({ x: nextPoint.x + 1, y: nextPoint.y });
clearInterval(interval); // 停止定时器
buildOtherPaths(); // 开始生成其他分支路径
}
}
drawLines(); // 绘制当前的路径
}, animationSpeed);
}
// 为迷宫添加更多分支路径
function addMorePaths() {
gridData.forEach((row, y) => {
row.forEach((cell, x) => {
// 如果该格子已占用且随机生成分支路径
if (cell && randomChance(splittingChance)) {
otherPaths.push([{ y, x }]);
}
})
});
}
// 判断迷宫是否完成(所有格子都被占用)
function mazeComplete() {
return gridData.flat().every(cell => cell === 1);
}
// 生成其他分支路径
function buildOtherPaths() {
interval = setInterval(() => {
addMorePaths(); // 添加分支路径
otherPaths.forEach((path) => {
const nextPoint = findNextPoint(path.at(-1)); // 找到下一个路径点
if (nextPoint) {
path.push(nextPoint); // 添加路径点
markPointAsTaken(nextPoint); // 标记为已占用
}
});
drawLines(); // 绘制路径
// 如果迷宫已完成,停止定时器
if (mazeComplete()) {
clearInterval(interval);
console.log('maze done');
}
}, animationSpeed);
}
// 绘制迷宫并初始化视图
function draw() {
gridEl.innerHTML = drawGrid(); // 绘制网格
const mazeWidth = gridWidth * scale;
const mazeHeight = gridHeight * scale;
svgEl.setAttribute('viewBox', `0 0 ${mazeWidth} ${mazeHeight}`);
svgEl.setAttribute('width', mazeWidth);
svgEl.setAttribute('height', mazeHeight);
patternEl.innerHTML = ''; // 清空已有路径
clearInterval(interval); // 清除现有的定时器
buildMainPath(); // 开始生成主路径
}
// 初始化迷宫
draw();
// 绑定“随机化”按钮的点击事件,重新生成迷宫
document.querySelector('.randomize').addEventListener('click', draw);
CSS代码
html, body {
--hue: 205;
display: grid;
place-items: center;
min-height: 100%;
--maze-bg-color: hsl(270deg, 15%, 44%);
--maze-stroke-color: hsl(270deg, 15%, 40%);
}
svg {
background: #fff;
max-width: 90vw;
max-height: 85vh;
width: auto;
height: auto;
overflow: visible;
background: var(--maze-bg-color);
}
button {
/* Text Colors */
--text-saturation: 90%;
--text-lightness: 40%;
--text-saturation-hover: calc(var(--text-saturation) + 10%);
--text-lightness-hover: calc(var(--text-lightness) - 5%); ;
--text-saturation-active: var(--text-saturation-hover);
--text-lightness-active: calc(var(--text-lightness) - 10%); ;
--text-saturation-disabled: calc(var(--text-saturation) - 60%);
--text-lightness-disabled: calc(var(--text-lightness) + 10%);
/* Background Colors */
--background-saturation: 0%;
--background-lightness: 100%;
--background-saturation-hover: calc(var(--background-saturation) + 80%);
--background-lightness-hover: calc(var(--background-lightness) - 5%);
--background-saturation-active: var(--background-saturation-hover);
--background-lightness-active: calc(var(--background-lightness) - 10%);
--background-saturation-disabled: calc(var(--background-saturation) + 30%);
--background-lightness-disabled: calc(var(--background-lightness) - 10%);
/* Border Colors */
--border-saturation: 90%;
--border-lightness: 60%;
--border-saturation-hover: calc(var(--border-saturation) + 10%);
--border-lightness-hover: calc(var(--border-lightness) - 10%);
--border-saturation-active: var(--border-saturation-hover);
--border-lightness-active: calc(var(--border-lightness) - 20%);
--border-saturation-disabled: calc(var(--border-saturation) - 60%);
--border-lightness-disabled: calc(var(--border-lightness) + 20%);
/* Focus shadow styles */
--shadow-saturation-focus: 100%;
--shadow-lightness-focus: 85%;
/* Color Styles */
color: hsl(var(--hue), var(--text-saturation), var(--text-lightness));
background-color: hsl(var(--hue), var(--background-saturation), var(--background-lightness));
border:0.1em solid hsl(var(--hue), var(--border-saturation), var(--border-lightness));
/* Misc. Styles */
border-radius: 0.25em;
cursor: pointer;
display: inline-block;
font-size: 1em;
padding: 0.5em 1em;
transition-property: box-shadow, background-color, border-color, color;
transition-timing-function: ease-out;
transition-duration: 0.2s;
}
button:hover {
color: hsl(
var(--hue),
var(--text-saturation-hover),
var(--text-lightness-hover)
);
background-color: hsl(
var(--hue),
var(--background-saturation-hover),
var(--background-lightness-hover)
);
border-color: hsl(
var(--hue),
var(--border-saturation-hover),
var(--border-lightness-hover)
);
}
button:active {
color: hsl(
var(--hue),
var(--text-saturation-active),
var(--text-lightness-active)
);
background-color: hsl(
var(--hue),
var(--background-saturation-active),
var(--background-lightness-active)
);
border-color: hsl(
var(--hue),
var(--border-saturation-active),
var(--border-lightness-active)
);
}
button:focus {
outline: none;
box-shadow: 0 0 0 0.25em hsl(
var(--hue),
var(--shadow-saturation-focus),
var(--shadow-lightness-focus)
);
}
.buttons {
display: flex;
gap: 0.5em;
}
- 感谢你赐予我前进的力量
赞赏者名单
因为你们的支持让我意识到写文章的价值🙏
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 小陈同学
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果