本文目录
- [🧩 第 5 篇:持久化、撤销、提示 ------ 那些"非核心"功能](#🧩 第 5 篇:持久化、撤销、提示 —— 那些"非核心"功能)
-
- [💾 1. 持久化系统:5 个文件](#💾 1. 持久化系统:5 个文件)
-
- [1.1 存什么?](#1.1 存什么?)
- [1.2 安全措施:版本号 + 校验和 + 原子写入](#1.2 安全措施:版本号 + 校验和 + 原子写入)
- [1.3 Validate 兜底(Fix #44, #46, #82)](#44, #46, #82))
- [↩️ 2. 撤销系统(Z 键,最多 10 步)](#↩️ 2. 撤销系统(Z 键,最多 10 步))
-
- [2.1 设计](#2.1 设计)
- [2.2 快照大小](#2.2 快照大小)
- [2.3 撤销](#2.3 撤销)
- [💡 3. Hint 提示系统(H 键)](#💡 3. Hint 提示系统(H 键))
-
- [3.1 算法](#3.1 算法)
- [🔊 4. 音效系统](#🔊 4. 音效系统)
- [😊 5. Smiley Face(笑脸重置按钮)](#😊 5. Smiley Face(笑脸重置按钮))
- [🔧 6. Toast 通知](#🔧 6. Toast 通知)
- [📊 7. 五层防护体系](#📊 7. 五层防护体系)
- [🔜 下篇预告](#🔜 下篇预告)
- [📚 系列目录](#📚 系列目录)
🎯 系列第 5 篇 · 核心算法只占项目代码的 20%,剩下的 80% 全在本文。
📖 上一篇 :中英文双语的工程实现
🧩 第 5 篇:持久化、撤销、提示 ------ 那些"非核心"功能
"你的扫雷能跑了吗?"
"能跑了。"
"能保存记录吗?能撤销吗?能提示吗?能调音量吗?能切语言吗?......"
"......"
"能跑"和"好用"之间,差了 80% 的工作量。
💾 1. 持久化系统:5 个文件
1.1 存什么?
| 文件 | 内容 | 触发时机 |
|---|---|---|
records.dat |
每难度最好成绩 | 每局胜利 |
stats.dat |
总局/胜/胜率/连胜 | 每局结束 |
history.dat |
最近 5 局 | 每局结束 |
settings.dat |
音量/主题/语言 | 设置变更 |
savegame.dat |
完整游戏棋盘 | Ctrl+S / Ctrl+L |
全部用二进制格式 ------ C 的 fwrite/fread 直接读写内存结构体,零序列化开销。
1.2 安全措施:版本号 + 校验和 + 原子写入
c
#define RECORD_FILE_VERSION 2
typedef struct {
unsigned int version;
unsigned int checksum; // XOR 校验
double bestTime[3];
} RecordFile;
写入(Fix #49, #85):
c
void SaveRecord(void) {
RecordFile rf = {
.version = RECORD_FILE_VERSION,
.checksum = 0
};
memcpy(rf.bestTime, record.bestTime, sizeof(record.bestTime));
rf.checksum = CalcChecksum(&rf, sizeof(rf));
// 🔑 Write-to-temp-then-rename(原子写入)
FILE* f = fopen("records.dat.tmp", "wb");
fwrite(&rf, sizeof(rf), 1, f);
fflush(f); // Fix #85:刷入内核
fclose(f);
remove("records.dat");
rename("records.dat.tmp", "records.dat"); // 原子操作
}
读取:
c
if (rf.version != RECORD_FILE_VERSION ||
CalcChecksum(&rf, sizeof(rf)) != 0) {
TraceLog(WARNING, "存档损坏或版本不匹配,已重置");
ResetToDefault();
}
1.3 Validate 兜底(Fix #44, #46, #82)
校验和过了就能放心?不,还有校验和测不到的语义错误:
c
static void ValidateStats(GL_Stats* s) {
for (int d = 0; d < DIFFICULTY_COUNT; d++) {
if (isnan(s->totalTime[d]) || isinf(s->totalTime[d]))
s->totalTime[d] = 0.0;
if (s->totalTime[d] > 8640000000.0) s->totalTime[d] = 0.0; // Fix #44
if (s->gamesWon[d] > s->gamesPlayed[d])
s->gamesWon[d] = s->gamesPlayed[d];
if (s->bestStreak[d] < s->currentStreak[d]) // Fix #82
s->bestStreak[d] = s->currentStreak[d];
}
}
🧪 实际被测试注入过的异常值:
1e300、INFINITY、NaN、-1、gamesWon > gamesPlayed------ 全部拦截,游戏正常运作。
↩️ 2. 撤销系统(Z 键,最多 10 步)
2.1 设计
c
typedef struct {
GL_Cell board[MAX_ROWS][MAX_COLS]; // 完整棋盘快照
int flagCount;
bool firstClick;
double elapsedTime;
} GL_GameSnapshot;
typedef struct {
GL_GameSnapshot snapshots[10];
int count;
} GL_UndoStack;
策略:每次玩家操作前,把当前状态完整拷贝一份。
2.2 快照大小
16 × 30 × 24 = 11,520 字节/快照
× 10 = 115 KB
栈上分配,零堆操作。
2.3 撤销
c
GL_GameSnapshot prev = GL_UndoStackPop(&undoStack);
memcpy(&game, &prev, sizeof(Game)); // 全量恢复
💡 如果撤销到的状态是
firstClick == true(还没点过),下一轮点击会重新布雷 ------ 坐标不同,绝对安全。
💡 3. Hint 提示系统(H 键)
3.1 算法
c
int GL_FindSafeHint(const GL_Game* game, int* outRow, int* outCol) {
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (!board[r][c].isRevealed) continue;
int flags = 0, unknowns = 0;
int lastR = -1, lastC = -1;
for (8个邻居) {
if (isFlagged) flags++;
if (!revealed && !flagged) {
unknowns++;
lastR = nr; lastC = nc;
}
}
// 🎯 旗子=雷数 → 剩下的未翻格都安全
if (flags == adj && unknowns == 1) {
*outRow = lastR; *outCol = lastC; return 1;
}
}
}
return 0; // 没有数学上确定的提示
}
⚠️ 局限:只能找到"数学上确定的"安全格(旗子数 = 雷数时剩余的格)。纯推理的"概率安全格"不提示。
🔊 4. 音效系统
c
revealSound = LoadSound("assets/audio/Boom.mp3");
if (revealSound.frameCount == 0)
TraceLog(WARNING, "加载 Boom.mp3 失败");
| 动作 | 音效 |
|---|---|
| 🔨 翻开格子 | Boom.mp3 |
| 🚩 标旗 | flag.mp3 |
| ❌ 取消旗 | unflag.mp3 |
| 🏆 胜利 | Victory.mp3 |
| 💀 失败 | lose.mp3 |
🔑 拖拽节流:拖拽时每 50ms 最多播放一次音效,防止拖过一片雷区时 50 个音效叠在一起。
S 键切换 + 持久化:
c
if (soundOn && sound.frameCount > 0) PlaySound(sound);
😊 5. Smiley Face(笑脸重置按钮)
用绘制代替文字按钮,四种状态:
游戏中(微笑) → 正常圆圈眼睛 + 弧线嘴
按钮按下(惊讶)→ 大圆圈眼睛 + O 形嘴
胜利(墨镜) → 8_8 眼睛 + 大弧度笑嘴
失败(死亡) → X_X 眼睛 + 向下弧线嘴
纯 DrawCircle + DrawLine 绘制,零纹理加载。
🔧 6. Toast 通知
┌──────────────────────────────────┐
│ 💬 游戏已保存 (ESC to close) │ ← 2 秒后自动消失
└──────────────────────────────────┘
c
static char toastMessage[64];
static double toastExpireTime;
void ShowToast(const char* msg) {
strncpy(toastMessage, msg, sizeof(toastMessage) - 1);
toastExpireTime = GetTime() + 2.0; // 2 秒
}
📊 7. 五层防护体系
1️⃣ 校验和 ─────── 文件完整性(防篡改/损坏)
2️⃣ 版本号 ─────── 格式演进(防不兼容)
3️⃣ Validate ───── 语义正确性(防不一致)
4️⃣ 计算截断 ───── 浮点 UB 防护(防 crash)
5️⃣ 显示 clamp ─── UI 溢出保护(防布局溢出)
🔜 下篇预告
第 6 篇:200 个单元测试:C 项目也能 TDD
Google Test + C 混合编译、test_fixes vs test_game_logic 双层架构、INT_MAX/NaN/Inf 边界值测试、ESC 八状态矩阵。
📚 系列目录
| # | 标题 | 状态 |
|---|---|---|
| 1 | 项目概览与扫雷核心算法 | ✅ 已发布 |
| 2 | Raylib 渲染架构 | ✅ 已发布 |
| 3 | 222 个 Bug 修复教会我的事 | ✅ 已发布 |
| 4 | 中英文双语的工程实现 | ✅ 已发布 |
| 5 | 持久化、撤销、提示 ← 本文 | ✅ 已发布 |
| 6 | 200 个单元测试:C 项目也能 TDD | 📝 待发布 |
| 7 | 960×640 → 1280×720:全局缩放重构实录 | 📝 待发布 |
👍 如果你觉得有帮助,点赞 + 收藏 + 关注,三连支持作者继续写下去 🚀