从一个视频到完整项目:写一个 Micromouse 电子鼠迷宫寻路模拟器

起源
前几天刷视频,偶然看到了一个关于 Micromouse(电子鼠) 迷宫竞赛的视频。视频中,一只小小的机器鼠在复杂的迷宫中快速穿梭,以惊人的速度找到通往终点的最短路径。那种"探索-记忆-规划-冲刺"的智能行为深深吸引了我。
作为程序员的本能反应------ "我也能做一个!"
于是,利用AI,我开发了这款 Micromouse Simulator,一个完全运行在浏览器中的电子鼠迷宫寻路模拟器。
🎮 在线体验地址:点击体验
什么是 Micromouse 竞赛?
Micromouse 是一项经典的机器人竞赛,起源于 1970 年代,现已成为 IEEE 等组织认可的标准化比赛项目。
比赛规则
-
迷宫规格:标准 16×16 格的迷宫,每个格子的墙壁信息未知
-
起点与终点:起点固定在左下角,终点是中心 4 个格子
-
机器人限制:
- 必须自主导航,不能远程控制
- 只能通过传感器探测周围墙壁
- 需要在有限时间内完成探索和冲刺
核心挑战
Micromouse 的精髓在于:
┌─────────────────────────────────────────────┐
│ 未知环境 → 局部感知 → 自主决策 → 最优路径 │
└─────────────────────────────────────────────┘
这与传统的"上帝视角"寻路完全不同!机器人必须:
- 逐步探索:只能看到当前格子的四面墙
- 地图构建:在内存中维护"已知地图"
- 路径规划:基于不完全信息做出决策
- 优化冲刺:找到最短路径后快速到达
项目架构设计
技术栈选择
| 类别 | 技术选型 | 选择理由 |
|---|---|---|
| 框架 | React 19 + TypeScript | 类型安全,组件化开发 |
| 状态管理 | Zustand | 轻量级,支持持久化 |
| 可视化 | HTML5 Canvas | 高性能动画渲染 |
| 样式 | Tailwind CSS | 快速构建现代化界面 |
| 构建 | Vite | 极速开发体验 |
目录结构
src/
├── components/ # UI 组件
│ ├── MazeBoard.tsx # Canvas 迷宫渲染
│ ├── ControlPanel.tsx # 控制面板
│ ├── StatsPanel.tsx # 实时统计
│ └── HistoryList.tsx # 历史记录
├── core/ # 核心算法
│ ├── algorithms/ # 寻路算法
│ │ └── index.ts # Flood Fill, A*, DFS, BFS
│ └── maze/ # 迷宫生成
│ └── generator.ts # 递归回溯, Prim算法
├── stores/ # 状态管理
│ └── simulatorStore.ts
└── types/ # TypeScript 类型定义
└── index.ts
核心数据结构
迷宫单元格
typescript
interface Cell {
north: boolean; // 是否有北墙
east: boolean; // 是否有东墙
south: boolean; // 是否有南墙
west: boolean; // 是否有西墙
visited: boolean; // 是否已访问(用于可视化)
isStart: boolean; // 是否是起点
isGoal: boolean; // 是否是终点
}
探索步骤(用于动画回放)
typescript
interface ExplorationStep {
position: Position; // 机器人位置
direction: Direction; // 机器人朝向
detectedWalls: Partial<Cell>; // 检测到的墙壁
action: 'move' | 'turn_left' | 'turn_right' | 'scan';
floodValue?: number; // Flood Fill 距离值
}
迷宫生成算法
递归回溯法(Recursive Backtracker)
这是生成"完美迷宫"的经典算法(任意两点间有且只有一条路径):
typescript
function generateMazeRecursiveBacktracker(): Cell[][] {
// 1. 初始化:所有墙壁都存在
const maze = createFullWallMaze();
// 2. 从起点开始,使用栈进行深度优先遍历
const stack: Position[] = [{ x: 0, y: 0 }];
const visited = new Set<string>();
while (stack.length > 0) {
const current = stack[stack.length - 1];
const neighbors = getUnvisitedNeighbors(current, visited);
if (neighbors.length === 0) {
// 死胡同,回溯
stack.pop();
} else {
// 随机选择邻居,移除之间的墙壁
const next = randomChoice(neighbors);
removeWallBetween(maze, current, next);
visited.add(key(next));
stack.push(next);
}
}
return maze;
}
效果示意:
┌─┬─┬─┬─┐ ┌─┬───┬─┐
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┤ ├─┼─┬─┼─┤
│ │ │ │ │ --> │ │ │ │ │
├─┼─┼─┼─┤ ├─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
└─┴─┴─┴─┘ └───┴─┴─┘
全墙壁状态 移除墙壁后
四种寻路算法实现
1. Flood Fill(洪水填充法)⭐ 推荐
这是 Micromouse 竞赛中最常用的算法!
核心思想:
1. 从目标(终点)开始,计算每个格子到终点的"距离"
2. 机器人始终向"距离值更小"的方向移动
3. 当发现新墙壁时,动态更新距离场
距离场计算(BFS 扩散):
typescript
function updateDistanceField(maze: Cell[][], field: DistanceField): void {
const queue: Position[] = [];
// 初始化:目标格子距离为 0
for (const goal of GOAL_POSITIONS) {
field[goal.y][goal.x] = 0;
queue.push(goal);
}
// BFS 向外扩散
while (queue.length > 0) {
const current = queue.shift()!;
const currentDist = field[current.y][current.x];
for (const neighbor of getAccessibleNeighbors(maze, current)) {
if (field[neighbor.y][neighbor.x] > currentDist + 1) {
field[neighbor.y][neighbor.x] = currentDist + 1;
queue.push(neighbor);
}
}
}
}
为什么 Flood Fill 最适合 Micromouse?
| 特性 | Flood Fill | A* | DFS | BFS |
|---|---|---|---|---|
| 支持动态更新 | ✅ 天然支持 | ❌ 需重新计算 | ❌ 需重新计算 | ❌ 需重新计算 |
| 内存效率 | ✅ 只存距离值 | ❌ 存开放/关闭列表 | ✅ 只存栈 | ❌ 存队列 |
| 最短路径 | ✅ 保证 | ✅ 保证 | ❌ 不保证 | ✅ 保证 |
| 实时性 | ✅ 高 | ⚠️ 中 | ✅ 高 | ⚠️ 中 |
2. A* 算法
使用启发式函数加速搜索:
typescript
function heuristic(pos: Position): number {
// 曼哈顿距离到最近目标
return Math.min(...GOAL_POSITIONS.map(g =>
Math.abs(pos.x - g.x) + Math.abs(pos.y - g.y)
));
}
// f(n) = g(n) + h(n)
// g(n): 从起点到当前的代价
// h(n): 从当前到目标的估计代价
3. DFS(深度优先搜索)
沿着一条路走到底,遇到死胡同再回溯:
typescript
while (stack.length > 0 && !found) {
const current = stack.pop()!;
if (isGoal(current.pos)) {
found = true;
break;
}
// 逆序添加邻居,保证优先方向
for (const neighbor of getNeighbors(current.pos).reverse()) {
if (!visited.has(key(neighbor))) {
stack.push(neighbor);
}
}
}
4. BFS(广度优先搜索)
逐层扩展,保证找到最短路径:
typescript
while (queue.length > 0 && !found) {
const current = queue.shift()!;
if (isGoal(current.pos)) {
found = true;
break;
}
for (const neighbor of getNeighbors(current.pos)) {
if (!visited.has(key(neighbor))) {
visited.add(key(neighbor));
queue.push(neighbor);
}
}
}
可视化实现
Canvas 渲染层次
┌─────────────────────────────────────┐
│ Layer 4: 机器人(三角形) │
│ ───────────────────────────────── │
│ Layer 3: 路径(蓝色线条) │
│ ───────────────────────────────── │
│ Layer 2: 探索范围(半透明覆盖) │
│ ───────────────────────────────── │
│ Layer 1: 墙壁(黑色线条) │
│ ───────────────────────────────── │
│ Layer 0: 单元格背景 │
└─────────────────────────────────────┘
动画循环
typescript
useEffect(() => {
if (runStatus !== 'running') return;
const interval = setInterval(() => {
advanceStep(); // 推进一步探索步骤
}, animationConfig.moveSpeed);
return () => clearInterval(interval);
}, [runStatus, animationConfig.moveSpeed]);
历史记录与回放
数据持久化
使用 Zustand 的 persist 中间件 + LocalStorage:
typescript
export const useSimulatorStore = create<SimulatorState>()(
persist(
(set, get) => ({
// ... state and actions
}),
{
name: 'micromouse-storage',
partialize: state => ({
historyRecords: state.historyRecords, // 只持久化历史记录
}),
}
)
);
回放机制
每次运行都会记录完整的探索步骤,用户可以点击历史记录进行回放:
typescript
interface PathRecord {
id: string;
timestamp: number;
algorithm: AlgorithmType;
mazeData: Cell[][]; // 迷宫数据
explorationSteps: ExplorationStep[]; // 完整探索步骤
path: Position[]; // 最终路径
totalSteps: number;
totalTime: number;
success: boolean;
}
效果展示
界面布局
┌────────────────────────────────────────────────────────────┐
│ 🤖 Micromouse Simulator - 电子鼠迷宫寻路模拟器 │
├──────────────┬─────────────────────────┬───────────────────┤
│ │ │ │
│ 控制面板 │ 迷宫可视化区域 │ 实时统计 │
│ │ │ │
│ ○ 迷宫类型 │ ┌─────────────────┐ │ 位置: (0, 15) │
│ ○ 算法选择 │ │ │ │ 朝向: ↑ 北 │
│ ○ 速度调节 │ │ 16x16 迷宫 │ │ 步数: 42 │
│ │ │ │ │ 时间: 123ms │
│ [开始运行] │ │ 🤖 → 🎯 │ │ │
│ [生成迷宫] │ │ │ ├───────────────────┤
│ │ └─────────────────┘ │ 历史记录 │
│ │ │ │
│ │ 图例: 🟢起点 🟡目标 │ □ 11:30 成功 │
│ │ 🔵已探索 🔴机器 │ □ 11:28 成功 │
│ │ │ □ 11:25 失败 │
└──────────────┴─────────────────────────┴───────────────────┘
算法对比
| 算法 | 平均步数 | 探索覆盖率 | 特点 |
|---|---|---|---|
| Flood Fill | ~120步 | 80%+ | 最适合动态环境 |
| A* | ~80步 | 60%+ | 高效但需全局信息 |
| DFS | ~200步 | 40%+ | 可能走弯路 |
| BFS | ~80步 | 80%+ | 保证最短但慢 |
遇到的坑与解决方案
1. 迷宫生成:从"空"到"满"
问题: 最初我从"没有墙"的状态开始生成迷宫,结果导致迷宫不完整,大量区域缺失墙壁。
解决: 改为从"所有墙壁都存在"的状态开始,通过"移除墙壁"来创建通道:
typescript
// ❌ 错误:从空开始
const maze = createEmptyMaze(); // 没有墙
addWalls(maze); // 尝试添加墙 → 不完整
// ✅ 正确:从满开始
const maze = createFullWallMaze(); // 所有墙都有
removeWalls(maze); // 移除墙创建通道 → 完美迷宫
2. Set 序列化问题
问题: Zustand persist 无法正确序列化 Set<string>,导致历史记录丢失。
解决: 改用数组:
typescript
// ❌ 无法序列化
exploredCells: Set<string>
// ✅ 可序列化
exploredCells: string[]
3. Vite React Refresh 冲突
问题: 开发环境下出现 Symbol "xxx" has already been declared 错误。
解决: 禁用 React Refresh,使用 classic JSX transform:
typescript
// vite.config.ts
export default defineConfig({
esbuild: {
jsx: 'transform',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
},
});
未来展望
这个项目还有很多可以扩展的方向:
- 迷宫编辑器:允许用户手动设计迷宫
- 传感器模拟:更真实的传感器模型(距离误差、噪声)
- 多机器人竞争:多只电子鼠同时探索
- 算法可视化对比:并列展示不同算法的执行过程
- 在线排行榜:记录最优解,支持社区挑战
总结
从一个 YouTube 视频到一个完整可运行的 Web 应用,这个项目让我深入理解了:
- 算法之美:Flood Fill 算法的优雅在于它完美契合"未知环境探索"的场景
- 数据结构设计:好的数据结构让代码更清晰、性能更好
- 可视化价值:动画让抽象的算法变得直观可理解
如果你也对机器人、算法可视化感兴趣,欢迎 Star ⭐ 和贡献代码!
相关链接
- 🎮 在线体验 :点击体验
如果这篇文章对你有帮助,欢迎点赞 👍 收藏 ⭐ 关注!
有任何问题欢迎评论区讨论~