← 都来码 / Dolaima← 例程索引rust/app/minesweeper.rs原文

rust/app/minesweeper.rs · 32507 字节 · 下载原文

// 扫雷 —— 用 **Rust** 写的手机游戏

// Minesweeper -- a mobile game written in **Rust**

//

// 玩法:棋盘横 9 列、行数按可用高度算,格子里埋着雷。**轻点翻开一格** ——

// Gameplay: the board is 9 columns wide with the row count derived from the usable height, and mines are hidden in the cells. **Tap a cell to open it** --

// 翻开的是空场(八邻没有雷)就连带把相连的一片全掀开;翻到雷这一局结束。

// open a cell with no neighbouring mines and the whole connected patch opens with it; open a mine and the round is over.

// **长按 450 毫秒插旗**(手指还按着就插,不等抬手),把每一颗雷都插上旗子即获胜。

// **Hold for 450 ms to flag a cell** (the flag goes down while the finger is still down, no waiting for lift-off); flag every mine and you win.

//

// 键盘兜底:方向键移动光标、回车翻开、F 插旗、R 重开、Esc 退出。

// Keyboard fallback: the arrow keys move a cursor, Enter opens, F flags, R restarts, Esc quits.

//

// ◆ 为什么这一份值得单独写(打砖块照不到的两条路径)

// ◆ Why this one is worth writing on its own (two paths Breakout cannot reach)

//

//   · **一维数组 + 二维坐标换算**:格子下标是 `row * 列数 + 列`,

//   · **A 1-D array plus 2-D coordinate arithmetic**: a cell index is `row * columns + column`,

//     而且下标是**先写进去、再从另一个下标读回来**(雷、翻开、旗子、邻雷数四张表)。

//     and the subscripts are **written first and then read back through a different subscript** (mines, opened, flagged, neighbour count).

//   · **显式队列做泛洪展开**:`vec_push(vec, x)` 入队、`v.contains(x)` 去重,替代递归。

//   · **Flood fill with an explicit queue**: `vec_push(vec, x)` to enqueue and `v.contains(x)` to dedup, instead of recursion.

//

// ◆ 顺带把 Rust 专属库的几个入口各用了一遍(它们本轮才第一次接通)

// ◆ It also walks through several entries of the Rust-specific library (they were only wired up this round)

//

//   `println!`(打终局摘要)、`rand()`(埋雷)、`sleep()`(让炸到的那一格停一下)、

//   `println!` (final summary), `rand()` (mine placement), `sleep()` (let the fatal cell stay on screen a moment),

//   `vec_push` / `v.contains`(泛洪)、`panic`(屏幕小到放不下棋盘时的守卫)。

//   `vec_push` / `v.contains` (flood fill), `panic` (guard for a screen too small to hold the board).

//

// ◆ 实测踩到的三条(都是本轮 Rust 库代理量出来的契约,照着写就不会错)

// ◆ Three measured contracts from this round's Rust library work (follow them and nothing goes wrong)

//

//   ① **成员调用的接收者排在最后**:`v.push(x)` 发射成 `method_push_1(value, vec)`,

//   ① **A member call pushes its receiver last**: `v.push(x)` emits `method_push_1(value, vec)`,

//      写成 `(vec, value)` 一个错都不报、数组却一个元素都不变。本文只用现成的 `push`/`contains`。

//      while writing `(vec, value)` reports no error at all and changes not one element. This file only uses the existing `push`/`contains`.

//   ② **一个程序里只养一条 Vec**:库里 `push` 是"写到 count 后面那一格",而 `vec![…]` 只按元素个数分配

//   ② **Keep exactly one Vec alive per program**: the library `push` writes past the element count, while `vec![...]` allocates

//      —— **再有第二处分配就会把先push出去的数据盖掉**(实测:每步重建的 Vec 走 20 步后内容全错)。

//      **only for the literal element count** -- a second allocation then overwrites what was pushed earlier (measured: a Vec rebuilt every step is

//      garbage after about 20 steps). 这份例程因此只有一条队列,而且**只往后 push、永不清空**。

//      This example therefore owns a single queue, and it **only pushes forward and never clears**.

//   ③ 数组字面量**必须写在一行**(跨行的 `];` 报「非法 token: SEMICOLON」)。

//   ③ An array literal **must be written on one line** (a line-broken `];` reports "invalid token: SEMICOLON").


// ◆ UI 库的显式引用

//   The explicit reference to the UI library

//

// `use vml_ui::*;` —— 声明文件是 `Lib/rust/vml_ui.rs`(GenLib 生成,155 个 `ui_*`,**不要手改**):

// `use vml_ui::*;` -- the declaration file is `Lib/rust/vml_ui.rs` (generated by GenLib, 155 `ui_*`; **do not hand-edit**):

//

//     cd third_party/vml && dotnet run --project tools/GenLib -- -n -c vmlui -l rust -o Lib/rust/vml_ui.rs

//

// 本前端**不需要**声明就能解析 `ui_*`(它按 `ui_` 前缀自动链 `vmlui.vml`),

// This frontend resolves `ui_*` **without** any declaration (it auto-links `vmlui.vml` by the `ui_` prefix),

// 这条引用是**文档级**的:它标明"这个程序用 UI 库",并让这份例程与其余语言同形。

// so this line is **documentation-level**: it states that this program uses the UI library and keeps

// this example shaped like every other language's.

// 实测(删掉这一行,产物与画面完全一致):见仓库 docs 里那份「UI 库导入约定」的第二节 rust 行。

// Measured: deleting this line changes nothing -- the same instruction count and the same drawn picture;

// see the rust row in section two of the UI-library import convention document under docs.

#include "../../../Lib/rust/vml_ui.rs"
use vml_ui::*;

fn main() {
    // 界面语言:开局问一次宿主要中文还是英文(0=中文 1=英文),之后整局按它分支。

    // UI language: ask the host once at the start whether Chinese or English (0=Chinese 1=English), then branch on it for the whole round.

    let lang = ui_scr_get_language();

    let t_title = if lang == 0 { "扫雷" } else { "Minesweeper" };
    let t_mine = if lang == 0 { "雷" } else { "Mines" };
    let t_seed = if lang == 0 { "首点必安全" } else { "First tap is safe" };
    let t_hint = if lang == 0 { "轻点翻开 · 长按插旗" } else { "Tap to open - hold to flag" };
    let t_restart = if lang == 0 { "按 R 重开" } else { "Press R to restart" };
    let t_keydrop = if lang == 0 { "键盘:方向键移动 · 回车翻开 · F 插旗" } else { "Keys: arrows move - Enter opens - F flags" };
    let t_win = if lang == 0 { "全清了!通关。\n再来一局?(选「否」退出)" } else { "Cleared! You win.\nPlay again? (choose \"No\" to quit)" };
    let t_lose = if lang == 0 { "踩雷了,这一局结束。\n再来一局?(选「否」退出)" } else { "Boom. Round over.\nPlay again? (choose \"No\" to quit)" };

    // 开窗时传 0:尺寸交给宿主(用真实可用区)。**开窗之后**再问一次真实画布 —— 开窗前问到的只是估算值。

    // Pass 0 when opening: the size is the host's to decide (the real usable area). Ask the **real** canvas only after the window is open -- before that the host can only estimate.

    ui_win_open(t_title, 0, 0);
    ui_scr_set_keep_on(1);

    // 可用绘图区(手机上约 395x578,标题栏与手柄区已经扣掉了)

    // The usable drawing area (about 395x578 on a phone; the title bar and gamepad strip are already excluded)

    let mut w = ui_scr_get_w();
    let mut h = ui_scr_get_h();
    if w <= 0 { w = 360; }
    if h <= 0 { h = 620; }

    // ── 版面:横 9 列固定,格边长取"按宽能放下的最大整数",行数再按剩余高度算 ──

    // -- Layout: 9 columns fixed; the cell size is the largest integer that fits the width, and the row count follows from the leftover height --

    let top = 46;
    let margin = 6;
    let cols = 9;
    let mut cell = (w - margin * 2) / cols;
    // 底下 58 像素留给三行提示 —— 不留的话最后一行会落到可视区外(实测:578 高时正好被切掉)。

    // The bottom 58 px are reserved for the three hint lines -- without them the last line falls off the visible area (measured: clipped at height 578).

    let mut rows = (h - top - margin * 2 - 58) / cell;
    // 行数封顶 14:下面四张表按 160 格(9x17)预留,多了会越界;正常手机上是 11 行。

    // Rows are capped at 14: the four tables below reserve 160 cells (9x17) and more would run past the end; a normal phone gives 11 rows.

    if rows > 14 { rows = 14; }

    // 守卫(**可以真的触发**:`--screen 60x120` 就走到这里)—— 与其画一个挤成一团的棋盘,不如说清楚。

    // Guard (**genuinely reachable**: `--screen 60x120` lands here) -- better to say so than to draw a board squeezed into nothing.

    // ⚠ 消息只写 ASCII:`panic()` 走共享库的 `debug_print`,而**实测非 ASCII 的文本在那里会变成双重编码的乱码**

    // ⚠ The message stays ASCII: `panic()` goes through the shared library's `debug_print`, and **measured: non-ASCII text comes out double-encoded there**

    //   (同一个宿主上 `println!` 打带中文的字符串是好的,所以这不是"字符串本身坏了",是那条输出路径按字节重新编了一遍)。

    //   (on the same host, `println!` with CJK text in it is fine, so it is not the string itself -- that output path re-encodes byte by byte.)

    //   括号里带上判定用的两个数,省得下次还要猜"到底差多少"。

    //   The two numbers used by the test are included so nobody has to guess "how far off" it was.

    if cell < 8 || rows < 4 {
        panic("minesweeper: screen too small (need cell >= 8 and rows >= 4)");
    }

    let cells = cols * rows;
    let mines = cells / 7;

    // 四张表:雷 / 已翻开 / 已插旗 / 邻雷数。**每个字面量都必须写在一行**(见文件头 ③)。

    // Four tables: mines / opened / flagged / neighbour count. **Each literal must sit on one line** (see note 3 in the file header).

    let mut mine = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
    let mut open = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
    let mut flag = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
    let mut adj = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];

    // 这一局的状态

    // The state of the round

    let mut state = 0;
    let mut placed = 0;
    let mut opened = 0;
    let mut flags = 0;
    let mut cur = 0;
    let mut holding = 0;
    let mut flagdone = 0;
    let mut holdcell = 0;
    let mut holdtick = 0;    // 按下的墙钟时刻(ms)—— 长按时长按毫秒算(用户 2026-10-04 口径)

                             // wall-clock time of the press, in ms -- the long-press duration is measured in wall-clock ms (user's 2026-10-04 rule)

    // `dirty` = "画面真的变了吗":一帧要把整幅场景光栅化一遍(手机上几百毫秒),

    // `dirty` = "did the picture really change": one frame rasterizes the whole scene (hundreds of ms on a phone),

    // 所以只有输入或状态变了才重画,定时器空转的那些拍直接跳过(见 Examples/README.md 的惯例)。

    // so a repaint happens only when input or state changed; idle timer ticks are skipped (see the convention in Examples/README.md).

    let mut dirty = 1;
    let mut lastopen = 0;
    let mut lastflag = 0;
    let mut quit = 0;

    // 泛洪用的队列:**全程序只有这一条 Vec**(见文件头 ②)。

    // The flood-fill queue: **the only Vec in the whole program** (see note 2 in the file header).

    // 种子那一格是哑元(游标从 1 起)~~,因为本前端不支持零元素字面量~~ —— ✅ **已修好(2026-10-04 复验)**:

    // The seed element is a dummy (the cursor starts at 1) ~~because this frontend has no zero-element literal~~ -- ✅ **fixed (re-checked 2026-10-04)**:

    //   实测 `let mut v = vec![]; v.push(10); ...; v.push(50);` 得 `len() = 5`、`contains(10/30/50)` 全 1。

    //   measured: `let mut v = vec![]; v.push(10); ...; v.push(50);` gives `len() = 5` and `contains(10/30/50)` all 1.

    //   ⇒ 本文件照旧用「种子哑元」写法(能跑就别动;新写的代码不必再绕)。

    //   => this file **still** uses the "dummy seed" form (if it runs, leave it alone; newly written code need not work around it).

    let mut queue = vec![0];
    let mut qi = 1;

    // 画一次棋盘要发几百条绘制指令,这里把版面算好的常量留着复用。

    // One board repaint issues a few hundred draw commands; the layout constants computed above are kept for reuse here.

    let boardw = cols * cell;
    let boardh = rows * cell;

    // 长按判定**按墙钟毫秒**算时长:`ui_time_get_ms()` 是系统毫秒(各平台定时器分辨率不同 ⇒

    // The long press measures its duration in **wall-clock milliseconds**: `ui_time_get_ms()` is system ms (timer resolution differs across platforms

    // 不能数拍)。用户 2026-10-04 口径:「过了多久」和「走了几步」是两个独立的量。

    // => never count ticks). User's 2026-10-04 rule: "how long it took" and "how many steps ran" are two independent quantities.

    // 判据是 450 毫秒 —— 与 Java / Swift 两版用同一套口径。

    // The criterion is 450 ms, the same one the Java and Swift versions use.

    let mut tid = ui_timer_set(50, 0);

    // ⚠ `println!("{}", 字符串变量)` 打出来的是那个字符串的**地址**(实测 1585),

    // ⚠ `println!("{}", some_string_variable)` prints the **address** of that string (measured: 1585),

    //   所以摘要一律**按语言各写一条字面量**,不把界面文字当格式实参传进去。

    //   so every summary is written as a **per-language literal** instead of passing UI text in as a format argument.

    if lang == 0 {
        println!("扫雷开局 {}x{} 格={} 雷={}", cols, rows, cells, mines);
    } else {
        println!("minesweeper start {}x{} cells={} mines={}", cols, rows, cells, mines);
    }

    while ui_win_is_closed() == 0 {

      // ── 画(只在这幅画真的变了的时候)──

      // -- draw (only when the picture really changed) --

      if dirty != 0 {
        ui_2d_clear(-15724520);
        ui_2d_rect(0, 0, w, top - 4, -14932944, 1, 0, 0);
        ui_2d_text(8, 10, t_title, -1, 15, VML_ANCHOR_LEFT);

        // 剩余雷数:用一列小竖条表示(省掉整数转字符串这条路)

        // Mines left: drawn as a column of small bars (which avoids integer-to-string conversion)

        let mut left = mines - flags;
        if left < 0 { left = 0; }
        let mut i = 0;
        while i < left && i < 20 {
            ui_2d_rect(w - 10 - i * 5, 8, 3, 14, -49088, 1, 0, 1);
            i = i + 1;
        }
        ui_2d_text(w - 8, 24, t_mine, -7695200, 10, VML_ANCHOR_RIGHT);

        // 进度条:已翻开的格数 / 需要翻开的格数

        // Progress bar: cells opened / cells that need opening

        let need = cells - mines;
        let mut prog = 0;
        if need > 0 { prog = opened * 120 / need; }
        ui_2d_rect(8, top - 10, 120, 4, -12299158, 1, 0, 2);
        ui_2d_rect(8, top - 10, prog, 4, -12855708, 1, 0, 2);

        let mut r = 0;
        while r < rows {
            let mut c = 0;
            while c < cols {
                let idx = r * cols + c;
                let x = margin + c * cell;
                let y = top + r * cell;
                if open[idx] != 0 {
                    ui_2d_rect(x + 1, y + 1, cell - 2, cell - 2, -14669260, 1, 0, 3);
                    if mine[idx] != 0 {
                        ui_2d_circle(x + cell / 2, y + cell / 2, cell / 4, -15657444, 1, 0);
                    }
                    if adj[idx] == 1 { ui_2d_text(x + cell / 2, y + cell / 4, "1", -11884289, 17, VML_ANCHOR_CENTER); }
                    if adj[idx] == 2 { ui_2d_text(x + cell / 2, y + cell / 4, "2", -12855708, 17, VML_ANCHOR_CENTER); }
                    if adj[idx] == 3 { ui_2d_text(x + cell / 2, y + cell / 4, "3", -49088, 17, VML_ANCHOR_CENTER); }
                    if adj[idx] == 4 { ui_2d_text(x + cell / 2, y + cell / 4, "4", -5210881, 17, VML_ANCHOR_CENTER); }
                    if adj[idx] == 5 { ui_2d_text(x + cell / 2, y + cell / 4, "5", -25797, 17, VML_ANCHOR_CENTER); }
                    if adj[idx] == 6 { ui_2d_text(x + cell / 2, y + cell / 4, "6", -13708346, 17, VML_ANCHOR_CENTER); }
                    if adj[idx] == 7 { ui_2d_text(x + cell / 2, y + cell / 4, "7", -36944, 17, VML_ANCHOR_CENTER); }
                    if adj[idx] == 8 { ui_2d_text(x + cell / 2, y + cell / 4, "8", -7695200, 17, VML_ANCHOR_CENTER); }
                } else {
                    // 关着的格子:键盘光标那一格亮一点

                    // A closed cell; the keyboard cursor cell is drawn brighter

                    if cur == idx { ui_2d_rect(x + 1, y + 1, cell - 2, cell - 2, -11903888, 1, 0, 3); }
                    else { ui_2d_rect(x + 1, y + 1, cell - 2, cell - 2, -12957094, 1, 0, 3); }
                    if flag[idx] != 0 {
                        ui_2d_line(x + cell / 2, y + 6, x + cell / 2, y + cell - 6, -42406, 2);
                        ui_2d_rect(x + cell / 2, y + 6, cell / 3, cell / 4, -42406, 1, 0, 1);
                    }
                }
                c = c + 1;
            }
            r = r + 1;
        }

        ui_2d_text(w / 2, top + boardh + 6, t_seed, -7695200, 11, VML_ANCHOR_CENTER);
        ui_2d_text(w / 2, top + boardh + 22, t_hint, -7695200, 11, VML_ANCHOR_CENTER);
        ui_2d_text(w / 2, top + boardh + 38, t_keydrop, -12299158, 10, VML_ANCHOR_CENTER);
        if state != 0 {
            ui_2d_text(w / 2, top + boardh / 2, t_restart, -11702, 17, VML_ANCHOR_CENTER);
        }
        ui_2d_present();
        dirty = 0;
      }

        // ── 收一条消息 ──

        // -- take one message --

        let t = ui_msg_fetch_wait(0);
        if t == 10 { break; }

        let mut want = 0 - 1;
        let mut wantflag = 0 - 1;

        if t == 9 {
            // 定时器**只负责唤醒**;长按时长按**墙钟毫秒**算(用户 2026-10-04 口径)。

            // The timer only wakes the loop; the long-press duration is measured in wall-clock ms.

            // 长按插旗:**还按着**就插上(不等抬手)—— 这就是"长按"的判定点

            // Long press to flag: the flag goes down while the finger is **still down** (no waiting for lift-off) -- this is the moment that defines "long press"

            if holding == 1 && flagdone == 0 {
                if ui_time_get_ms() - holdtick >= 450 {
                    flagdone = 1;
                    wantflag = holdcell;
                }
            }
        }

        // 触摸与鼠标走同一套:4/6 = 按下、7 = 移动、5/8 = 抬手

        // Touch and mouse share one path: 4/6 = press, 7 = move, 5/8 = release

        let isdown = t == 6 || t == 4;
        let ismove = t == 7;
        let isup = t == 8 || t == 5;
        if isdown || ismove || isup {
            let mx = ui_msg_get_a();
            let my = ui_msg_get_b();
            let inside = mx >= margin && mx < margin + boardw && my >= top && my < top + boardh;
            // 二维到一维:下标 = 行 × 列数 + 列(同一算式在画那边也用,别各算一遍)

            // 2-D to 1-D: index = row * columns + column (the same expression is used when drawing; never compute it twice)

            let hit = if inside { ((my - top) / cell) * cols + ((mx - margin) / cell) } else { 0 - 1 };

            if isdown {
                if hit >= 0 && state == 0 {
                    holding = 1;
                    holdcell = hit;
                    holdtick = ui_time_get_ms();   // 记住按下的墙钟时刻(长按时长按毫秒算)

                                                   // remember the wall-clock time of the press (the long-press duration is measured in ms)

                    flagdone = 0;
                    cur = hit;
                }
            }
            if ismove {
                // 按住之后挪出原来那一格 = 撤单:抬手既不开也不插旗

                // Moving off the original cell after pressing cancels the gesture: lifting neither opens nor flags

                if holding == 1 && hit != holdcell { holding = 2; }
            }
            if isup {
                if holding == 1 && flagdone == 0 && hit >= 0 && hit == holdcell {
                    want = holdcell;
                }
                holding = 0;
            }
        }

        if t == 1 {
            let k = ui_msg_get_a();
            if k == 27 { quit = 1; break; }
            if k == 82 || k == 114 {
                // R 重开

                // R restarts

                state = 0;
                placed = 0;
                opened = 0;
                flags = 0;
                cur = 0;
                lastopen = 0;
                lastflag = 0;
                let mut j = 0;
                while j < cells { mine[j] = 0; open[j] = 0; flag[j] = 0; adj[j] = 0; j = j + 1; }
            }
            if k == 70 || k == 102 {
                if state == 0 { wantflag = cur; }
            }
            if k == 13 || k == 32 {
                if state == 0 { want = cur; }
            }
            if k == 37 { if cur % cols > 0 { cur = cur - 1; } }
            if k == 39 { if cur % cols < cols - 1 { cur = cur + 1; } }
            if k == 38 { if cur - cols >= 0 { cur = cur - cols; } }
            if k == 40 { if cur + cols < cells { cur = cur + cols; } }
        }

        // 任何一条**输入**消息都让画面重画;纯定时器的那几拍不重画。

        // Any **input** message marks the picture dirty; idle timer ticks do not.

        if t != 9 { dirty = 1; }

        // ── 插旗 ──

        // -- flagging --

        if wantflag >= 0 && state == 0 {
            if open[wantflag] == 0 {
                if flag[wantflag] == 0 { flag[wantflag] = 1; flags = flags + 1; lastflag = lastflag + 1; }
                else { flag[wantflag] = 0; flags = flags - 1; }
                // 长按插的旗也要重画(它由定时器那一拍触发,不走上面那条"输入就重画")

                // A long-press flag must repaint too (it is triggered by a timer tick, which the "input repaints" rule above does not cover)

                dirty = 1;
                ui_audio_beep(1568, 45);
            }
        }

        // ── 翻开 ──

        // -- opening --

        if want >= 0 && state == 0 && flag[want] == 0 && open[want] == 0 {

            // 首点安全:**第一次翻开时才埋雷**,避开点中的那一格以及它的八邻

            // First tap is safe: **mines are only placed on the first open**, avoiding the tapped cell and its eight neighbours

            if placed == 0 {
                let wcol = want % cols;
                let wrow = want / cols;
                let mut k = 0;
                while k < mines {
                    let cand = rand() % cells;
                    let ccol = cand % cols;
                    let crow = cand / cols;
                    let dcol = ccol - wcol;
                    let drow = crow - wrow;
                    let near = dcol >= 0 - 1 && dcol <= 1 && drow >= 0 - 1 && drow <= 1;
                    // 也避开已经放下的雷(否则同一格会被算两次、雷数就少了)

                    // Also skip cells that already hold a mine (otherwise one cell is counted twice and the mine count comes out short)

                    if near == 0 && mine[cand] == 0 { mine[cand] = 1; k = k + 1; }
                }
                // 邻雷数:八邻里雷的个数(下标的换算是同一套算式)

                // Neighbour counts: how many of the eight neighbours hold a mine (same index arithmetic)

                let mut a = 0;
                while a < cells {
                    if mine[a] == 0 {
                        let ci = a % cols;
                        let ri = a / cols;
                        let mut n = 0;
                        let mut dr = 0 - 1;
                        while dr <= 1 {
                            let mut dc = 0 - 1;
                            while dc <= 1 {
                                if dr != 0 || dc != 0 {
                                    let rr = ri + dr;
                                    let cc = ci + dc;
                                    if rr >= 0 && rr < rows && cc >= 0 && cc < cols {
                                        if mine[rr * cols + cc] != 0 { n = n + 1; }
                                    }
                                }
                                dc = dc + 1;
                            }
                            dr = dr + 1;
                        }
                        adj[a] = n;
                    }
                    a = a + 1;
                }
                placed = 1;
            }

            if mine[want] != 0 {
                // 踩雷:把全部雷摊开给玩家看

                // Boom: lay every mine open so the player can see them

                let mut z = 0;
                while z < cells { if mine[z] != 0 { open[z] = 1; } z = z + 1; }
                state = 1;
                lastopen = opened;
                ui_audio_beep(131, 320);
                ui_2d_present();
                // 让炸到的那一格在屏幕上停一下再弹框(单音 ui_audio_beep 只有几十毫秒)

                // Let the fatal cell stay on screen a moment before the dialog (a single-tone ui_audio_beep lasts only tens of ms)

                sleep(350);
                if lang == 0 {
                    println!("扫雷结束 踩雷 翻开={} 插旗={} 雷={}", opened, flags, mines);
                } else {
                    println!("minesweeper end BOOM opened={} flagged={} mines={}", opened, flags, mines);
                }
                if (ui_dlg_msg(t_title, t_lose, VML_DLG_INFO)) != 0 { quit = 1; break; }
                // 重开(与 R 键同一套复位)

                // Restart (the same reset the R key performs)

                state = 0;
                placed = 0;
                opened = 0;
                flags = 0;
                cur = 0;
                let mut j = 0;
                while j < cells { mine[j] = 0; open[j] = 0; flag[j] = 0; adj[j] = 0; j = j + 1; }
            } else {
                // 泛洪展开:种子入队,然后一直抽到队尾

                // Flood fill: enqueue the seed, then drain up to the tail

                vec_push(queue, want);
                while qi < queue.len() {
                    let c = queue[qi];
                    qi = qi + 1;
                    if open[c] == 0 {
                        open[c] = 1;
                        opened = opened + 1;
                        // 只有空场(邻雷 0)才往外扩

                        // Only empty ground (0 neighbouring mines) expands further

                        if adj[c] == 0 {
                            let ci = c % cols;
                            let ri = c / cols;
                            let mut dr = 0 - 1;
                            while dr <= 1 {
                                let mut dc = 0 - 1;
                                while dc <= 1 {
                                    if dr != 0 || dc != 0 {
                                        let rr = ri + dr;
                                        let cc = ci + dc;
                                        if rr >= 0 && rr < rows && cc >= 0 && cc < cols {
                                            let nb = rr * cols + cc;
                                            // `v.contains` 就是"这一格是不是已经排队了"的答案 ——

                                            // `v.contains` is exactly the answer to "is this cell already queued" --

                                            // 少了它同一格会被翻两次、`opened` 也跟着虚高。

                                            // without it the same cell is opened twice and `opened` is inflated.

                                            if mine[nb] == 0 && open[nb] == 0 && queue.contains(nb) == 0 {
                                                vec_push(queue, nb);
                                            }
                                        }
                                    }
                                    dc = dc + 1;
                                }
                                dr = dr + 1;
                            }
                        }
                    }
                }
                // **一次翻开只响一声**:`ui_audio_beep` 是单通道的(后一个音掐掉前一个),

                // **One sound per open**: `ui_audio_beep` is single-channel (a later tone cuts the previous one),

                // 泛洪一次掀开三四十格时逐格响只会听见最后一声、还白跑几十次 syscall。

                // so beeping once per flooded cell would only let the last one be heard and waste dozens of syscalls.

                ui_audio_beep(784, 30);
                lastopen = opened;

                // 赢:非雷的格子全翻开

                // Win: every non-mine cell is open

                if opened == cells - mines {
                    state = 2;
                    ui_audio_beep(1047, 320);
                    ui_2d_present();
                    sleep(350);
                    if lang == 0 {
                        println!("扫雷结束 通关 翻开={} 插旗={} 雷={}", opened, flags, mines);
                    } else {
                        println!("minesweeper end WIN opened={} flagged={} mines={}", opened, flags, mines);
                    }
                    if (ui_dlg_msg(t_title, t_win, VML_DLG_INFO)) != 0 { quit = 1; break; }
                    state = 0;
                    placed = 0;
                    opened = 0;
                    flags = 0;
                    cur = 0;
                    let mut j = 0;
                    while j < cells { mine[j] = 0; open[j] = 0; flag[j] = 0; adj[j] = 0; j = j + 1; }
                }
            }
        }
    }

    // 收尾摘要(被墙钟叫停时也会走到这里):`lastopen` / `lastflag` 是本局里翻得最多的一次。

    // Final summary (also reached when the wall clock stops the run): `lastopen` / `lastflag` are the largest counts seen this round.

    if quit == 0 {
        if lang == 0 {
            println!("扫雷结束 退出 翻开={} 插旗={} 雷={}", lastopen, lastflag, mines);
        } else {
            println!("minesweeper end quit opened={} flagged={} mines={}", lastopen, lastflag, mines);
        }
    }
    ui_timer_kill(tid);
    ui_scr_set_keep_on(0);
    ui_win_close();
}