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

swift/app/minesweeper.swift · 32875 字节 · 下载原文

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

// Minesweeper — a phone game written in **Swift**

//

// ◆ 玩法

// ◆ Gameplay

//

//   棋盘**自适应**:横 9 列固定,格子边长按可用宽度算,行数按可用高度算(411×726 上得到 9×14)。

//   An **adaptive** board: 9 columns wide, the cell size from the available width and the row count from

//   the available height (9×14 on 411×726).

//   **首点安全**:地雷在**第一次轻点之后**才布,且避开点中那格与它周围 8 格 —— 第一下永远不会踩雷。

//   **First tap is safe**: mines are placed only **after the first tap**, avoiding the tapped cell and its 8

//   neighbours — the first tap can never hit a mine.

//   **长按插旗**:手指**还按着**的时候就开始计时,按住 ≥450ms 立刻插旗(不等抬手)。

//   **Long-press to flag**: the clock starts while the finger is **still down**; at ≥450ms the flag is toggled

//   right there (it does not wait for the finger to lift).

//   **泛洪展开用显式栈**(数组当队列,head/tail 两个下标),**不递归**。

//   **Flood fill uses an explicit stack** (the array is the queue, with head/tail indices), **not recursion**.

//   顶部:剩余雷数 / 笑脸(点它重开)/ 计时。踩雷后**把所有雷显示出来**。

//   Top bar: mines left / a smiley (tap it to restart) / the timer. After a mine goes off **every mine is shown**.

//   触摸为主,键盘兜底(方向键移光标、回车翻开、F 插旗、R 重开、ESC 退出)。

//   Touch first, keyboard as backup (arrows move the cursor, Enter reveals, F flags, R restarts, ESC quits).

//

// ◆ Swift 前端的实测约束(写这个文件时逐条撞过,结论都由**真实编译/运行**得到)

// ◆ Measured constraints of the Swift frontend (hit one by one while writing this file; every conclusion comes

//   from a **real compile/run**, not from reading the spec)

//

//   ① **只能有一个全局数组** —— 开多个(`var A` + `var B`)会互相踩内存。

//   ① **Only one global array** — opening several (`var A` + `var B`) makes them clobber each other's memory.

//      所以整个游戏的状态都塞进 `A`,按下标分区(见下面那张表)。

//      So all game state is packed into `A`, partitioned by index (see the table below).

//   ② ~~**字符串拼接是坏的**:`"AB" + "CD"` 得到的是**空串**。判据不是"看着不对",而是~~

//   ② ~~**String concatenation is broken**: `"AB" + "CD"` yields the **empty string**; the criterion was~~

//      ~~`ui_2d_text(10,10, "AB" + "CD", …)` 在 trace 里落成 `str=""`。~~

//      ~~reading what `ui_2d_text` received with `--trace-draw` — it landed as `str=""`.~~

//      ✅ **已修好(2026-10-04 复验,v0.96.592 落地)**:同一句现在在 trace 里落成 `str="ABCD"`。

//      ✅ **Fixed (re-checked 2026-10-04; landed in v0.96.592)**: that same call now lands as `str="ABCD"`.

//      ~~(顺带发现:`snake.swift` / `plane.swift` 的 `numToStr` 只靠 `if v == 0 { return "0" }` 这条捷径才显示出 0。)~~

//      ~~(Side finding: `numToStr` in `snake.swift` / `plane.swift` only showed 0 through the `if v == 0` shortcut.)~~

//      ✅ 同一次复验也推翻了它 —— 分数非 0 时**不再显示成空**,`numToStr(42)` 实测 `"42"`。

//      ✅ Overturned by the same re-check -- a non-zero score is **no longer rendered as empty**; `numToStr(42)` measures `"42"`.

//      ⇒ 数字仍走 `int_to_str()`、多段文本仍拆成多次 `ui_2d_text` 调用 —— 本文件**能跑就别动**;

//      ⇒ numbers still go through `int_to_str()` and multi-piece text is still split across several `ui_2d_text` calls --

//        **新写的代码不必再绕**(可以直接用 `+`,见 `flappy.swift` 的 HUD 那一行)。

//        this file runs, so leave it; **new code need not work around it** (use `+` directly, see flappy's HUD).

//   ③ **函数名不能撞库里已有的符号**:实测 `add` / `min` / `max` / `abs` / `pow` / `sum` 这几个名字

//   ③ **Function names must not collide with symbols already in the libraries**: measured, defining

//      定义出来会被(链接进来的)库版本**静默顶掉** —— `func add(a,b){return a*100+b}; add(3,4)`

//      `add` / `min` / `max` / `abs` / `pow` / `sum` gets **silently overridden** by the library version

//      得到的是 `7`(库里的加法),不是 `304`。⇒ 本文件所有函数都带 `ms` 前缀。

//      that gets linked in — `func add(a,b){return a*100+b}; add(3,4)` yields `7` (the library's addition),

//      not `304`. ⇒ Every function here carries an `ms` prefix.

//      (2026-10-04 复验**仍成立**:实测 `add(3,4)` = 7、改名的 `addx(3,4)` = 304。冲突源是 `Lib/shared/crosslang.vml` 里的裸标签 `add:`)

//      (Re-checked 2026-10-04, **still true**: measured `add(3,4)` = 7 while a renamed `addx(3,4)` = 304. The colliding symbol is the bare label `add:`

//      —— 链接器"后链的赢"把用户版本静默顶掉。)

//      in `Lib/shared/crosslang.vml` -- the linker's "last linked wins" silently overrides the user's version.)

//   ④ 下面这些**要么编不过、要么编译通过但语义是错的**(各条的最小复现见探针报告):

//   ④ The following **either fail to compile or compile into wrong semantics** (minimal reproductions are in the probe report):

//      `struct` / `class` / `enum`(含关联值)/ `protocol` / `extension` / 闭包 / 元组与解构 /

//      `struct` / `class` / `enum` (with associated values) / `protocol` / `extension` / closures / tuples and destructuring /

//      可选**类型**(`Int?`、`??`、`!`)/ `willSet`/`didSet` / 字符串插值 `\(x)` / `switch` 的**元组**模式 / `map`/`filter`/`reduce` / `$0`。

//      optional **types** (`Int?`, `??`, `!`) / `willSet`/`didSet` / string interpolation `\(x)` / **tuple** patterns in `switch` / `map`/`filter`/`reduce` / `$0`.

//      (顺带:字符串插值 `\(x)` 是**编得过但语义错** —— 实测 `"n=\(n)"` 原样落成 `str="n=\(n)"`。)

//      (Note: string interpolation `\(x)` is the **compiles-but-wrong** kind -- measured `"n=\(n)"` traces literally as `str="n=\(n)"`.)

//      ✅ 2026-10-04 复验:下面四项**已不再属于这一类**(实测通过,v0.96.592 落地)——

//      ✅ Re-checked 2026-10-04: four items **no longer belong here** (measured to pass; landed in v0.96.592) --

//         · `defer`(`func f(){ defer{A[1]=99}; return 7 }` ⇒ 返回 7 且 `A[1]`=99);

//         · `defer` (`func f(){ defer{A[1]=99}; return 7 }` ⇒ 7 with `A[1]`=99);

//         · `switch` 的**区间**模式(`v=5` 落进 `case 0...10` ⇒ 111;元组模式仍编不过);

//         · **range** patterns in `switch` (`v=5` hits `case 0...10` ⇒ 111; tuple patterns still fail);

//         · `if let` / `guard let` 的**绑定名**(`if let v = x { r = v }` ⇒ 5,`guard let` 同;`Int?` 类型标注仍编不过);

//         · the **binding name** of `if let` / `guard let` (`if let v = x { r = v }` ⇒ 5, same for `guard let`; an `Int?` annotation still fails);

//         · 调用处参数标签(见下 ⑤)。

//         · call-site argument labels (see (5) below).

//      ⇒ 全篇只用 if/else + while + 数组下标 + 带参函数。

//      ⇒ Everything here uses only if/else + while + array subscripts + functions with arguments.

//   ⑤ ~~**调用处不能写参数标签**(`f(a: 1)` → 解析报错)~~,**定义处的标签是好的** ⇒ 一律写 `f(1)`。

//   ⑤ ~~**No argument labels at call sites** (`f(a: 1)` → parse error)~~, while **labels in the declaration are fine**.

//      ✅ **已修好(2026-10-04 复验)**:调用处也可写标签 —— `g(x: 3, y: 4)` 实测 **7**;本文件仍写 `f(1)`(能跑就别动,**新写的代码不必再绕**)。

//      ✅ **Fixed (re-checked 2026-10-04)**: call-site labels work too -- `g(x: 3, y: 4)` measures **7**; this file still calls as `f(1)` (it runs, so leave it; **new code need not work around it**).

//

// 数值的唯一真源仍是 `Lib/c/vml_ui.h`。

// The single source of truth for these numbers is still `Lib/c/vml_ui.h`.


// 状态表 —— **只用一个全局数组**(见文件头 ①),每格一个 int 的打包编码:

// State table — **one global array only** (see header ①); each cell is one packed int:

//   bit0 = 雷,bit1 = 已翻开,bit2 = 已插旗,bit3.. = 邻雷数×8

//   bit0 = mine, bit1 = revealed, bit2 = flagged, bit3.. = adjacent mines ×8

//   ci = 行 × 列数 + 列(左上角 = 0)

//   ci = row × cols + col (top-left = 0)

//   ├   0..191    棋盘格(9×14 最多 126 格,留余量)

//   ├   0..191    board cells (9×14 is at most 126, with headroom)

//   ├ 192..319    状态槽(见下)

//   ├ 192..319    state slots (below)

//   └ 320..511    泛洪展开的显式栈(容量 192 ≥ 最大格数)

//   └ 320..511    the explicit flood-fill stack (192 ≥ the largest board)

//   192=st 193=cols 194=rows 195=cell 196=ox 197=oy 198=ncell 199=nmines 200=nrevealed 201=nflags

//   202=startTick 203=endSec 204=firstDone 205=w 206=h 207=lang 208=downActive 209=downX 210=downY

//   211=downTick 212=downCi 213=consumed 214=pressCi 215=cursorCi 216=kbMode 217=timerId

//   218=faceS 219=faceX 220=faceY

//   st: 0=待开局 1=进行中 2=通关 3=踩雷

//   st: 0=ready 1=playing 2=won 3=boom

#include "../../../Lib/swift/vml_ui.swift"
var A = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

// ── 界面语言(只查一次,`ui_scr_get_language` 是 syscall)──────────────────

// ── UI language (queried once; `ui_scr_get_language` is a syscall) ──────────────────

func msT(zh: String, en: String) -> String {
    if A[207] == 0 { return zh }
    return en
}

// ── 颜色(0xAARRGGBB 写成负数十进制)──────────────────────────────

// ── Colors (0xAARRGGBB written as negative decimals) ──────────────────────────────

func msColorBg() -> Int { return -15724520 }      // 0xFF101018

func msColorPanel() -> Int { return -15064528 }   // 0xFF1A2230

func msColorRaised() -> Int { return -13023650 }  // 0xFF39465E

func msColorRaised2() -> Int { return -11904392 } // 0xFF4A5A78

func msColorFlat() -> Int { return -14669767 }    // 0xFF202839

func msColorText() -> Int { return -6643536 }     // 0xFF9AA0B0

func msColorWire() -> Int { return -2039584 }     // 0xFFE0E0E0

func msColorFlag() -> Int { return -50384 }       // 0xFFFF3B30

func msColorBoom() -> Int { return -63488 }       // 0xFFFF0800

func msColorFace() -> Int { return -11713 }       // 0xFFFFD23F

func msColorBlack() -> Int { return -16777216 }   // 0xFF000000


// 数字配色:8 个档次各不相同(经典扫雷观感,一眼分得出是几)

// Digit colors: eight distinct levels (the classic Minesweeper look — you can tell the count at a glance)

func msNumColor(n: Int) -> Int {
    if n == 1 { return -11549705 }   // 0xFF4FC3F7

    if n == 2 { return -10044566 }   // 0xFF66BB6A

    if n == 3 { return -36797 }      // 0xFFFF7043

    if n == 4 { return -5552196 }    // 0xFFAB47BC

    if n == 5 { return -13784 }      // 0xFFFFCA28

    if n == 6 { return -14235942 }   // 0xFF26C6DA

    if n == 7 { return -2039584 }    // 0xFFE0E0E0

    return -4342339                  // 0xFFBDBDBD

}

// ── 消息类型 / 键码(唯一真源 Lib/c/vml_ui.h)────────────────────────

// ── Message types / key codes (source of truth: Lib/c/vml_ui.h) ────────────────────

func msMsgKeydown() -> Int { return 1 }
func msMsgTouchdown() -> Int { return 6 }
func msMsgTouchmove() -> Int { return 7 }
func msMsgTouchup() -> Int { return 8 }
func msMsgTimer() -> Int { return 9 }
func msMsgClose() -> Int { return 10 }
func msKeyEnter() -> Int { return 13 }
func msKeyEsc() -> Int { return 27 }
func msKeySpace() -> Int { return 32 }
func msKeyLeft() -> Int { return 37 }
func msKeyUp() -> Int { return 38 }
func msKeyRight() -> Int { return 39 }
func msKeyDown() -> Int { return 40 }
func msKeyF() -> Int { return 70 }
func msKeyR() -> Int { return 82 }

// ── 打包格的四个字段 ────────────────────────────────────────────

// ── The four fields of a packed cell ───────────────────────────────────────────

func msIsMine(ci: Int) -> Int { return A[ci] % 2 }
func msIsRev(ci: Int) -> Int { return A[ci] / 2 % 2 }
func msIsFlag(ci: Int) -> Int { return A[ci] / 4 % 2 }
func msAdj(ci: Int) -> Int { return A[ci] / 8 }

// ── 版面:横 9 列固定、格子边长按可用宽、行数按可用高(自适应)──────

// ── Layout: 9 columns fixed, cell size from the available width, rows from the available height (adaptive) ──

func msLayout() {
    var w = A[205]
    var cols = 9
    var cell = (w - 12) / cols
    // 格子边长与行数同时有上下限:上限 64px / 18 行是**图元预算**倒推出来的 ——

    // Both the cell size and the row count are bounded: the 64px / 18-row caps come backwards from the

    // 18×9 = 162 格 × 每格最多 2 个图元 + HUD ≈ 340 < 400(--trace-draw 实测的最大值见文件头)。

    // **primitive budget** — 18×9 = 162 cells × at most 2 primitives each + HUD ≈ 340 < 400 (the measured

    // maximum is in the header).

    if cell > 64 { cell = 64 }
    if cell < 18 { cell = 18 }
    var topH = 62
    var botH = 34
    var rows = (A[206] - topH - botH - 6) / cell
    if rows < 8 { rows = 8 }
    if rows > 18 { rows = 18 }
    A[193] = cols
    A[194] = rows
    A[195] = cell
    A[198] = cols * rows
    A[196] = (w - cols * cell) / 2
    A[197] = topH
    A[199] = A[198] / 6
    if A[199] < 10 { A[199] = 10 }
}

// 数一个格子周围 8 格的雷数(只在布雷时算一次,画图直接查位)

// Count the mines in the 8 neighbours of one cell (computed once at placement; drawing reads the packed bits)

func msCountAdj(ci: Int) -> Int {
    var n = 0
    var r = ci / A[193]
    var c = ci % A[193]
    var dr = -1
    while dr <= 1 {
        var dc = -1
        while dc <= 1 {
            if dr != 0 || dc != 0 {
                var rr = r + dr
                var cc = c + dc
                if rr >= 0 && rr < A[194] && cc >= 0 && cc < A[193] {
                    if A[rr * A[193] + cc] % 2 != 0 { n = n + 1 }
                }
            }
            dc = dc + 1
        }
        dr = dr + 1
    }
    return n
}

// 布雷:**首点安全区**(safeCi 自己和它周围 8 格都不放雷)

// Place the mines: **a safe zone around the first tap** (safeCi itself and its 8 neighbours get none)

func msPlace(safeCi: Int) {
    var i = 0
    while i < A[198] { A[i] = 0; i = i + 1 }
    var sr = safeCi / A[193]
    var sc = safeCi % A[193]
    var placed = 0
    var tries = 0
    while placed < A[199] && tries < 40000 {
        tries = tries + 1
        var k = ui_math_rand(A[198])
        if A[k] % 2 == 0 {
            var rr = k / A[193]
            var cc = k % A[193]
            var dr = rr - sr
            var dc = cc - sc
            if dr < 0 { dr = 0 - dr }
            if dc < 0 { dc = 0 - dc }
            if dr > 1 || dc > 1 {
                A[k] = A[k] + 1
                placed = placed + 1
            }
        }
    }
    // 邻雷数:每格 +n*8(bit3 起),之后画图与泛洪都只读这个字段

    // Adjacent counts: each cell gets +n*8 (from bit 3 on); drawing and flooding then only read this field

    i = 0
    while i < A[198] {
        A[i] = A[i] + msCountAdj(i) * 8
        i = i + 1
    }
}

// 泛洪展开:**显式栈**(A[320..] 当队列,head/tail 两个下标),**没有递归**

// Flood fill: **an explicit stack** (A[320..] as the queue, head/tail indices), **no recursion**

// 返回 1 = 这一下确实翻开了新格

// returns 1 = this call really did open new cells

func msReveal(start: Int) -> Int {
    if A[start] / 2 % 2 != 0 { return 0 }
    if A[start] / 4 % 2 != 0 { return 0 }
    var head = 0
    var tail = 0
    A[320] = start
    tail = 1
    A[start] = A[start] + 2
    A[200] = A[200] + 1
    while head < tail {
        var k = A[320 + head]
        head = head + 1
        if A[k] / 8 == 0 {
            var r = k / A[193]
            var c = k % A[193]
            var dr = -1
            while dr <= 1 {
                var dc = -1
                while dc <= 1 {
                    var rr = r + dr
                    var cc = c + dc
                    if rr >= 0 && rr < A[194] && cc >= 0 && cc < A[193] {
                        var nk = rr * A[193] + cc
                        // 只推开"不是雷、没翻开、没插旗"的邻格;带数字的格子只翻开、不再展开

                        // Only push neighbours that are "not a mine, not open, not flagged"; numbered cells

                        // are opened but not expanded further

                        if A[nk] % 2 == 0 && A[nk] / 2 % 2 == 0 && A[nk] / 4 % 2 == 0 {
                            A[nk] = A[nk] + 2
                            A[200] = A[200] + 1
                            A[320 + tail] = nk
                            tail = tail + 1
                        }
                    }
                    dc = dc + 1
                }
                dr = dr + 1
            }
        }
    }
    return 1
}

// 踩雷后**把所有雷显示出来**(只置"已翻开"位,不改雷标记)

// After a mine goes off **show every mine** (only the "revealed" bit is set; the mine bits stay as they are)

func msShowMines() {
    var i = 0
    while i < A[198] {
        if A[i] % 2 != 0 { A[i] = A[i] + 2 }
        i = i + 1
    }
}

// 结算:翻开的格数够了就通关

// Settle: enough cells open means the board is cleared

func msCheckWin() {
    if A[200] >= A[198] - A[199] {
        A[192] = 2
        A[203] = (ui_time_get_ms() - A[202]) / 1000
        ui_audio_beep(1568, 240)
        ui_vibrate_simple(70, 1)
    }
}

// 开局:**第一次动手**(翻开或插旗都算)才布雷 —— 而且以这一格为安全区

// Start: mines go down on the **first action** (open or flag both count) — and that cell is the safe zone

func msEnsureStarted(ci: Int) {
    if A[204] == 0 {
        A[204] = 1
        msPlace(ci)
        A[202] = ui_time_get_ms()
        A[192] = 1
    }
}

// 轻点一格:翻开(第一次轻点会先布雷,带安全区)

// Tap a cell: open it (the first tap places the mines first, with the safe zone)

func msTap(ci: Int) {
    if A[192] == 2 || A[192] == 3 { return }
    if ci < 0 || ci >= A[198] { return }
    if A[ci] / 4 % 2 != 0 { return }
    msEnsureStarted(ci)
    if A[ci] % 2 != 0 {
        A[192] = 3
        A[203] = (ui_time_get_ms() - A[202]) / 1000
        msShowMines()
        ui_audio_beep(131, 420)
        ui_vibrate_simple(140, 1)
        return
    }
    if msReveal(ci) != 0 {
        ui_audio_beep(880, 28)
        msCheckWin()
    }
}

// 插旗 / 拔旗(**待开局时也能插**:那就以这一格为安全区先把雷布下去)

// Flag / unflag (**also allowed before the first open**: mines are then placed with this cell as the safe zone)

func msFlag(ci: Int) {
    if A[192] == 2 || A[192] == 3 { return }
    if ci < 0 || ci >= A[198] { return }
    if A[ci] / 2 % 2 != 0 { return }
    msEnsureStarted(ci)
    if A[ci] / 4 % 2 != 0 {
        A[ci] = A[ci] - 4
        A[201] = A[201] - 1
        ui_audio_beep(520, 40)
    } else {
        A[ci] = A[ci] + 4
        A[201] = A[201] + 1
        ui_audio_beep(392, 70)
    }
}

// 屏幕坐标 → 格号(不在棋盘上返回 -1)。⚠ 负数除法先挡掉,不靠它取整。

// Screen coordinates → cell index (-1 when off the board). ⚠ Negative division is guarded against first,

// its rounding is not relied upon.

func msCellAt(x: Int, y: Int) -> Int {
    if x < A[196] { return -1 }
    if y < A[197] { return -1 }
    var c = (x - A[196]) / A[195]
    var r = (y - A[197]) / A[195]
    if c >= A[193] { return -1 }
    if r >= A[194] { return -1 }
    return r * A[193] + c
}

func msHitFace(x: Int, y: Int) -> Int {
    if x >= A[219] && x < A[219] + A[218] && y >= A[220] && y < A[220] + A[218] { return 1 }
    return 0
}

func msElapsed() -> Int {
    if A[192] == 1 { return (ui_time_get_ms() - A[202]) / 1000 }
    if A[192] == 2 || A[192] == 3 { return A[203] }
    return 0
}

func msReset() {
    var i = 0
    while i < A[198] { A[i] = 0; i = i + 1 }
    A[192] = 0
    A[200] = 0
    A[201] = 0
    A[202] = ui_time_get_ms()
    A[203] = 0
    A[204] = 0
    A[208] = 0
    A[212] = 0
    A[213] = 0
    A[214] = -1
    A[215] = 0
    A[216] = 0
    // 上一局没读完的输入(一次点击常有多条)不能喂给新一局。

    // ⚠ Input left over from the previous round must not feed the new one, but **the very first call at

    // ⚠ **但程序刚起来那一次不能丢**:启动(开窗 + 首帧)不比玩家的手快多少,

    //    startup must not drop anything**: start-up (open window + first frame) is not much faster than a

    //    早到的那一下轻点会正躺在队列里,被这句 `ui_msg_clear()` 静默吃掉 ⇒

    //    player's finger, so an early tap is already in the queue and this `ui_msg_clear()` silently ate it.

    //    玩家看到"点了一下没反应"。所以只从**第二局**起清队列。

    //    The queue is therefore cleared only from the **second** round on.

    if A[221] != 0 { ui_msg_clear() }
    else { A[221] = 1 }
}

// ── 每拍(50ms)一次:长按判定 + 计时刷新 ──────────────────────────

// ── Once per tick (50ms): long-press detection + clock refresh ──────────────────────────

func msTick() {
    if A[208] == 1 && A[213] == 0 {
        if ui_time_get_ms() - A[211] >= 450 {
            // **手指还按着**就把旗插上(不等抬手)—— 这就是"长按插旗"的判定点

            // The flag is toggled **while the finger is still down** (it does not wait for the lift) —

            // that is the decision point of "long-press to flag"

            msFlag(A[212])
            A[213] = 1
            A[214] = -1
            ui_vibrate_simple(35, 1)
        }
    }
}

func msMoveCursor(dc: Int, dr: Int) {
    A[216] = 1
    var c = A[215] % A[193]
    var r = A[215] / A[193]
    c = c + dc
    r = r + dr
    if c < 0 { c = 0 }
    if c >= A[193] { c = A[193] - 1 }
    if r < 0 { r = 0 }
    if r >= A[194] { r = A[194] - 1 }
    A[215] = r * A[193] + c
}

// ── 画一帧 ───────────────────────────────────────────────────

// ── Draw one frame ───────────────────────────────────────────

func msDraw() {
    ui_2d_clear(msColorBg())
    var w = A[205]
    var h = A[206]
    var cell = A[195]
    var s = cell - 2
    var i = 0
    while i < A[198] {
        var r = i / A[193]
        var c = i % A[193]
        var x = A[196] + c * cell
        var y = A[197] + r * cell
        if msIsRev(i) != 0 {
            ui_2d_rect(x, y, s, s, msColorFlat(), 1, 0, 2)
            if msIsMine(i) != 0 {
                ui_2d_circle(x + s / 2, y + s / 2, s / 4, msColorBoom(), 1, 0)
            } else {
                var n = msAdj(i)
                if n > 0 { ui_2d_text_v(x + s / 2, y + s / 2, int_to_str(n), msNumColor(n), cell * 3 / 5, VML_ANCHOR_CENTER, VML_VANCHOR_MIDDLE, VML_FONT_BOLD) }
            }
        } else {
            if A[214] == i { ui_2d_rect(x, y, s, s, msColorRaised2(), 1, 0, 3) }
            else { ui_2d_rect(x, y, s, s, msColorRaised(), 1, 0, 3) }
            if msIsFlag(i) != 0 {
                var cx = x + s / 2
                ui_2d_line(cx, y + 5, cx, y + s - 5, msColorFlag(), 2)
                ui_2d_rect(cx + 1, y + 6, s / 3, s / 4, msColorFlag(), 1, 0, 0)
            }
        }
        i = i + 1
    }

    // 键盘光标(只有用过键盘才画,触摸玩的时候屏幕上不多一样东西)

    // Keyboard cursor (drawn only after the keyboard has been used, so touch play sees nothing extra)

    if A[216] == 1 {
        var cc = A[215] % A[193]
        var cr = A[215] / A[193]
        ui_2d_rect(A[196] + cc * cell - 2, A[197] + cr * cell - 2, cell + 2, cell + 2, msColorWire(), 0, 2, 3)
    }

    // 顶部面板

    // Top panel

    ui_2d_rect(0, 0, w, A[197] - 4, msColorPanel(), 1, 0, 0)
    // 剩余雷数(插旗会减)—— 小旗图标 + 数字

    // Mines remaining (flagging decrements it) — flag icon plus the number

    ui_2d_line(22, 14, 22, 40, msColorFlag(), 3)
    ui_2d_rect(23, 15, 16, 12, msColorFlag(), 1, 0, 0)
    ui_2d_text(46, 16, int_to_str(A[199] - A[201]), msColorWire(), 20, VML_ANCHOR_LEFT)
    // 计时

    // Elapsed timer

    ui_2d_circle(w - 56, 26, 12, msColorWire(), 0, 2)
    ui_2d_line(w - 56, 26, w - 56, 18, msColorWire(), 2)
    ui_2d_text(w - 14, 16, int_to_str(msElapsed()), msColorWire(), 20, VML_ANCHOR_RIGHT)
    // 笑脸(点它重开)

    // Smiley face (tap it to restart)

    var fx = A[219] + A[218] / 2
    var fy = A[220] + A[218] / 2
    ui_2d_circle(fx, fy, A[218] / 2, msColorFace(), 1, 0)
    ui_2d_rect(fx - 10, fy - 7, 6, 6, msColorBlack(), 1, 0, 3)
    ui_2d_rect(fx + 4, fy - 7, 6, 6, msColorBlack(), 1, 0, 3)
    if A[192] == 3 { ui_2d_line(fx - 11, fy + 11, fx + 11, fy + 4, msColorBlack(), 2) }
    else if A[192] == 2 { ui_2d_line(fx - 11, fy + 4, fx + 11, fy + 11, msColorBlack(), 2) }
    else { ui_2d_line(fx - 11, fy + 8, fx + 11, fy + 8, msColorBlack(), 2) }

    // 底部提示

    // Bottom hint line

    var hint = msT("长按插旗 · 轻点翻开", "Tap to open · long-press to flag")
    if A[192] == 0 { hint = msT("轻点任意一格开始(首点安全)", "Tap any cell to start (first tap is safe)") }
    else if A[192] == 2 { hint = msT("通关!点笑脸再来一局", "Cleared! Tap the face to play again") }
    else if A[192] == 3 { hint = msT("踩雷了 · 点笑脸重开", "Boom. Tap the face to retry") }
    ui_2d_text(w / 2, h - 24, hint, msColorText(), 13, VML_ANCHOR_CENTER)
    ui_2d_present()
}

func main() {
    // 界面语言:开局问一次宿主(0=中文 1=英文),存进 A[207](见 msT)

    // UI language: ask the host once at the start (0=Chinese 1=English) and store it in A[207] (see msT)

    A[207] = ui_scr_get_language()
    var lang = A[207]

    // 竖屏 + 不要屏幕手柄区(画布吃满整屏)—— 声明在**开窗之前**就生效

    // Portrait + no on-screen gamepad (the canvas fills the screen) — declared **before** the window opens

    // 开窗:尺寸传 0 ⇒ 交给宿主给真实可用区(开窗**之前**问到的只是估算值)

    // Opening: pass size 0 ⇒ the host supplies the real available area (a **pre-open** read is only an estimate)

    ui_win_open_ex(lang == 0 ? "扫雷" : "Minesweeper", 0, 0, VML_WIN_PORTRAIT, VML_WIN_NO_GAMEPAD)

    // 尺寸:开窗**之后**再问一次 —— 这时 `ui_scr_get_w/h` 给的是实测画布(真值)

    // Size: re-read **after** the open — `ui_scr_get_w/h` now report the measured canvas (the real value)

    A[205] = ui_scr_get_w()
    A[206] = ui_scr_get_h()
    if A[205] <= 0 { A[205] = 411 }
    if A[206] <= 0 { A[206] = 726 }
    msLayout()
    A[218] = 44
    A[219] = A[205] / 2 - 22
    A[220] = 8

    ui_scr_set_keep_on(1)
    msReset()
    msDraw()

    A[217] = ui_timer_set(50, 0)

    while ui_win_is_closed() == 0 {
        var t = ui_msg_fetch_wait(50)
        if t == msMsgClose() { break }
        if t == msMsgKeydown() {
            var k = ui_msg_get_a()
            if k == msKeyEsc() { break }
            else if k == msKeyLeft() { msMoveCursor(-1, 0) }
            else if k == msKeyRight() { msMoveCursor(1, 0) }
            else if k == msKeyUp() { msMoveCursor(0, -1) }
            else if k == msKeyDown() { msMoveCursor(0, 1) }
            else if k == msKeyEnter() || k == msKeySpace() { msTap(A[215]) }
            else if k == msKeyF() { msFlag(A[215]) }
            else if k == msKeyR() { msReset() }
            msDraw()
        }
        else if t == msMsgTouchdown() {
            var x = ui_msg_get_a()
            var y = ui_msg_get_b()
            if msHitFace(x, y) != 0 {
                msReset()
            } else {
                var ci = msCellAt(x, y)
                if ci >= 0 {
                    A[208] = 1
                    A[209] = x
                    A[210] = y
                    A[211] = ui_time_get_ms()
                    A[212] = ci
                    A[213] = 0
                    A[214] = ci
                }
            }
            msDraw()
        }
        else if t == msMsgTouchmove() {
            // 移动类消息是"追最新位置"的语义,旧位置没有价值:先丢积压,免得越拖越滞后

            // Move messages mean "follow the newest position"; old ones are worthless — drop the backlog first

            ui_msg_drop(1)
            if A[208] == 1 && A[213] == 0 {
                var x = ui_msg_get_a()
                var y = ui_msg_get_b()
                var dx = x - A[209]
                var dy = y - A[210]
                if dx < 0 { dx = 0 - dx }
                if dy < 0 { dy = 0 - dy }
                // 手指划走一格以上 = 不是"按住了",取消这一下(抬手也不翻开)

                // Moving more than one cell means "not holding still": cancel it (lifting will not open either)

                if dx > A[195] || dy > A[195] { A[213] = 2; A[214] = -1 }
            }
            msDraw()
        }
        else if t == msMsgTouchup() {
            if A[208] == 1 {
                if A[213] == 0 { msTap(A[212]) }
                A[208] = 0
                A[213] = 0
                A[214] = -1
                msDraw()
            }
        }
        else if t == msMsgTimer() {
            msTick()
            msDraw()
        }
    }

    ui_timer_kill(A[217])
    ui_scr_set_keep_on(0)
    ui_win_close()
}