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