Flipper Zero 扫雷游戏全解析:玩法、源码架构与 fbt 编译部署指南
【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper
本指南以仓库中收录的社区作品 Minesweeper(扫雷)应用为主体,完整讲解它在 Flipper Zero 上的玩法操作、棋盘与游戏状态的数据结构、渲染与输入的事件循环,以及基于 fbt 的编译部署方法。读完本文,你将能上手这款游戏、理解其单文件 C 实现的底层原理,并掌握如何按需调整棋盘参数后自行构建 FAP 应用。
应用背景与仓库定位
Minesweeper 是一款为 Flipper Zero 设备实现的经典扫雷游戏,原作者为 panki27,本仓库将其收录在 Applications/Official/source-OLDER/grnch/minesweeper 目录下,属于"官方/更早来源(source-OLDER)"收藏区中的社区移植作品。游戏针对 Flipper Zero 的 128×64 单色屏幕、方向键与 OK 键交互方式做了专门适配,采用全屏 ViewPort 直接渲染,不依赖任何复杂 UI 框架,非常适合作为学习 Flipper Zero 外置应用(FAP)开发的小型样例。
该目录内除 README.md 外,核心文件仅有 5 个:
- minesweeper.c:约 520 行的单文件完整实现,包含渲染、输入、游戏逻辑与对话框流程;
- assets.h:由图像资产生成的 XBM 位图数据,定义所有 8×8 瓦片;
- application.fam:fbt 构建系统的应用清单,声明应用元信息;
assets/与img/目录:分别存放瓦片 PNG/XBM 源资产与运行截图。
游戏规则与按键操作
根据原文档,游戏操作完全围绕 Flipper Zero 的 5 键(方向键 + OK 键)展开,共 4 条核心规则:
| 操作 | 按键行为 | 效果 |
|---|---|---|
| 移动光标 | 方向键(短按) | 在棋盘上移动光标,光标到达边缘时会循环回绕到对侧 |
| 翻开格子 | 短按中心 OK 键 | 打开光标所在格子,若踩中地雷则游戏失败 |
| 切换旗帜 | 长按中心 OK 键 | 在"未翻开"与"插旗"状态之间切换,用于标记疑似地雷 |
| 快速展开(chord) | 在已翻开且有数字的格子上短按 OK | 当该格子周围旗帜数量与数字相等时,自动翻开周围剩余未翻开格子(感谢 @gelin 的贡献) |
这四条规则在源码中都有对应实现:
- 光标移动在 minesweeper.c 的短按分支中处理,四个方向均带边界回绕(
cursor_x越界时折返到对侧); - 翻开格子由
InputKeyOk短按触发,见 minesweeper.c; - 长按 OK 切换旗帜调用
place_flag(),见 minesweeper.c; - 数字格上自动展开周围未翻开格子的逻辑位于
play_move()的"flags == mines"分支,见 minesweeper.c。
游戏胜出条件为:翻开的格子数fields_cleared等于棋盘总格子数 − 地雷数(见 minesweeper.c)。失败时弹出"Game Over"对话框并触发设备振动提示;胜利时弹出"Game won!"对话框并显示通关用时。
核心数据结构:从宏到状态机
棋盘尺寸与难度常量
在 minesweeper.c 顶部定义了整套游戏的参数:
#define PLAYFIELD_WIDTH 16 // 棋盘宽度(列数) #define PLAYFIELD_HEIGHT 7 // 棋盘高度(行数) #define TILE_WIDTH 8 // 瓦片像素宽度 #define TILE_HEIGHT 8 // 瓦片像素高度 #define MINECOUNT 20 // 地雷总数16×7 的棋盘加上顶部 8 像素信息栏(8 + y * TILE_HEIGHT的渲染偏移),恰好填满 Flipper Zero 的 128×64 屏幕。20 颗地雷在 112 个格子中约占 17.9% 的密度,属于中等偏易的难度。
双层棋盘:逻辑层与表现层
游戏采用"逻辑棋盘 + 表现棋盘"双数组设计,体现在Minesweeper状态结构体(minesweeper.c)中:
typedef struct { Field minefield[PLAYFIELD_WIDTH][PLAYFIELD_HEIGHT]; // 逻辑层:哪些格子是雷 TileType playfield[PLAYFIELD_WIDTH][PLAYFIELD_HEIGHT]; // 表现层:格子当前显示状态 FuriTimer* timer; // 失败振动的单次定时器 int cursor_x, cursor_y; // 光标位置 int mines_left; // 剩余地雷数 int fields_cleared; // 已翻开格子数 int flags_set; // 已插旗数 bool game_started; // 是否已开始(首次翻开前不布置雷) uint32_t game_started_tick; // 计时起点 } Minesweeper;Field枚举只有FieldEmpty/FieldMine两态,回答"这里到底有没有雷";TileType枚举有 12 个成员(minesweeper.c):TileType0~TileType8对应 0~8 的数字提示(源码注释特别强调枚举顺序必须固定,因为提示数字直接复用枚举值作为"周围雷数"),外加TileTypeUncleared(未翻开)、TileTypeFlag(旗帜)、TileTypeMine(地雷)。
这种分离带来的好处是:数字提示可以在翻开瞬间一次性计算并固化到playfield中,无需每次渲染时重新扫描雷区;而minefield只负责布雷与判定。
开局布置与"首击必安全"机制
setup_playfield()(minesweeper.c)负责每局初始化:
- 将
minefield全部清为FieldEmpty,playfield全部置为TileTypeUncleared; - 循环
MINECOUNT次随机布雷,使用rand() % PLAYFIELD_WIDTH/rand() % PLAYFIELD_HEIGHT生成随机坐标; - 关键设计:布雷时要求随机坐标必须满足
FieldEmpty且不与当前光标位置同行或同列(cursor_x != rand_x && cursor_y != rand_y),从源码注释"make sure first guess isn't a mine"可知,这是为了保证玩家的第一次点击绝不会踩雷; - 重置
mines_left、fields_cleared、flags_set等计数并记录计时起点。
值得注意的实现细节:首次按下 OK 时(game_started == false)会先调用setup_playfield()再执行play_move()(minesweeper.c),因此"首击安全"实际上是在布雷阶段通过坐标排除实现的,而不是翻开时二次判定。
翻开逻辑:数字提示、空区展开与连锁判定
play_move()(minesweeper.c)是整款游戏的核心函数,返回false表示踩雷失败。其执行顺序:
- 旗帜保护:若目标格已插旗,直接返回
true(不产生任何操作),防止误翻; - 踩雷判定:若
minefield对应位置为FieldMine,将该格设为TileTypeMine并返回false; - chord 快速展开:若目标格是已翻开的数字格(1~8),遍历其 3×3 邻域统计旗帜数,若旗帜数等于该格数字,则对周围所有
TileTypeUncleared格子递归调用play_move()——若递归过程中踩到雷,说明旗帜标错了,直接判定失败; - 正常翻开:统计 3×3 邻域内的实际地雷数得到
hint,写入playfield,fields_cleared++; - 空区自动展开:若
hint == 0(空区),递归翻开周围所有未翻开格子,形成经典的"一大片空白自动打开"效果,见 minesweeper.c。
边界安全贯穿始终:所有 3×3 邻域遍历都先做x >= 0 && x < PLAYFIELD_WIDTH && y >= 0 && y < PLAYFIELD_HEIGHT的越界检查(例如 minesweeper.c),并跳过自身格子。
旗帜管理
place_flag()(minesweeper.c)是双向切换逻辑:
- 目标格为
TileTypeUncleared时置为TileTypeFlag,flags_set++; - 目标格已是
TileTypeFlag时还原为TileTypeUncleared,flags_set--; - 其余状态(已翻开/数字/雷)不做任何处理。
顶部信息栏实时显示Mines: 20 - flags_set(见 minesweeper.c),即"剩余疑似雷数"。注意游戏胜负判定并不校验旗帜数量,flags_set仅用于展示与 chord 判定,插错旗也不会直接失败,只会在 chord 展开时引爆。
渲染管线:XBM 瓦片、光标反色与顶部 HUD
瓦片资源
assets.h 定义了全部 13 种 8×8 瓦片的位图数据(tile_0_bits~tile_8_bits、tile_flag_bits、tile_mine_bits、tile_uncleared_bits),每个瓦片以 8 字节 XBM 行数据描述,例如数字"1"瓦片:
#define tile_1_width 8 #define tile_1_height 8 static uint8_t tile_1_bits[] = { 0x00, 0x10, 0x18, 0x10, 0x10, 0x10, 0x10, 0x00, };assets/目录同时保留了对应的 PNG 源文件与.xbm文件,便于开发者用图像工具重新绘制瓦片后重新生成头文件。
渲染函数
render_callback()(minesweeper.c)负责每帧绘制,要点:
- 先通过
acquire_mutex加锁获取共享状态(25 tick 超时,超时则跳过本帧),保证与输入线程间的数据一致性; - 顶部 HUD:左上方绘制
Mines: N,右上方绘制分:秒计时(从game_started_tick起算,换算为分钟与秒); - 棋盘主体:双重循环遍历 16×7 棋盘,根据
playfield[x][y]的枚举值 switch 到对应瓦片位图,用canvas_draw_xbm以x*8, 8+y*8为坐标绘制; - 光标表示:在绘制当前光标格前后各调用一次
canvas_invert_color(反色),实现"光标所在格颜色反转"的高亮效果(minesweeper.c 与 minesweeper.c); - 数字枚举值直接作为瓦片索引的 switch 分支,与
TileType的排序约束呼应。
事件循环与输入处理
minesweeper_app()(minesweeper.c)是应用入口,完整呈现了经典 Flipper Zero FAP 应用的骨架:
- 启动对话框:先弹出提示页,显示游戏名与操作提示"Hold OK pressed to toggle flags."(含原作者署名),按 OK 进入游戏;
- 事件队列:
furi_message_queue_alloc(8, sizeof(PluginEvent))创建容量 8 的消息队列,input_callback(minesweeper.c)将每个InputEvent封装为PluginEvent后入队; - 状态互斥:
ValueMutex保护Minesweeper状态,渲染回调与主循环通过acquire_mutex/release_mutex协作; - ViewPort 注册:创建
ViewPort,设置绘制回调与输入回调,通过furi_record_open("gui")获取 GUI 服务并注册到GuiLayerFullscreen全屏层; - 主循环:
furi_message_queue_get(..., 100)以 100 tick 超时轮询事件;短按InputKeyShort处理方向与 OK,长按InputKeyLong处理插旗与退出;每次循环末尾view_port_update触发重绘; - 退出路径:短按 Back 或长按 Back 均置
processing = false退出循环,随后依次注销 ViewPort、释放队列、删除互斥量与定时器、释放内存,返回 0。
游戏失败时,game_lost()(minesweeper.c)通过RECORD_NOTIFICATION触发sequence_set_vibro_on振动,并启动FuriTimerTypeOnce定时器(0.2 秒)在timer_callback中发送sequence_reset_vibro停止振动,实现短暂震感提醒。弹出 "Play again" 对话框后,玩家可按 OK 重开或按其他键退出。
编译、构建与部署
应用清单 application.fam
application.fam 是 fbt 的声明式清单,关键字段如下:
App( appid="Minesweeper", name="Minesweeper", apptype=FlipperAppType.EXTERNAL, # 外部应用(FAP),非固件内置 entry_point="minesweeper_app", # 入口函数 cdefines=["APP_MINESWEEPER"], requires=["gui"], # 依赖 GUI 服务 stack_size=8 * 1024, # 8KB 任务栈 fap_category="Games", # 归类为游戏 fap_icon="minesweeper_icon.png", order=35, )从apptype=FlipperAppType.EXTERNAL与fap_category="Games"可以看出,它被构建为独立的.fap外部应用,通过 qFlipper 或 Flipper Mobile App 拷入 SD 卡后即可在"应用 → Games"分类下运行。
编译命令
原文档给出的编译命令为:
./fbt firmware_minesweeper在仓库对应的旧版 fbt 工具链中,该目标会编译整个固件并包含 Minesweeper 应用。需要说明的是:由于application.fam将应用声明为FlipperAppType.EXTERNAL,可以推断在现代 fbt(fbt 2.x / 3.x)下,更通用的做法是使用 FAP 相关目标单独构建该应用(例如./fbt fap_minesweeper或通过./fbt launch_app APPSRC=Applications/...一键编译并部署到已连接的 Flipper Zero),具体目标名以你本地固件仓库的 fbt 版本为准。编译前提是已按官方流程配置好 Flipper Zero 固件开发环境(包含 ARM 交叉编译工具链、fbt脚本及其依赖)。
按需调整参数
想改难度或棋盘布局,只需修改 minesweeper.c 顶部的四个宏后重新构建即可:
- 增大
MINECOUNT提高地雷密度(20/112 ≈ 17.9%,可试 24~28); - 修改
PLAYFIELD_WIDTH/PLAYFIELD_HEIGHT改变棋盘形状,但需注意与 128×64 屏幕的适配:16 列 × 8 像素瓦片刚好占满横向 128 像素,顶部信息栏占用 8 像素,纵向剩余 56 像素最多容纳 7 行,因此加大棋盘需同步缩小瓦片尺寸或滚动方案; - 修改
TILE_WIDTH/TILE_HEIGHT可改变瓦片像素尺寸,但必须与 assets.h 中瓦片的实际宽度/高度宏保持一致,否则渲染会错位。
小结
Minesweeper 是观察 Flipper Zero FAP 应用最小实现范式的绝佳样例:一个单文件 C 程序同时完成"事件队列 + 互斥锁 + ViewPort 渲染"的标准主循环、双层棋盘数据模型、首次必安全布雷、递归空区展开与 chord 快速翻开等完整游戏机制,并通过application.fam与./fbt一键构建部署。无论你是想在 Flipper Zero 上直接游玩这款扫雷,还是把它当作学习 FAP 开发的入门源码,本文覆盖的玩法、数据结构和构建链路都已足以支撑你完成从"看懂"到"改造成自己的游戏"的跨越。
【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考