05 | 持久化撤销提示非核心功能
本文目录 第 5 篇持久化、撤销、提示 —— 那些非核心功能 1. 持久化系统5 个文件1.1 存什么1.2 安全措施版本号 校验和 原子写入1.3 Validate 兜底Fix #44, #46, #82↩️ 2. 撤销系统Z 键最多 10 步2.1 设计2.2 快照大小2.3 撤销 3. Hint 提示系统H 键3.1 算法 4. 音效系统 5. Smiley Face笑脸重置按钮 6. Toast 通知 7. 五层防护体系 下篇预告 系列目录系列第 5 篇· 核心算法只占项目代码的 20%剩下的 80% 全在本文。上一篇中英文双语的工程实现 第 5 篇持久化、撤销、提示 —— 那些非核心功能“你的扫雷能跑了吗”“能跑了。”“能保存记录吗能撤销吗能提示吗能调音量吗能切语言吗……”“……”能跑和好用之间差了 80% 的工作量。 1. 持久化系统5 个文件1.1 存什么文件内容触发时机records.dat每难度最好成绩每局胜利stats.dat总局/胜/胜率/连胜每局结束history.dat最近 5 局每局结束settings.dat音量/主题/语言设置变更savegame.dat完整游戏棋盘CtrlS / CtrlL全部用二进制格式—— C 的fwrite/fread直接读写内存结构体零序列化开销。1.2 安全措施版本号 校验和 原子写入#defineRECORD_FILE_VERSION2typedefstruct{unsignedintversion;unsignedintchecksum;// XOR 校验doublebestTime[3];}RecordFile;写入Fix #49, #85voidSaveRecord(void){RecordFile rf{.versionRECORD_FILE_VERSION,.checksum0};memcpy(rf.bestTime,record.bestTime,sizeof(record.bestTime));rf.checksumCalcChecksum(rf,sizeof(rf));// Write-to-temp-then-rename原子写入FILE*ffopen(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);// 原子操作}读取if(rf.version!RECORD_FILE_VERSION||CalcChecksum(rf,sizeof(rf))!0){TraceLog(WARNING,存档损坏或版本不匹配已重置);ResetToDefault();}1.3 Validate 兜底Fix #44, #46, #82校验和过了就能放心不还有校验和测不到的语义错误staticvoidValidateStats(GL_Stats*s){for(intd0;dDIFFICULTY_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 #44if(s-gamesWon[d]s-gamesPlayed[d])s-gamesWon[d]s-gamesPlayed[d];if(s-bestStreak[d]s-currentStreak[d])// Fix #82s-bestStreak[d]s-currentStreak[d];}} 实际被测试注入过的异常值1e300、INFINITY、NaN、-1、gamesWon gamesPlayed——全部拦截游戏正常运作。↩️ 2. 撤销系统Z 键最多 10 步2.1 设计typedefstruct{GL_Cell board[MAX_ROWS][MAX_COLS];// 完整棋盘快照intflagCount;bool firstClick;doubleelapsedTime;}GL_GameSnapshot;typedefstruct{GL_GameSnapshot snapshots[10];intcount;}GL_UndoStack;策略每次玩家操作前把当前状态完整拷贝一份。2.2 快照大小16 × 30 × 24 11,520 字节/快照 × 10 115 KB 栈上分配零堆操作。2.3 撤销GL_GameSnapshot prevGL_UndoStackPop(undoStack);memcpy(game,prev,sizeof(Game));// 全量恢复 如果撤销到的状态是firstClick true还没点过下一轮点击会重新布雷 —— 坐标不同绝对安全。 3. Hint 提示系统H 键3.1 算法intGL_FindSafeHint(constGL_Game*game,int*outRow,int*outCol){for(intr0;rrows;r){for(intc0;ccols;c){if(!board[r][c].isRevealed)continue;intflags0,unknowns0;intlastR-1,lastC-1;for(8个邻居){if(isFlagged)flags;if(!revealed!flagged){unknowns;lastRnr;lastCnc;}}// 旗子雷数 → 剩下的未翻格都安全if(flagsadjunknowns1){*outRowlastR;*outCollastC;return1;}}}return0;// 没有数学上确定的提示}⚠️局限只能找到数学上确定的安全格旗子数 雷数时剩余的格。纯推理的概率安全格不提示。 4. 音效系统revealSoundLoadSound(assets/audio/Boom.mp3);if(revealSound.frameCount0)TraceLog(WARNING,加载 Boom.mp3 失败);动作音效 翻开格子Boom.mp3 标旗flag.mp3❌ 取消旗unflag.mp3 胜利Victory.mp3 失败lose.mp3拖拽节流拖拽时每 50ms 最多播放一次音效防止拖过一片雷区时 50 个音效叠在一起。S 键切换 持久化if(soundOnsound.frameCount0)PlaySound(sound); 5. Smiley Face笑脸重置按钮用绘制代替文字按钮四种状态游戏中微笑 → 正常圆圈眼睛 弧线嘴 按钮按下惊讶→ 大圆圈眼睛 O 形嘴 胜利墨镜 → 8_8 眼睛 大弧度笑嘴 失败死亡 → X_X 眼睛 向下弧线嘴纯DrawCircleDrawLine绘制零纹理加载。 6. Toast 通知┌──────────────────────────────────┐ │ 游戏已保存 (ESC to close) │ ← 2 秒后自动消失 └──────────────────────────────────┘staticchartoastMessage[64];staticdoubletoastExpireTime;voidShowToast(constchar*msg){strncpy(toastMessage,msg,sizeof(toastMessage)-1);toastExpireTimeGetTime()2.0;// 2 秒} 7. 五层防护体系1️⃣ 校验和 ─────── 文件完整性防篡改/损坏 2️⃣ 版本号 ─────── 格式演进防不兼容 3️⃣ Validate ───── 语义正确性防不一致 4️⃣ 计算截断 ───── 浮点 UB 防护防 crash 5️⃣ 显示 clamp ─── UI 溢出保护防布局溢出 下篇预告第 6 篇200 个单元测试C 项目也能 TDDGoogle Test C 混合编译、test_fixes vs test_game_logic 双层架构、INT_MAX/NaN/Inf 边界值测试、ESC 八状态矩阵。 系列目录#标题状态1项目概览与扫雷核心算法✅ 已发布2Raylib 渲染架构✅ 已发布3222 个 Bug 修复教会我的事✅ 已发布4中英文双语的工程实现✅ 已发布5持久化、撤销、提示← 本文✅ 已发布6200 个单元测试C 项目也能 TDD 待发布7960×640 → 1280×720全局缩放重构实录 待发布如果你觉得有帮助点赞 收藏 关注三连支持作者继续写下去