本文目录
- [⛏️ 第 1 篇:项目概览与扫雷核心算法](#⛏️ 第 1 篇:项目概览与扫雷核心算法)
-
- [📌 1. 前言](#📌 1. 前言)
- [🏗️ 2. 项目架构一览](#🏗️ 2. 项目架构一览)
- [🧱 3. 棋盘数据结构](#🧱 3. 棋盘数据结构)
-
- [3.1 Cell ------ 每个格子都在想什么?](#3.1 Cell —— 每个格子都在想什么?)
- [3.2 Game ------ 全局状态机](#3.2 Game —— 全局状态机)
- [⚙️ 4. 四大核心算法](#⚙️ 4. 四大核心算法)
-
- [4.1 首次点击安全 + Fisher-Yates 布雷 🎲](#4.1 首次点击安全 + Fisher-Yates 布雷 🎲)
- [4.2 BFS 泛洪展开 🌊](#4.2 BFS 泛洪展开 🌊)
- [4.3 Chord(和弦揭开)⚡](#4.3 Chord(和弦揭开)⚡)
- [4.4 相邻雷数计算 🔢](#4.4 相邻雷数计算 🔢)
- [📐 5. 难度系统](#📐 5. 难度系统)
- [📊 6. 核心逻辑测试矩阵](#📊 6. 核心逻辑测试矩阵)
- [🔜 下篇预告](#🔜 下篇预告)
- [📚 系列目录](#📚 系列目录)
🎯 系列定位 :面向有 C 基础、想了解游戏开发与中型工程实践的开发者。
📦 项目地址 :GitCode · 扫雷游戏 · C11 + Raylib · 222轮修复 · 200单元测试
⛏️ 第 1 篇:项目概览与扫雷核心算法
📌 1. 前言
扫雷(Minesweeper)是那个经典得不能再经典的逻辑游戏 ------ 左键翻开格子,右键标旗子,数字告诉周围有几颗雷。
操作:左键 翻开 · 右键 标旗 · R 重开 · Z 撤销 · H 提示 · ESC 暂停/菜单
人人都点过,但亲手写一个会发现很多"一看就会、一写就废"的问题:
- ❓ 第一次点击不能踩雷 ------ 怎么保证?
- ❓ 点到一个空白格,周围一大片自动翻开 ------ BFS 怎么写才不会重复入栈?
- ❓ 布雷用随机选格子,密度高了极其缓慢 ------ 怎么 O(N) 解决?
- ❓ 800 行能跑,3000 行才"好用" ------ 代码的边界在哪?
本文就是这些问题的答案。一起从零搭一个扫雷的核心逻辑。

🏗️ 2. 项目架构一览
MS_GUI/
├── src/
│ ├── main.c # ≈ 3200 行 · 游戏主程序:渲染、输入、UI、持久化
│ └── game_logic.c # 纯逻辑层(供单元测试)
├── assets/
│ ├── fonts/ # font2.ttf(英文)+ font4.ttf(CJK,25MB)
│ ├── audio/ # 5 个音效(.mp3)
│ └── background.png # 泼墨风格背景
├── lib/ # raylib 静态库
├── tests/ # Google Test × 200 个用例
└── docs/ # FIX_LOG(222条)· FEATURES · TEST_LOG
💡 设计哲学 :
main.c是"全能老大",game_logic.c是"逻辑打工人"。后者专为单元测试剥离 ------ C++ 的 Google Test 可以直接编译它。
🧱 3. 棋盘数据结构
3.1 Cell ------ 每个格子都在想什么?
c
typedef struct {
bool isMine; // 是不是雷?
bool isRevealed; // 翻开了吗?
bool isFlagged; // 标旗了吗?
bool isQuestionMark; // 打了问号吗?
bool wrongFlag; // 游戏结束时,标错的格子
int adjacentMines; // 周围有几颗雷(0~8)
double revealTime; // 什么时候翻开的(动画用)
} GL_Cell;
每个格子 5 个布尔量 + 1 个 int + 1 个 double ≈ 24 字节 。16×30×24 = 11,520 字节,栈上分配,零堆操作。
3.2 Game ------ 全局状态机
c
typedef struct {
GL_Cell board[MAX_ROWS][MAX_COLS];
int rows, cols;
int totalMines, flagCount;
bool firstClick; // 还没点过?
bool isCustom; // 自定义游戏?
double elapsedTime;
bool timerRunning;
GL_GameState state; // 🎯 核心:MENU / PLAYING / WIN / LOSE / PAUSE ...
GL_Difficulty difficulty;
int clickedMineRow, clickedMineCol; // 踩雷位置(红色高亮用)
} GL_Game;
🖼️ 状态机流转图:
#mermaid-svg-uxWf3TRR1pGl0HxW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uxWf3TRR1pGl0HxW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uxWf3TRR1pGl0HxW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uxWf3TRR1pGl0HxW .error-icon{fill:#552222;}#mermaid-svg-uxWf3TRR1pGl0HxW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uxWf3TRR1pGl0HxW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uxWf3TRR1pGl0HxW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uxWf3TRR1pGl0HxW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uxWf3TRR1pGl0HxW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uxWf3TRR1pGl0HxW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uxWf3TRR1pGl0HxW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uxWf3TRR1pGl0HxW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uxWf3TRR1pGl0HxW .marker.cross{stroke:#333333;}#mermaid-svg-uxWf3TRR1pGl0HxW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uxWf3TRR1pGl0HxW p{margin:0;}#mermaid-svg-uxWf3TRR1pGl0HxW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-uxWf3TRR1pGl0HxW .cluster-label text{fill:#333;}#mermaid-svg-uxWf3TRR1pGl0HxW .cluster-label span{color:#333;}#mermaid-svg-uxWf3TRR1pGl0HxW .cluster-label span p{background-color:transparent;}#mermaid-svg-uxWf3TRR1pGl0HxW .label text,#mermaid-svg-uxWf3TRR1pGl0HxW span{fill:#333;color:#333;}#mermaid-svg-uxWf3TRR1pGl0HxW .node rect,#mermaid-svg-uxWf3TRR1pGl0HxW .node circle,#mermaid-svg-uxWf3TRR1pGl0HxW .node ellipse,#mermaid-svg-uxWf3TRR1pGl0HxW .node polygon,#mermaid-svg-uxWf3TRR1pGl0HxW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uxWf3TRR1pGl0HxW .rough-node .label text,#mermaid-svg-uxWf3TRR1pGl0HxW .node .label text,#mermaid-svg-uxWf3TRR1pGl0HxW .image-shape .label,#mermaid-svg-uxWf3TRR1pGl0HxW .icon-shape .label{text-anchor:middle;}#mermaid-svg-uxWf3TRR1pGl0HxW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uxWf3TRR1pGl0HxW .rough-node .label,#mermaid-svg-uxWf3TRR1pGl0HxW .node .label,#mermaid-svg-uxWf3TRR1pGl0HxW .image-shape .label,#mermaid-svg-uxWf3TRR1pGl0HxW .icon-shape .label{text-align:center;}#mermaid-svg-uxWf3TRR1pGl0HxW .node.clickable{cursor:pointer;}#mermaid-svg-uxWf3TRR1pGl0HxW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uxWf3TRR1pGl0HxW .arrowheadPath{fill:#333333;}#mermaid-svg-uxWf3TRR1pGl0HxW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uxWf3TRR1pGl0HxW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uxWf3TRR1pGl0HxW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uxWf3TRR1pGl0HxW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uxWf3TRR1pGl0HxW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uxWf3TRR1pGl0HxW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uxWf3TRR1pGl0HxW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uxWf3TRR1pGl0HxW .cluster text{fill:#333;}#mermaid-svg-uxWf3TRR1pGl0HxW .cluster span{color:#333;}#mermaid-svg-uxWf3TRR1pGl0HxW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uxWf3TRR1pGl0HxW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uxWf3TRR1pGl0HxW rect.text{fill:none;stroke-width:0;}#mermaid-svg-uxWf3TRR1pGl0HxW .icon-shape,#mermaid-svg-uxWf3TRR1pGl0HxW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uxWf3TRR1pGl0HxW .icon-shape p,#mermaid-svg-uxWf3TRR1pGl0HxW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uxWf3TRR1pGl0HxW .icon-shape .label rect,#mermaid-svg-uxWf3TRR1pGl0HxW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uxWf3TRR1pGl0HxW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uxWf3TRR1pGl0HxW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uxWf3TRR1pGl0HxW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 点击难度
ESC
ESC / P
踩雷
翻完
R
R
统计按钮
自定义按钮
教程按钮
设置按钮
ESC
ESC / Enter
🏠 MENU 主菜单
🎮 PLAYING 游戏中
⏸️ PAUSE 暂停
💀 LOSE 失败
🏆 WIN 胜利
📊 STATS 统计
⚙️ CUSTOM 自定义
📖 TUTORIAL 教程
🔧 SETTINGS 设置
⚙️ 4. 四大核心算法
4.1 首次点击安全 + Fisher-Yates 布雷 🎲
扫雷的黄金法则:第一次点击永远安全。实现方式 ------ 点击后才布雷,而且避开点击位置周围 3×3。
c
static void PlaceMines(int safeRow, int safeCol) {
// 第 1 步:收集所有"可以放雷"的格子索引
int available = 0;
for (int r = 0; r < game.rows; r++)
for (int c = 0; c < game.cols; c++)
if (!isInSafeZone(r, c, safeRow, safeCol))
indices[available++] = r * game.cols + c;
// 第 2 步:Fisher-Yates 洗牌一次
for (int i = available - 1; i > 0; i--) {
int j = GetRandomValue(0, i);
int tmp = indices[i];
indices[i] = indices[j];
indices[j] = tmp;
}
// 第 3 步:前 N 个就是雷
for (int i = 0; i < game.totalMines; i++) {
int r = indices[i] / game.cols;
int c = indices[i] % game.cols;
game.board[r][c].isMine = true;
}
}
| 算法 | Expert 布雷次数 | 自定义 400 雷/480 格 |
|---|---|---|
| ❌ 随机碰撞 | ~110 次尝试 | 可能数千次甚至死循环 |
| ✅ Fisher-Yates | 固定 470 次 | 固定 470 次 |
🔑 关键点 :洗牌保证每个格子恰好被处理一次,布多少雷就跑多少步,严格 O(N)。
🖼️ Fisher-Yates 洗牌流程:
#mermaid-svg-QI2lj9iFXEZEjNUS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QI2lj9iFXEZEjNUS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QI2lj9iFXEZEjNUS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QI2lj9iFXEZEjNUS .error-icon{fill:#552222;}#mermaid-svg-QI2lj9iFXEZEjNUS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QI2lj9iFXEZEjNUS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QI2lj9iFXEZEjNUS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QI2lj9iFXEZEjNUS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QI2lj9iFXEZEjNUS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QI2lj9iFXEZEjNUS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QI2lj9iFXEZEjNUS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QI2lj9iFXEZEjNUS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QI2lj9iFXEZEjNUS .marker.cross{stroke:#333333;}#mermaid-svg-QI2lj9iFXEZEjNUS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QI2lj9iFXEZEjNUS p{margin:0;}#mermaid-svg-QI2lj9iFXEZEjNUS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QI2lj9iFXEZEjNUS .cluster-label text{fill:#333;}#mermaid-svg-QI2lj9iFXEZEjNUS .cluster-label span{color:#333;}#mermaid-svg-QI2lj9iFXEZEjNUS .cluster-label span p{background-color:transparent;}#mermaid-svg-QI2lj9iFXEZEjNUS .label text,#mermaid-svg-QI2lj9iFXEZEjNUS span{fill:#333;color:#333;}#mermaid-svg-QI2lj9iFXEZEjNUS .node rect,#mermaid-svg-QI2lj9iFXEZEjNUS .node circle,#mermaid-svg-QI2lj9iFXEZEjNUS .node ellipse,#mermaid-svg-QI2lj9iFXEZEjNUS .node polygon,#mermaid-svg-QI2lj9iFXEZEjNUS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QI2lj9iFXEZEjNUS .rough-node .label text,#mermaid-svg-QI2lj9iFXEZEjNUS .node .label text,#mermaid-svg-QI2lj9iFXEZEjNUS .image-shape .label,#mermaid-svg-QI2lj9iFXEZEjNUS .icon-shape .label{text-anchor:middle;}#mermaid-svg-QI2lj9iFXEZEjNUS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QI2lj9iFXEZEjNUS .rough-node .label,#mermaid-svg-QI2lj9iFXEZEjNUS .node .label,#mermaid-svg-QI2lj9iFXEZEjNUS .image-shape .label,#mermaid-svg-QI2lj9iFXEZEjNUS .icon-shape .label{text-align:center;}#mermaid-svg-QI2lj9iFXEZEjNUS .node.clickable{cursor:pointer;}#mermaid-svg-QI2lj9iFXEZEjNUS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QI2lj9iFXEZEjNUS .arrowheadPath{fill:#333333;}#mermaid-svg-QI2lj9iFXEZEjNUS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QI2lj9iFXEZEjNUS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QI2lj9iFXEZEjNUS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QI2lj9iFXEZEjNUS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QI2lj9iFXEZEjNUS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QI2lj9iFXEZEjNUS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QI2lj9iFXEZEjNUS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QI2lj9iFXEZEjNUS .cluster text{fill:#333;}#mermaid-svg-QI2lj9iFXEZEjNUS .cluster span{color:#333;}#mermaid-svg-QI2lj9iFXEZEjNUS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QI2lj9iFXEZEjNUS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QI2lj9iFXEZEjNUS rect.text{fill:none;stroke-width:0;}#mermaid-svg-QI2lj9iFXEZEjNUS .icon-shape,#mermaid-svg-QI2lj9iFXEZEjNUS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QI2lj9iFXEZEjNUS .icon-shape p,#mermaid-svg-QI2lj9iFXEZEjNUS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QI2lj9iFXEZEjNUS .icon-shape .label rect,#mermaid-svg-QI2lj9iFXEZEjNUS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QI2lj9iFXEZEjNUS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QI2lj9iFXEZEjNUS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QI2lj9iFXEZEjNUS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
📦 收集可用索引
🎲 从剩余索引中随机选一个 j
🔄 交换 indicesi ↔ indicesj
i-- > 0?
✅ 前 N 个索引为雷
4.2 BFS 泛洪展开 🌊
点到一个 adjacentMines == 0 的格子,周围一片自动翻开 ------ 这就是扫雷最爽的操作。也是坑最多的地方。
c
static void RevealCell(int row, int col) {
static int stackR[MAX_ROWS * MAX_COLS];
static int stackC[MAX_ROWS * MAX_COLS];
int top = 0;
// ✅ push 时立刻标记!【血的教训】
game.board[row][col].isRevealed = true;
stackR[top] = row; stackC[top] = col; top++;
while (top > 0) {
top--;
int r = stackR[top], c = stackC[top];
if (game.board[r][c].adjacentMines > 0) continue;
for (int dr = -1; dr <= 1; dr++) {
for (int dc = -1; dc <= 1; dc++) {
int nr = r + dr, nc = c + dc;
if (inBounds(nr, nc) && !revealed && !flagged) {
// ✅ push 时标记!【血的教训 × 2】
game.board[nr][nc].isRevealed = true;
game.board[nr][nc].revealTime = GetTime();
if (top < MAX_ROWS * MAX_COLS) {
stackR[top] = nr; stackC[top] = nc; top++;
}
}
}
}
}
}
🐛 经典 Bug(Fix #15) :一开始我在 pop 时才标记 isRevealed。结果 A 展开 B/C/D,B 又展开 A/C/D ------ 因为 A 还没被 pop,isRevealed == false,被重复入栈。480 个位置的栈迅速被重复项填满,后面的格子静默丢失。泛洪展开不完整,玩家以为点了个空白格,其实只展开了一小片。
✅ 教训 :BFS 的标准写法 ------ enqueue 时标记 visited,不是 dequeue 时。
4.3 Chord(和弦揭开)⚡
老玩家最爱的高效操作:左键点击已翻开的数字格,如果周围旗子数等于数字,自动翻开剩余格子。
c
static void ChordReveal(int row, int col) {
int flagCount = 0;
for (8个邻居) if (isFlagged) flagCount++;
if (flagCount != board[row][col].adjacentMines) return;
for (8个邻居) {
if (!revealed && !flagged)
RevealCell(nr, nc); // 小心!旗子标错就踩雷了
}
}
4.4 相邻雷数计算 🔢
c
static void CalculateAdjacent(void) {
for (int r = 0; r < game.rows; r++) {
for (int c = 0; c < game.cols; c++) {
if (game.board[r][c].isMine) continue;
int count = 0;
for (int dr = -1; dr <= 1; dr++)
for (int dc = -1; dc <= 1; dc++) {
if (dr == 0 && dc == 0) continue;
// 8 方向检查
}
game.board[r][c].adjacentMines = count;
}
}
}
💡 Expert 棋盘 480 格,每格检查 8 个方向 = 3840 次判断,每个判断仅几个 int 比较,帧率无感。
📐 5. 难度系统
| 难度 | 行 × 列 | 雷数 | 格子数 | 雷密度 |
|---|---|---|---|---|
| 🟢 初级 Beginner | 9×9 | 10 | 81 | 12.3% |
| 🔵 中级 Intermediate | 16×16 | 40 | 256 | 15.6% |
| 🟣 高级 Expert | 16×30 | 99 | 480 | 20.6% |
| ⚙️ 自定义 | 5~16 × 5~30 | 1~格子数-9 | 可变 | 可变 |
⚠️ 自定义的安全陷阱(Fix #87) :
maxMines = customRows * customCols - 9用int乘法,虽然最大 480 不会溢出,但防御性编程改为(int)((unsigned)customRows * (unsigned)customCols) - 9,从语言层面消除有符号整数溢出的 UB 风险。
📊 6. 核心逻辑测试矩阵
| 维度 | 测试策略 | 覆盖条件 |
|---|---|---|
| 布雷 | Fisher-Yates 输出 | 雷数正确、安全区干净、不重叠 |
| 展开 | BFS 边界 | 零格全展开、边界格不越界、已旗格跳过 |
| 和弦 | 旗数与雷数匹配 | 正确展开、旗数不足不展开、标错踩雷 |
| 胜利检测 | 全翻开非雷格 | 胜利触发、未翻完不触发 |
| 自定义 | 边界值 | 行 5/16、列 5/30、雷 1/最大 |
🔜 下篇预告
第 2 篇:Raylib 渲染架构:从 Camera2D 踩坑到 RenderTexture2D
为什么 Camera2D 在高 DPI 窗口上会黑边?RenderTexture2D + bilinear 的方案解决了什么问题?全屏下中文文字不模糊背后的
canvasMult机制是什么?
📚 系列目录
| # | 标题 | 状态 |
|---|---|---|
| 1 | 项目概览与扫雷核心算法 ← 本文 | ✅ 已发布 |
| 2 | Raylib 渲染架构:Camera2D → RenderTexture2D | ✅ 已发布 |
| 3 | 222 个 Bug 修复教会我的事 | 📝 待发布 |
| 4 | 中英文双语的工程实现 | 📝 待发布 |
| 5 | 持久化、撤销、提示:非核心功能 | 📝 待发布 |
| 6 | 200 个单元测试:C 项目也能 TDD | 📝 待发布 |
| 7 | 960×640 → 1280×720:全局缩放重构实录 | 📝 待发布 |
👍 如果你觉得有帮助,点赞 + 收藏 + 关注,三连支持作者继续写下去 🚀