05 | 持久化撤销提示非核心功能

本文目录

  • [🧩 第 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];
    }
}

🧪 实际被测试注入过的异常值:1e300INFINITYNaN-1gamesWon > 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:全局缩放重构实录 📝 待发布

👍 如果你觉得有帮助,点赞 + 收藏 + 关注,三连支持作者继续写下去 🚀

相关推荐
lueluelue472 小时前
LeetCode:链表
算法·leetcode·链表
橘子汽水1685 小时前
Leetcode 23,543合并K个升序链表,二叉树的直径
算法·leetcode·链表
huameinan狮子5 小时前
Adaboost算法原理与计算实例
算法
别怪我很水5 小时前
API中提供了VelocityTracker类用于计算触摸事件MotionEvent的速度,而其内部默认使用的方法就是最小二乘法,本 ...
算法·gitee·最小二乘法
Re.不晚8 小时前
挑战做100道力扣算法- DAY1
算法·leetcode·职场和发展
蓝悦无人机9 小时前
《Planning algorithms》读书笔记——第1章 引言
算法·读书笔记·规划算法·lavalle
Sagittarius_A*9 小时前
分组密码基础(二):Feistel 结构与 DES 的设计思想
算法·信息安全·密码学·des·数论
青山木9 小时前
Hot 100 --- 搜索插入位置
java·数据结构·算法·leetcode
alexwang21110 小时前
HDU 4348 详细题解
c++·算法·题解·hdu·主席树·可持久化数据结构·可持久化线段树