弹珠迷宫python版

2026-09-250

行空板 K10 弹珠迷宫


一、项目背景

1.1 需求来源

原项目是 DFRobot 社区上一篇弹珠迷宫教程:用一块带屏幕和三轴加速度计的小型开发板,做一个"靠倾斜板子控制弹珠滚出迷宫"的掌上小游戏。原方案硬件是 Stick S3,开发方式是 Arduino C++。

1.2 硬件平台

行空板 K10 是一块高度集成的 ESP32-S3 教学板,本项目用到的资源全部是板载的,没有外接任何一个元件:

板载资源 规格 在本项目中的用途
主控 ESP32-S3 运行 MicroPython
屏幕 2.8" ILI9341,240×320 显示迷宫与弹珠
三轴加速度计 SC7A20H 感知倾斜,作为"重力"输入
按键 A / B 两个 A=开始/换图,B=校正方向
RGB 灯 3 × WS2812 方向模式指示、胜利提示
扬声器 I2S + ES8311 撞墙/胜利音效
USB-C 原生 USB Serial/JTAG 供电、烧录、REPL

固件版本:micropython_unihiker_k10&Box_20260430_V0.9.8.bin
MicroPython 版本标识:05bed2375-dirty on 2026-04-30; Generic ESP32S3 module


二、功能介绍

2.1 玩法

倾斜板子,靠"重力"让金色弹珠在迷宫里滚动,滚进右上角的红色终点即通关。计时从开局开始,用时越短越好。

2.2 功能清单

类别 功能 说明
关卡 随机迷宫 15×17 格,每局重新生成,用随机 DFS 保证有解且全连通
操作 A 键 菜单页=开始游戏(同时重新校准水平);游戏中=换一张新迷宫;胜利页=回菜单
操作 B 键 循环切换 4 种重力方向映射,解决"弹珠往反方向滚"的问题
输入 加速度计 实时读取倾斜量,转成弹珠加速度
输入 开机校准 开局采样 16 次当前姿态作为"水平基准",所以可以用任意舒服的姿势握持
显示 顶栏 实时计时 + 步数
显示 底栏 操作提示 + 当前方向模式
显示 菜单页 标题、玩法说明、演示小球、历史最佳
显示 胜利页 用时、步数、是否破纪录
音效 撞墙音 180 Hz 短音,带 180 ms 冷却防连响
音效 开始音 / 胜利音 开始=两声上行;胜利=C-E-G-C 上行琶音
存档 最佳成绩 写入板载文件系统 maze_best.txt,断电不丢

2.3 状态机

程序有三个状态,由 A 键驱动流转:

A键(开始)             到达终点
  [菜单页] ──────────> [游戏中] ──────────> [胜利页]
     ^                    |                    |
     |                    | A键(换图)          | A键
     └────────────────────┘                    └──> [菜单页]

  B键:在菜单页/游戏页随时循环切换方向模式(1→2→3→4→1)

三、硬件清单

3.1 必需件

序号 名称 数量 备注
1 行空板 K10 1 主控 + 屏幕 + 加速度计 + 按键 + 喇叭全在板上
2 USB-C 数据线 1 需支持数据传输,不能是纯充电线
3 电脑(Windows) 1 用于烧录和上传程序;板子识别为 COM3

就这些。 本项目没有外接传感器、没有杜邦线、没有焊接——弹珠是画在屏幕上的虚拟小球,重力是板载加速度计的读数。


四、程序编写

4.1 整体结构

源码 15_marble_maze.py 共 592 行,从上到下分为 8 个模块:

序号 模块 主要函数 作用
1 配置区 常量定义 分辨率、迷宫尺寸、颜色、物理参数
2 方向映射 apply_orient() 4 种重力方向组合,B 键切换
3 迷宫生成 shuffle4(), gen_maze(), wall_at(), cell_cx/cy() 随机 DFS 生成完美迷宫
4 音效 tone(), sfx_bump(), sfx_win(), sfx_start() 扬声器兼容封装
5 校准 calibrate() 采样当前姿态作为水平基准
6 物理 update_ball() 加速度→速度→位移→碰撞
7 绘制 draw_maze_full(), erase_region(), draw_ball(), draw_bar(), render_play_full(), render_menu(), render_win() 全量绘制 + 增量绘制
8 存档与主循环 load_best(), save_best(), main() 最佳成绩持久化;状态机与帧循环

5.2 配置区:把"魔法数字"集中管理

SCREEN_DIR = 2                 # 2 = 竖屏 240x320
MAZE_COLS = 15                 # 列数(必须奇数)
MAZE_ROWS = 17                 # 行数(必须奇数)
CELL = 16                      # 每格像素
TOP_BAR = 20                   # 顶部信息栏高度(容纳16号字)
MAZE_Y0 = TOP_BAR
FOOT_Y   = TOP_BAR + MAZE_H    # 底部提示区起始 y (290)
BALL_R = 5                     # 弹珠半径(像素)
### 5.3 迷宫生成:随机 DFS

```python
def gen_maze(cols, rows):
    """随机深度优先(DFS)生成完美迷宫。返回 bytearray, 1=墙 0=路。"""
    g = bytearray([1] * (cols * rows))
    stack = [(1, 1)]
    g[1 * cols + 1] = 0
    dirs = [(0, 2), (0, -2), (2, 0), (-2, 0)]
    while stack:
        cr, cc = stack[-1]
        d = shuffle4(list(dirs))
        moved = False
        for dr, dc in d:
            nr, nc = cr + dr, cc + dc
            if 0 < nr < rows - 1 and 0 < nc < cols - 1 and g[nr * cols + nc] == 1:
                g[((cr + nr) // 2) * cols + (cc + nc) // 2] = 0   # 打通中间墙
                g[nr * cols + nc] = 0
                stack.append((nr, nc))
                moved = True
                break
        if not moved:
            stack.pop()
    return g

说明:

  • 先把整张图填成墙(全 1),然后从 (1,1) 开始"挖洞"。
  • 每次只朝偶数步方向跳两格(步长 2),并把起点和目标格之间的那格墙也打通——这就是 g[((cr+nr)//2)*cols + (cc+nc)//2] = 0 这一行的意义。
  • 用显式栈而不是递归:MicroPython 的递归深度有限,17×15 的迷宫递归会爆栈。
  • 用 bytearray 而不是 list:255 个格子只占 255 字节,ESP32 内存吃紧,这很重要。

5.4 物理模块:从倾斜到移动

def update_ball():
    global bx, by, vx, vy, moves
    ax = (acce.read_x() - bias_x) * X_DIR
    ay = (acce.read_y() - bias_y) * Y_DIR

    # 死区
    if abs(ax) < DEADZONE:
        ax = 0.0
    else:
        ax = ax - DEADZONE if ax > 0 else ax + DEADZONE

    # 加速度 -> 速度, 再摩擦衰减
    vx = (vx + ax * ACCEL_SCALE) * FRICTION
    vy = (vy + ay * ACCEL_SCALE) * FRICTION
    # ... 限速 ...

四步流水线:读加速度 → 减基准 → 去死区 → 积分成速度(带摩擦)。

  • bias_x / bias_y 是 calibrate() 采到的水平基准,减去它意味着"相对于你握持时的姿态"计算倾斜,而不是相对于绝对水平面。这条设计让游戏非常好上手。
  • * X_DIR / * Y_DIR 是方向符号,见 5.5。

碰撞采用分轴处理,这是关键:

# ---- X 轴移动 + 碰撞 ----
    if abs(vx) > 0.03:
        nx = bx + vx
        ex = nx + BALL_R if vx > 0 else nx - BALL_R     # 检测边缘点
        ec = int(ex) // CELL
        er = int(by - MAZE_Y0) // CELL
        if wall_at(ec, er):
            nx = (ec * CELL - BALL_R - 1) if vx > 0 else ((ec + 1) * CELL + BALL_R + 1)
            vx = 0.0
            sfx_bump()
        bx = nx

意义:X、Y 分开算,每轴撞墙只清掉该轴速度、并把弹珠贴到墙面前 1 px(- BALL_R - 1),另一轴速度保留。效果是弹珠会沿墙滑行,而不是撞墙就死死停住——这是手感好坏的分水岭。

检测点用 bx ± BALL_R(球边缘)而不是球心,保证球体不会视觉上嵌进墙里。

最后判断终点:

bc = int(bx) // CELL
    br = int(by - MAZE_Y0) // CELL
    return bc == goal_c and br == goal_r

六、总结:遇到的问题与解决办法

这是本项目真正有价值的部分。按时间顺序整理,每项都写明现象 → 原因 → 解决办法。

# 问题 现象 根本原因 解决办法
1 烧录/运行崩溃 MemoryError: memory allocation failed, allocating 1013454792 bytes 显示驱动被重复初始化(上一次程序还在跑,又跑一次) 每次 mpremote run 前先复位板子;封装成 run_k10.sh
2 字号报错 AttributeError: module has no attribute 'font_montserrat_12' K10 的 MicroPython 不支持 12 号字 实测可用字号为 8/10/14/16/18/20/22/24/28/32;全部改用 14 或 16,顶栏高度相应从 18 加到 20
3 洗牌报错 AttributeError: module has no attribute 'shuffle' MicroPython 的 random 是裁剪版,没有 shuffle 手写 shuffle4()(Fisher-Yates,只需洗 4 个元素)
4 帧率只有 4.7 FPS 弹珠一顿一顿地瞬移 每帧全量重绘 144 个矩形 ≈ 210 ms 发现 show_draw() 会清空绘制队列 → 改为脏矩形增量渲染,迷宫只画一次,每帧只擦 1~4 格 + 画 1 个球,降到 ≈50 ms / 19 FPS
5 弹珠往反方向滚 板子往左倾,球往右走;静止时速度不为 0 加速度计 X/Y 轴符号取决于传感器安装方向,离线无法确定(实测水平静置 X≈18、Y≈-21、Z≈-946,1g≈946) 不猜:加 X_DIR/Y_DIR 常量,B 键循环切换 4 种组合,使用者一键试出正确的那一种;同时用 RGB 灯号指示当前模式
6 加速度单位困惑 水平放桌面时 Z 读数是 -946 而不是 9.8 驱动返回的是原始计数值,1g ≈ 946;负号说明 Z 轴方向朝下 文档化"1g ≈ 946 原始单位",物理参数全部按此标定(死区 45 ≈ 0.048g)
7 扬声器 API 不明 想确认 playTone 的签名,一调用板子就 Guru Meditation ESP32-S3 上对 C 层函数做参数试探会直接崩溃 放弃探测,改为 try/except 双签名兜底(playTone → play_tone → 静默),保证音效失败也不影响游戏
8 USB 串口被占死 程序跑起来后串口读 0 字节、write 报 Write timeout、mpremote 连接挂死 主循环持续渲染时 ESP32-S3 的 USB CDC 接收不再被排空 ① 硬件复位(翻转 RTS/DTR 触发 USB_UART_CHIP_RESET);② 代码里加上电 2 秒按住 A 键退出到 REPL 的逃生窗口
9 崩溃后串口句柄失效 板子重启后,宿主机已打开的串口写超时、读 0 字节 USB 重新枚举,旧句柄作废 每次操作前重新 serial.Serial() 打开端口
10 软复位不管用 发 Ctrl+C + Ctrl+D 后程序照样占死 软重启会重新执行 main.py,屏幕照样被占 改用 RTS/DTR 硬件复位,并在 main.py 启动窗口内连发 Ctrl+C 打断
11 区分不了"死机"和"正常运行" 串口 20 秒 0 字节输出 游戏源码本身没有任何 print 判断依据是关键字扫描(Traceback / Guru Meditation / MemoryError),而不是"有没有输出";同时给程序加了 boot 打印
12 递归深度风险 迷宫 DFS 若用递归可能爆栈 MicroPython 栈很浅 一开始就写成显式栈迭代版本

创作许可协议

本项目采用 CC BY(署名) 进行许可。

评论(0)
- 没有更多了 -