行空板 K10 弹珠迷宫
一、项目背景

原项目是 DFRobot 社区上一篇弹珠迷宫教程:用一块带屏幕和三轴加速度计的小型开发板,做一个"靠倾斜板子控制弹珠滚出迷宫"的掌上小游戏。原方案硬件是 Stick S3,开发方式是 Arduino C++。
硬件平台
行空板 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 |
二、功能介绍
玩法
倾斜板子,靠"重力"让金色弹珠在迷宫里滚动,滚进右上角的红色终点即通关。计时从开局开始,用时越短越好。
功能
| 类别 | 功能 | 说明 |
|---|---|---|
| 关卡 | 随机迷宫 | 15×17 格,每局重新生成 |
| 操作 | A 键 | 菜单页=开始游戏;游戏中=换一张新迷宫;胜利页=回菜单 |
| 操作 | B 键 | 循环切换 4 种重力方向映射,解决"弹珠往反方向滚"的问题 |
| 输入 | 加速度计 | 实时读取倾斜量,转成弹珠加速度 |
| 输入 | 开机校准 | 开局采样 16 次当前姿态作为"水平基准",所以可以用任意舒服的姿势握持 |
| 显示 | 顶栏 | 实时计时 + 步数 |
| 显示 | 底栏 | 操作提示 + 当前方向模式 |
| 显示 | 菜单页 | 标题、玩法说明、演示小球、历史最佳 |
| 显示 | 胜利页 | 用时、步数、是否破纪录 |
状态
程序有三个状态,由 A 键驱动流转:
A键(开始) 到达终点
[菜单页] ──────────> [游戏中] ──────────> [胜利页]
^ | |
| | A键(换图) | A键
└────────────────────┘ └──> [菜单页]
B键:在菜单页/游戏页随时循环切换方向模式(1→2→3→4→1)
三、硬件清单
| 序号 | 名称 | 数量 | 备注 |
|---|---|---|---|
| 1 | 行空板 K10 | 1 | 主控 + 屏幕 + 加速度计 + 按键 + 喇叭全在板上 |
| 2 | USB-C 数据线 | 1 | 需支持数据传输,不能是纯充电线 |
| 3 | 电脑(Windows) | 1 | 用于烧录和上传程序;板子识别为 COM3 |
本项目没有外接传感器、没有杜邦线——弹珠是画在屏幕上的虚拟小球,重力是板载加速度计的读数。
四、程序编写
整体结构
源码 15_marble_maze.py 共 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() | 最佳成绩持久化;状态机与帧循环 |
配置区
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 # 弹珠半径(像素)
迷宫生成
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) 开始"挖洞"。
- 用
bytearray而不是 list:255 个格子只占 255 字节,ESP32 内存吃紧,这很重要。
从倾斜到移动
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
# ... 限速 ...
# ---- 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
最后判断终点:
bc = int(bx) // CELL
br = int(by - MAZE_Y0) // CELL
return bc == goal_c and br == goal_r
六、遇到的问题与解决办法
| 问题 | 现象 | 根本原因 | 解决办法 | |
|---|---|---|---|---|
| 1 | 字号报错 | 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 |
| 2 | 弹珠往反方向滚 | 板子往左倾,球往右走;静止时速度不为 0 | 加速度计 X/Y 轴符号取决于传感器安装方向 | 加 X_DIR/Y_DIR 常量,B 键循环切换 4 种组合 |
| 3 | 加速度单位困惑 | 水平放桌面时 Z 读数是 -946 而不是 9.8 | 驱动返回的是原始计数值,1g ≈ 946;负号说明 Z 轴方向朝下 | "1g ≈ 946 原始单位",物理参数全部按此标定 |
硬件清单
- 行空板K10 MicroPython编程AI教学/学习主控板(含两头PH2.0白色硅胶线)- 蘑菇云科创教育1
- 弹珠通关.rar










用DFS生成完美迷宫这个思路不错,bytearray省内存细节到位
B键切换方向映射解决加速度计方向问题实用
K10板载资源全用上不外接元件教学演示方便