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:全局缩放重构实录 📝 待发布

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

相关推荐
伊玛目的门徒3 小时前
试用leetcode之典中典 二数之和问题
java·算法·leetcode
Jerry3 小时前
LeetCode 226. 翻转二叉树
算法
想做小南娘,发现自己是女生喵4 小时前
【无标题】
数据结构·算法
言乐66 小时前
Python实现可运行解密游戏游戏框架
python·游戏·小程序·游戏程序·关卡设计
Kx_Triumphs6 小时前
HDU4348 To the moon(主席树区间修改模板)
算法·题解
旖-旎6 小时前
《LeetCode647 回文子串 || LeetCode 5 最长回文子串》
c++·算法·leetcode·动态规划·哈希算法
轻颂呀7 小时前
约瑟夫环问题
算法
科技大视界9 小时前
投资AI项目,传统尽调不够用了——李章虎律师拆解算法、数据、算力三大雷区
人工智能·算法·数据挖掘
郝学胜-神的一滴9 小时前
算法实战:最小k个数——大顶堆的优雅解法
开发语言·数据结构·c++·python·程序人生·算法·排序算法