01 | 项目概览与扫雷核心算法

本文目录

  • [⛏️ 第 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 - 9int 乘法,虽然最大 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:全局缩放重构实录 📝 待发布

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