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

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

起源

前几天刷视频,偶然看到了一个关于 Micromouse(电子鼠) 迷宫竞赛的视频。视频中,一只小小的机器鼠在复杂的迷宫中快速穿梭,以惊人的速度找到通往终点的最短路径。那种"探索-记忆-规划-冲刺"的智能行为深深吸引了我。

作为程序员的本能反应------ "我也能做一个!"

于是,利用AI,我开发了这款 Micromouse Simulator,一个完全运行在浏览器中的电子鼠迷宫寻路模拟器。

🎮 在线体验地址:点击体验


什么是 Micromouse 竞赛?

Micromouse 是一项经典的机器人竞赛,起源于 1970 年代,现已成为 IEEE 等组织认可的标准化比赛项目。

比赛规则

  1. 迷宫规格:标准 16×16 格的迷宫,每个格子的墙壁信息未知

  2. 起点与终点:起点固定在左下角,终点是中心 4 个格子

  3. 机器人限制

    • 必须自主导航,不能远程控制
    • 只能通过传感器探测周围墙壁
    • 需要在有限时间内完成探索和冲刺

核心挑战

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 应用,这个项目让我深入理解了:

  1. 算法之美:Flood Fill 算法的优雅在于它完美契合"未知环境探索"的场景
  2. 数据结构设计:好的数据结构让代码更清晰、性能更好
  3. 可视化价值:动画让抽象的算法变得直观可理解

如果你也对机器人、算法可视化感兴趣,欢迎 Star ⭐ 和贡献代码!


相关链接


如果这篇文章对你有帮助,欢迎点赞 👍 收藏 ⭐ 关注!

有任何问题欢迎评论区讨论~