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();
}