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

kotlin/app/minesweeper.kt · 32825 字节 · 下载原文

// 扫雷 —— 用 **Kotlin** 写的手机游戏(9×9、10 颗雷、首击必安全、插旗、计时)

// Minesweeper — a phone game written in **Kotlin** (9x9, 10 mines, a safe first tap, flags, a timer)

//

// 玩法:点开格子,数字表示"周围八格里有几颗雷";把所有非雷格都点开就赢,点中雷就输。

// Play: open cells; a number tells how many mines sit in the eight cells around it. Open every

// non-mine cell to win, tap a mine and you lose.

//

// ## 它补的是"逻辑推理"这一档

// ## It fills the **logic / deduction** slot

//

// `snake.kt` 是实时动作,`gomoku.kt` 是回合制对弈,`puzzle15.kt` 是滑块谜题 —— 这一份是 **靠推理缩小范围**的游戏:没有时间压力,胜负取决于"敢不敢点"。

// `snake.kt` is real-time action, `gomoku.kt` is a turn-based board game and `puzzle15.kt` is a

// sliding puzzle — this one is a **deduction** game: no time pressure, and winning is about deciding

// which cell is safe to open.

//

// ## 三条**设计上必须写对、写错了玩家看不出来**的规则

// ## Three rules that **must be right and whose failure the player cannot see**

//

//   · **首击必安全**:第一次点开的格子周围 3×3 **不放雷**。少了这条,玩家一开局就可能一点即死 —— 那不是难度,是坏局(15 拼图那条"一半的局无解"是同一族问题)。

//   · **the first tap is always safe**: no mine is placed in the 3x3 around the first cell opened.

//     Without it the very first tap can kill you — that is not difficulty, that is a broken game

//     (same family as the "half of all 15-puzzle shuffles are unsolvable" problem).

//   · **翻开是一片用栈、不用递归**:点到 0 格要连带翻开一整片。~~递归在本前端没有验证过~~ —— 2026-10-04 复验递归已可用(`fact(5)`=120);这里仍**用显式栈**,因为它还有个额外好处 —— 入栈时就把 `gOpen` 置 1,同一个格子**永远不会被压两次** (否则 81 格的棋盘能压出几百个待处理项,栈要开多大就成了猜谜)。

//   · **the cascade uses an explicit stack, not recursion**: opening a 0-cell must flood the whole

//     region. ~~Recursion is unproven on this frontend~~ -- re-verified 2026-10-04: recursion works (`fact(5)`=120); this file still

//     uses an **explicit stack** because it buys a second thing — `gOpen` is set at push time, so **no cell is ever

//     pushed twice** (otherwise an 81-cell board can queue hundreds of pending entries and sizing the stack becomes guesswork).

//   · **"插旗"必须是一个模式,不能绑长按**:长按要靠 `TOUCHUP` 判时长,而手指滑出格子、 系统吃掉 CANCEL 都会让 UP 丢失 —— 那时旗子会**卡在按下状态**。一个显式的模式按钮没有这个问题。

//   · **flagging is a mode, never a long-press**: a long press needs `TOUCHUP` to measure duration, and

//     UP is lost whenever the finger slides off the cell or the system eats CANCEL — the flag then

//     **sticks in the pressed state**. An explicit mode button has no such failure.

//

// ## 这一份例程用到的 Kotlin 特性

// ## Kotlin features used here

//

//   · **字符串模板**(`"用时 $gSec 秒"`)与 **`when` 作表达式**(`numColor`);

//   · **string templates** (`"time $gSec s"`) and a **`when` as an expression** (`numColor`);

//   · **多参数函数**、**扩展函数** `fun Int.c9()` / `r9()`;

//   · **multi-parameter functions** and the **extension functions** `fun Int.c9()` / `r9()`;

//   · **`for (i in 0..n)`** + **`x in a..b`** 双重区间判定;

//   · **`for (i in 0..n)`** plus double `x in a..b` range tests;

//   · **顶层 `var` 数组**(棋盘的四个数组全在文件级);

//   · **file-level `var` arrays** (all four board arrays live at file scope);

//   · **无 C 风格三目** —— 条件取值一律写全 `if/else`(`cond ? a : b` 2026-10-04 复验仍会静默错编;Kotlin 的 Elvis `?:` 已经能用)。

//   · **no C-style ternary** — every conditional value is written as a full `if/else` (`cond ? a : b` still silently miscompiles as of 2026-10-04; Kotlin's Elvis `?:` works now).

//

// ## 音效(`ui_audio_beep` 单通道:一个事件一个音,连发只听得见最后一个)

// ## Sound (`ui_audio_beep` is single-channel: one event one tone, a burst only lets the last be heard)

//   翻开 600/45 · 插旗 780/50 · 拔旗 520/50 · 进插旗模式 900/60 · 出插旗模式 340/60 · 重开 660/90

//   reveal 600/45 · flag on 780/50 · flag off 520/50 · flag mode on 900/60 · off 340/60 · restart 660/90

//   踩雷 131/320(**最低**)、通关 1047/320(**最高**)—— 输赢取两端极值,不看屏幕也分得出输赢

//   boom 131/320 (**lowest**), win 1047/320 (**highest**) — the endings take the two extremes, so the outcome is audible without looking

// ⚠ 最低音只到 131Hz(C3):手机外放在 200Hz 以下衰减很快,写 65Hz 出来是"噗"的一声,玩家听着像没响

// ⚠ nothing goes below 131 Hz (C3): a phone speaker rolls off fast under 200 Hz, where a 65 Hz tone just sounds like a click

//

// 跑法 —— 手机:`vml run examples/kotlin/minesweeper.kt`

// How to run — on the phone: `vml run examples/kotlin/minesweeper.kt`

// 跑法 —— 桌面:`vmlcli Examples/kotlin/minesweeper.kt --screen 480x640 --frames /tmp/fr`

// How to run — on the desktop: `vmlcli Examples/kotlin/minesweeper.kt --screen 480x640 --frames /tmp/fr`

//   两档确定性自动局(固定种子):

//   Two deterministic auto-play modes (fixed seed):

//     `--arg demo` → 按行序乱点,**会踩雷**(验收"输"的路径与翻开过程)

//     `--arg demo` → row-major blind tapping: **it will hit a mine** (exercises the loss path)

//     `--arg demo --arg W` → 自动避开雷,**一定通关**(验收"赢"的路径)

//     `--arg demo --arg W` → auto-play that avoids mines: **always clears the board** (win path)


// ── 显式引用:下面用到的 `ui_*` 来自共享库 vmlui.vml ──────────────────

// ── Explicit reference: the `ui_*` used below come from the shared library vmlui.vml ──

// ⚠ `.*` 必须有:`import vmlui`(无 `.*`)会被 `ExtractImports` 的正则**静默忽略**。

// ⚠ The `.*` is required: `import vmlui` (without `.*`) is silently ignored by `ExtractImports`.

// ⚠ 但整行删掉也照样编、照样跑 —— 前端靠 `ui_` 前缀**自动**链 `vmlui.vml`。

// ⚠ But deleting the whole line still compiles and runs — the frontend auto-links `vmlui.vml` from the `ui_` prefix.

// ⇒ 所以这条是**文档级**的显式声明,不是强制开关(判据见 `FRONTEND_DEFECTS.md` 的 Kotlin 一节)。

// ⇒ So this is a **documentation-level** explicit reference, not a hard switch (see the Kotlin section of `FRONTEND_DEFECTS.md`).

// ✅ 2026-10-04 复验仍成立:三种写法(`import vmlui` / `import vmlui.*` / 整行删掉)产出的指令数完全相同(59377 条)。

// ✅ Re-verified 2026-10-04, still holds: all three forms (`import vmlui` / `import vmlui.*` / line deleted) produce the identical instruction count (59377).

#include "../../../Lib/kotlin/vml_ui.kt"
import vmlui.*

const val COLS = 9
const val ROWS = 9
const val CELLS = COLS * ROWS
const val MINES = 10

// ── 配色:十六进制 `0xAARRGGBB` ──

// ── Palette: hex `0xAARRGGBB` ──

// 历史:v0.96.592 及之前本前端不认 `0x` 字面量(词法器把它切成 `0` + 一个标识符)。

// History: up to v0.96.592 this frontend did not accept `0x` literals (the lexer split them into `0` plus an identifier).

// 实参位置还会被静默吃掉 ⇒ 颜色变 0、一条错都不报;当时只能用负十进制绕过去。

// In argument position the stray identifier was swallowed silently ⇒ the color became 0 with no error; negative decimals were the workaround.

// 现已修(词法器认 `0x`/`0b`,解析器按 **32 位字**收窄)。

// Fixed now (the lexer accepts `0x`/`0b` and the parser narrows to a **32-bit word**).

// ✅ 2026-10-04 复验仍成立:`0xFF` 得 255、`0xFF101820` 得 -15722464、`0b1011` 得 11,实参位置也不再被吃掉。

// ✅ Re-verified 2026-10-04, still holds: `0xFF` gives 255, `0xFF101820` gives -15722464, `0b1011` gives 11, and argument positions are no longer swallowed.

// 下面那张是当年的对照表,留作记录。

// The table below is the old cross-reference, kept for the record.

//   BG     0xFF101018 = -15724520     PANEL  0xFF1A2230 = -15064528

//   CLOSED 0xFF3A4A66 = -12957082     OPENED 0xFF232A38 = -14472648

//   EDGE   0xFF5A6A88 = -10851704     TEXT   0xFFE8E8F0 =  -1513232

//   MUTED  0xFF9AA0B0 =  -6643536     GOLD   0xFFFFC040 =    -16320

//   GREEN  0xFF51E86E = -11409298     RED    0xFFE06050 =  -2072496

//   BUTTON 0xFF2A3A56 = -14009770

// 数字 1..8 的颜色(扫雷的老惯例:1 蓝 2 绿 3 红 4 紫 …)

// Number colors 1..8 (the classic convention: 1 blue, 2 green, 3 red, 4 purple, ...)

//   N1 0xFF4E9AE0 = -11625760    N2 0xFF51E86E = -11409298

//   N3 0xFFE06050 =  -2072496    N4 0xFF8A5AD0 =  -7710000

//   N5 0xFFD9B44A =  -2509750    N6 0xFF4EC9D9 = -11613735

//   N7 0xFFE08AC0 =  -2061632    N8 0xFFB0B8C8 =  -5195576

var LANG = 0
var SW = 0
var SH = 0
var BX = 0
var BY = 0
var CELL = 0
var gMine = arrayOf(
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0)
var gOpen = arrayOf(
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0)
var gFlag = arrayOf(
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0)
var gAdj = arrayOf(
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,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 explicit reveal stack: its capacity is the cell count (see the "never pushed twice" note above)

var gStack = arrayOf(
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0)
// 首击之前不布雷:雷是**首击那一刻**才布的,而且绕开首击格周围的 3×3

// No mine is placed before the first tap: they appear at that moment, avoiding the 3x3 around it

var gStarted = 0
var gDead = 0
var gWon = 0
var gRevealed = 0
var gFlags = 0
// 模式:0 = 挖开、1 = 插旗

// Mode: 0 = dig, 1 = flag

var gFlagMode = 0
// 键盘光标(正中那一格)

// The keyboard cursor (the centre cell)

var gCursor = 40
var gSec = 0
var gT0 = 0
var gSeed = 1
var gAuto = 0
var gAutoWin = 0
var gTick = 0
var gTickLimit = 0
var gMaxPrims = 0

/// 确定性伪随机(**不用 `ui_math_rand`**:验收要能复现同一局)

/// Deterministic PRNG (**not `ui_math_rand`**: acceptance needs to reproduce the same board)

fun rnd(n: Int): Int {
    gSeed = gSeed * 1103515245 + 12345
    if (gSeed < 0) { gSeed = 0 - gSeed }
    return gSeed % n
}

// 扩展函数

// extension functions

fun Int.c9(): Int { return this % COLS }
fun Int.r9(): Int { return this / COLS }

/// 八邻域

//the eight neighbours

fun neighborCount(idx: Int): Int {
    var r = idx.r9()
    var c = idx.c9()
    var n = 0
    if (r > 0 && c > 0 && gMine[idx - COLS - 1] == 1) { n = n + 1 }
    if (r > 0 && gMine[idx - COLS] == 1) { n = n + 1 }
    if (r > 0 && c < COLS - 1 && gMine[idx - COLS + 1] == 1) { n = n + 1 }
    if (c > 0 && gMine[idx - 1] == 1) { n = n + 1 }
    if (c < COLS - 1 && gMine[idx + 1] == 1) { n = n + 1 }
    if (r < ROWS - 1 && c > 0 && gMine[idx + COLS - 1] == 1) { n = n + 1 }
    if (r < ROWS - 1 && gMine[idx + COLS] == 1) { n = n + 1 }
    if (r < ROWS - 1 && c < COLS - 1 && gMine[idx + COLS + 1] == 1) { n = n + 1 }
    return n
}

/// 把八邻域压进栈(**入栈当刻就置 `gOpen`**,所以每个格子至多进栈一次)

/// Push the eight neighbours (**`gOpen` is set at push time**, so a cell enters the stack at most once)

fun pushNeighbors(idx: Int, top: Int): Int {
    var r = idx.r9()
    var c = idx.c9()
    var t = top
    var n = 0
    while (n < 8) {
        var nr = r
        var nc = c
        if (n == 0 || n == 1 || n == 2) { nr = r - 1 }
        if (n == 5 || n == 6 || n == 7) { nr = r + 1 }
        if (n == 0 || n == 3 || n == 5) { nc = c - 1 }
        if (n == 2 || n == 4 || n == 7) { nc = c + 1 }
        if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS) {
            var k = nr * COLS + nc
            if (gOpen[k] == 0 && gFlag[k] == 0) {
                gOpen[k] = 1
                gRevealed = gRevealed + 1
                gStack[t] = k
                t = t + 1
            }
        }
        n = n + 1
    }
    return t
}

/// 布雷:**首击那一刻才布**,并且绕开首击格周围的 3×3(首击必安全)

/// Place the mines **at the moment of the first tap**, avoiding the 3x3 around it (a safe first tap)

fun layMines(safeIdx: Int) {
    var i = 0
    while (i < CELLS) { gMine[i] = 0; i = i + 1 }
    var placed = 0
    var sr = safeIdx.r9()
    var sc = safeIdx.c9()
    var guard = 0
    while (placed < MINES && guard < 5000) {
        var k = rnd(CELLS)
        var r = k.r9()
        var c = k.c9()
        // 3×3 安全区:行列都落在 [safe-1, safe+1] 内就重摇

        // the 3x3 safe zone: re-roll when both the row and the column fall inside [safe-1, safe+1]

        var near = 0
        if (r >= sr - 1 && r <= sr + 1 && c >= sc - 1 && c <= sc + 1) { near = 1 }
        if (near == 0 && gMine[k] == 0) {
            gMine[k] = 1
            placed = placed + 1
        }
        guard = guard + 1
    }
    i = 0
    while (i < CELLS) { gAdj[i] = neighborCount(i); i = i + 1 }
    gStarted = 1
    gT0 = ui_time_get_ms()
}

fun restart() {
    var i = 0
    while (i < CELLS) {
        gMine[i] = 0
        gOpen[i] = 0
        gFlag[i] = 0
        gAdj[i] = 0
        i = i + 1
    }
    gStarted = 0
    gDead = 0
    gWon = 0
    gRevealed = 0
    gFlags = 0
    gFlagMode = 0
    gCursor = 40
    gSec = 0
    gTick = 0
}

/// 点开一格:踩雷 → 输;否则连带翻开一整片。返回 0=没变化 1=翻开过 2=踩雷 3=赢了

/// Open a cell: a mine loses; otherwise the whole region floods open.

/// Returns 0=no change 1=opened 2=hit a mine 3=won

fun reveal(idx: Int): Int {
    if (gDead == 1 || gWon == 1) { return 0 }
    if (idx < 0 || idx >= CELLS) { return 0 }
    if (gOpen[idx] == 1 || gFlag[idx] == 1) { return 0 }
    if (gStarted == 0) { layMines(idx) }
    if (gMine[idx] == 1) {
        gOpen[idx] = 1
        gDead = 1
        ui_audio_beep(131, 320)
        return 2
    }
    var top = 0
    gOpen[idx] = 1
    gRevealed = gRevealed + 1
    gStack[top] = idx
    top = top + 1
    while (top > 0) {
        top = top - 1
        var c = gStack[top]
        if (gAdj[c] == 0) { top = pushNeighbors(c, top) }
    }
    // 数字格子给一个短促的"咔";一片翻开也只响一声(事件在 tap 那一层,不在这里循环里)

    // A short click for a numbered cell; a cascade still fires only once (the event is at the tap

    // layer, not inside this loop)

    ui_audio_beep(600, 45)
    if (gRevealed >= CELLS - MINES) {
        gWon = 1
        gSec = elapsedSec()
        ui_audio_beep(1047, 320)
        return 3
    }
    return 1
}

/// 插旗 / 拔旗。返回 0=没变化 1=插上 2=拔掉

/// Toggle a flag. Returns 0=no change 1=planted 2=removed

fun toggleFlag(idx: Int): Int {
    if (gDead == 1 || gWon == 1) { return 0 }
    if (idx < 0 || idx >= CELLS) { return 0 }
    if (gOpen[idx] == 1) { return 0 }
    if (gFlag[idx] == 1) {
        gFlag[idx] = 0
        gFlags = gFlags - 1
        ui_audio_beep(520, 50)
        return 2
    }
    gFlag[idx] = 1
    gFlags = gFlags + 1
    ui_audio_beep(780, 50)
    return 1
}

/// 一个格子被点:按当前模式走

//a cell was tapped: act according to the current mode

fun tapCell(idx: Int) {
    if (gDead == 1 || gWon == 1) { return }
    if (gFlagMode == 1) { toggleFlag(idx) } else { reveal(idx) }
}

fun elapsedSec(): Int {
    if (gStarted == 0) { return 0 }
    var ms = ui_time_get_ms() - gT0
    if (ms < 0) { ms = 0 }
    return ms / 1000
}

/// 数字颜色 —— **`when` 作表达式**(修之前它把数据标签的地址当结果返回)

/// Number color — a **`when` as an expression** (it used to return a data-label address)

fun numColor(n: Int): Int {
    return when {
        n == 1 -> 0xFF4E9AE0
        n == 2 -> 0xFF51E86E
        n == 3 -> 0xFFE06050
        n == 4 -> 0xFF8A5AD0
        n == 5 -> 0xFFD9B44A
        n == 6 -> 0xFF4EC9D9
        n == 7 -> 0xFFE08AC0
        else -> 0xFFB0B8C8
    }
}

fun relayout() {
    SW = ui_scr_get_w()
    SH = ui_scr_get_h()
    if (SW <= 0) { SW = 411 }
    if (SH <= 0) { SH = 726 }
    BY = 96
    // 宽高都要管:只按宽算,矮屏上底部两个按钮会被挤出去

    // respect width *and* height — width alone pushes the two buttons off a short screen

    var cw = (SW - 24) / COLS
    var chh = (SH - BY - 168) / ROWS
    CELL = cw
    if (chh < CELL) { CELL = chh }
    if (CELL < 12) { CELL = 12 }
    BX = (SW - COLS * CELL) / 2
}

/// 屏幕上一个格子的左上角像素坐标(画与命中判定共用这一处)

/// Pixel top-left of a cell (drawing and hit testing share this one function)

fun cellX(idx: Int): Int { return BX + idx.c9() * CELL }
fun cellY(idx: Int): Int { return BY + idx.r9() * CELL }

fun statusText() {
    if (gWon == 1) {
        if (LANG == 0) { ui_2d_text(SW / 2, 62, "通关!用了 $gSec 秒", 0xFF51E86E, 16, VML_ANCHOR_CENTER) }
        else { ui_2d_text(SW / 2, 62, "Cleared in $gSec s", 0xFF51E86E, 16, VML_ANCHOR_CENTER) }
    } else if (gDead == 1) {
        if (LANG == 0) { ui_2d_text(SW / 2, 62, "踩雷了 · 点屏幕重开", 0xFFE06050, 16, VML_ANCHOR_CENTER) }
        else { ui_2d_text(SW / 2, 62, "Boom · tap to restart", 0xFFE06050, 16, VML_ANCHOR_CENTER) }
    } else {
        // **字符串模板**:修之前这一句会静默打出一串数字,而不是画在状态行上

        // **String template**: before the fix this silently printed digits instead of drawing the line

        // ✅ 2026-10-04 复验:字符串模板已可用(`"n=$n"` ⇒ `n=7`),这一段保持原样。

        // ✅ Re-verified 2026-10-04: string templates work (`"n=$n"` => `n=7`); this section is unchanged.

        var left = MINES - gFlags
        if (LANG == 0) { ui_2d_text(SW / 2, 62, "还剩 $left 颗雷 · 已翻开 $gRevealed 格", 0xFFE8E8F0, 15, VML_ANCHOR_CENTER) }
        else { ui_2d_text(SW / 2, 62, "$left mines left · $gRevealed open", 0xFFE8E8F0, 15, VML_ANCHOR_CENTER) }
    }
}

/// 一格上画旗子:一根杆 + 一面小旗(**不用图片**,两个图元拼出来)

/// The flag on a cell: a pole plus a small banner (**no image asset**, two primitives)

fun drawFlag(x: Int, y: Int) {
    var cx = x + CELL / 2
    var cy = y + CELL / 2
    ui_2d_rect(cx - 1, cy - CELL / 4, 3, CELL / 2, 0xFFFFC040, 1, 0, 0)
    ui_2d_circle(cx + CELL / 6, cy - CELL / 6, CELL / 7, 0xFFE06050, 1, 0)
}

/// 一格上画雷:一个圆 + 四根刺

//the mine on a cell: a circle plus four spikes

fun drawMine(x: Int, y: Int) {
    var cx = x + CELL / 2
    var cy = y + CELL / 2
    var r = CELL / 5
    ui_2d_circle(cx, cy, r, 0xFFE8E8F0, 1, 0)
    ui_2d_line(cx - r - 3, cy, cx + r + 3, cy, 0xFFE8E8F0, 2)
    ui_2d_line(cx, cy - r - 3, cx, cy + r + 3, 0xFFE8E8F0, 2)
    ui_2d_line(cx - r, cy - r, cx + r, cy + r, 0xFFE8E8F0, 1)
    ui_2d_line(cx - r, cy + r, cx + r, cy - r, 0xFFE8E8F0, 1)
}

fun draw() {
    ui_2d_clear(0xFF101018)

    if (LANG == 0) { ui_2d_text_styled(SW / 2, 18, "扫雷", 0xFFE8E8F0, 20, VML_ANCHOR_CENTER, VML_FONT_BOLD) }
    else { ui_2d_text_styled(SW / 2, 18, "Minesweeper", 0xFFE8E8F0, 20, VML_ANCHOR_CENTER, VML_FONT_BOLD) }
    statusText()

    // 棋盘底

    // board panel

    ui_2d_rect(BX - 6, BY - 6, COLS * CELL + 12, ROWS * CELL + 12, 0xFF1A2230, 1, 0, 8)

    var i = 0
    while (i < CELLS) {
        var x = cellX(i)
        var y = cellY(i)
        if (gOpen[i] == 1) {
            ui_2d_rect(x + 1, y + 1, CELL - 2, CELL - 2, 0xFF232A38, 1, 0, 3)
            if (gMine[i] == 1) {
                drawMine(x, y)
            } else {
                var n = gAdj[i]
                if (n > 0) {
                    var fs = CELL / 2
                    if (fs < 10) { fs = 10 }
                    ui_2d_text_v(x + CELL / 2, y + CELL / 2, "$n", numColor(n), fs, VML_ANCHOR_CENTER, VML_VANCHOR_MIDDLE, VML_FONT_BOLD)
                }
            }
        } else {
            // 盖上:填色 + 一圈亮边("能点"的观感全靠这圈边)

            // covered: fill plus a lighter rim (that rim is what makes it look tappable)

            ui_2d_rect(x + 1, y + 1, CELL - 2, CELL - 2, 0xFF3A4A66, 1, 0, 3)
            ui_2d_rect(x + 2, y + 2, CELL - 4, CELL - 4, 0xFF5A6A88, 0, 2, 3)
            if (gFlag[i] == 1) { drawFlag(x, y) }
        }
        i = i + 1
    }

    // 输的那一局:把所有没标出来的雷**亮出来**(不亮玩家不知道输在哪)

    // On a loss, **show** every mine that was not flagged (otherwise the player cannot see why)

    if (gDead == 1) {
        i = 0
        while (i < CELLS) {
            if (gMine[i] == 1 && gFlag[i] == 0 && gOpen[i] == 0) {
                ui_2d_rect(cellX(i) + 1, cellY(i) + 1, CELL - 2, CELL - 2, 0xFF232A38, 1, 0, 3)
                drawMine(cellX(i), cellY(i))
            }
            i = i + 1
        }
    }

    // 键盘光标

    // keyboard cursor

    if (gDead == 0 && gWon == 0 && gCursor >= 0) {
        ui_2d_rect(cellX(gCursor) - 1, cellY(gCursor) - 1, CELL + 2, CELL + 2, 0xFF51E86E, 0, 2, 3)
    }

    // 两个按钮:模式开关 / 重开

    // two buttons: the mode toggle and restart

    var bw = (SW - 36) / 2
    var byy = BY + ROWS * CELL + 24
    if (gFlagMode == 1) {
        ui_2d_rect(12, byy, bw, 44, 0xFFFFC040, 1, 0, 8)
        if (LANG == 0) { ui_2d_text(12 + bw / 2, byy + 22, "插旗模式", 0xFF101018, 16, VML_ANCHOR_CENTER) }
        else { ui_2d_text(12 + bw / 2, byy + 22, "FLAG mode", 0xFF101018, 16, VML_ANCHOR_CENTER) }
    } else {
        ui_2d_rect(12, byy, bw, 44, 0xFF2A3A56, 1, 0, 8)
        if (LANG == 0) { ui_2d_text(12 + bw / 2, byy + 22, "挖开模式", 0xFFE8E8F0, 16, VML_ANCHOR_CENTER) }
        else { ui_2d_text(12 + bw / 2, byy + 22, "DIG mode", 0xFFE8E8F0, 16, VML_ANCHOR_CENTER) }
    }
    ui_2d_rect(24 + bw, byy, bw, 44, 0xFF2A3A56, 1, 0, 8)
    if (LANG == 0) { ui_2d_text(24 + bw + bw / 2, byy + 22, "重开", 0xFFE8E8F0, 16, VML_ANCHOR_CENTER) }
    else { ui_2d_text(24 + bw + bw / 2, byy + 22, "Restart", 0xFFE8E8F0, 16, VML_ANCHOR_CENTER) }

    // ⚠ 英文比中文长:这一行按"一个字约 6 像素"估宽,宁可短一点,不然两边被裁

    // ⚠ English is longer than Chinese: budget ~6 px per character here, or both ends get clipped

    if (LANG == 0) { ui_2d_text(SW / 2, byy + 66, "点格子挖开 · F 切模式 · 方向键+回车 · R 重开 · Esc 退出", 0xFF9AA0B0, 11, VML_ANCHOR_CENTER) }
    else { ui_2d_text(SW / 2, byy + 66, "Tap to dig · F flag mode · arrows+Enter · R restart", 0xFF9AA0B0, 11, VML_ANCHOR_CENTER) }

    // 图元数自检:**量的是"这一帧往场景里放了多少条"**(`ui_2d_present` 之前的那个数)

    // Primitive self-check: the count of scene primitives **before `ui_2d_present` flushes them**

    var n = ui_res_get_count(0)
    if (n > gMaxPrims) { gMaxPrims = n }

    ui_2d_present()
}

/// 自动局:按行序找第一个可动的格子(`gAutoWin` 时跳过有雷的,于是必定通关)

/// Auto-play: take the first actionable cell in row-major order (`gAutoWin` skips mined cells, so it

/// always clears the board)

fun autoStep() {
    if (gDead == 1 || gWon == 1) { return }
    var i = 0
    var picked = -1
    while (i < CELLS && picked < 0) {
        if (gOpen[i] == 0 && gFlag[i] == 0) {
            var bad = 0
            if (gAutoWin == 1 && gStarted == 1 && gMine[i] == 1) { bad = 1 }
            // 首击那一下连"雷在哪"都还不知道,所以自动局的第一格直接取 0 号格

            // On the very first tap the mines do not exist yet, so auto-play just takes cell 0

            if (bad == 0) { picked = i }
        }
        i = i + 1
    }
    if (picked >= 0) { reveal(picked) }
}

fun dumpBoard() {
    var i = 0
    while (i < CELLS) {
        if (gOpen[i] == 1) {
            if (gMine[i] == 1) { printf("*") } else { printf("%d", gAdj[i]) }
        } else {
            if (gFlag[i] == 1) { printf("F") } else { printf(".") }
        }
        if (i.c9() == COLS - 1) { printf("\n") }
        i = i + 1
    }
}

fun main() {
    var w = 0
    var h = 0
    var tid = 0

    LANG = ui_scr_get_language()
    w = ui_scr_get_w()
    h = ui_scr_get_h()
    if (w <= 0) { w = 411 }
    if (h <= 0) { h = 726 }
    // 竖屏 + 不要手柄区(画布吃满整屏)

    // Portrait + no gamepad area (the canvas fills the whole screen)

    // ⚠ 上面那两句是**开窗前的估算值**,只作兜底;开窗后 `relayout()` 会重新问一次拿到**实测画布**。

    // ⚠ The two reads above are the **pre-open estimate**, a fallback only; `relayout()` re-reads the **measured canvas** after the open.

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

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

    if (LANG == 0) { ui_win_open_ex("扫雷", 0, 0, VML_WIN_PORTRAIT, VML_WIN_NO_GAMEPAD) } else { ui_win_open_ex("Minesweeper", 0, 0, VML_WIN_PORTRAIT, VML_WIN_NO_GAMEPAD) }
    ui_scr_lock_orientation(VML_LOCK_PORTRAIT)      // VML_LOCK_PORTRAIT

    ui_scr_set_keep_on(1)

    // 两档无头验收(不传参数时是完全正常的对局): `--arg demo`          → 行序乱点,会踩雷(走"输"那条路) `--arg demo --arg W`  → 自动绕开雷,必定通关(走"赢"那条路) 判据只用 `ui_arg_get_count()`(读参数值要传缓冲区,而 `ui_arg_get` 收数组,Kotlin 侧那条约已知会错位)

    // Two headless modes (with no argument the game is completely normal):

    //   `--arg demo`          → blind row-major tapping, hits a mine (the loss path)

    //   `--arg demo --arg W`  → auto-play that avoids mines, always clears the board (the win path)

    // The test only uses `ui_arg_get_count()` (reading argument *values* needs a buffer, and `ui_arg_get` takes an

    // array, which is the known-shifted path on the Kotlin side)

    if (ui_arg_get_count() > 1) {
        gAuto = 1
        gSeed = 20261001
        gTickLimit = 80
        if (ui_arg_get_count() > 2) { gAutoWin = 1 }
    } else {
        gSeed = ui_time_get_ms()
        if (gSeed < 1000) { gSeed = gSeed + 1000 }
    }
    relayout()
    restart()
    draw()

    tid = ui_timer_set(60, 1)
    while (ui_win_is_closed() == 0) {
        var t = ui_msg_fetch_wait(120)
        if (t == 10) { break }                  // MSG_WINDOWCLOSE


        if (t == 9) {                           // MSG_TIMER

            gTick = gTick + 1
            if (gAuto == 1) { autoStep() }
            gSec = elapsedSec()
            draw()
            if (gTickLimit > 0 && (gTick >= gTickLimit || gDead == 1 || gWon == 1)) { break }
            continue
        }

        if (t == 1) {                           // MSG_KEYDOWN

            var k = ui_msg_get_a()
            // ESC 退出

            // ESC quits

            if (k == 27) { break }
            if (gDead == 1 || gWon == 1) {
                if (k == 13 || k == 32 || k == 82) { restart(); ui_audio_beep(660, 90) }
            } else {
                if (k == 37 && gCursor.c9() > 0) { gCursor = gCursor - 1 }
                if (k == 39 && gCursor.c9() < COLS - 1) { gCursor = gCursor + 1 }
                if (k == 38 && gCursor.r9() > 0) { gCursor = gCursor - COLS }
                if (k == 40 && gCursor.r9() < ROWS - 1) { gCursor = gCursor + COLS }
                if (k == 13 || k == 32) { tapCell(gCursor) }
                // 'F' 切模式 —— **和按钮走同一个音效判据**(切模式 900/340、插拔旗 780/520), 一个事件一个音,别让"按 F"和"插旗"听成同一件事

                // 'F' toggles the mode — **the same sound rule as the button** (mode 900/340,

                // flag 780/520), one event one sound, so "press F" and "plant a flag" never sound alike

                if (k == 70) {
                    gFlagMode = 1 - gFlagMode
                    if (gFlagMode == 1) { ui_audio_beep(900, 60) } else { ui_audio_beep(340, 60) }
                }
                if (k == 82) { restart(); ui_audio_beep(660, 90) }      // 'R'

            }
            draw()
            continue
        }

        if (t == 6 || t == 4) {                 // MSG_TOUCHDOWN / MSG_MOUSEDOWN

            var x = ui_msg_get_a()
            var y = ui_msg_get_b()
            var bw2 = (SW - 36) / 2
            var byy2 = BY + ROWS * CELL + 24
            if (gDead == 1 || gWon == 1) {
                // 结束后点**任意处**重开(复盘的时候手上不会正好停在按钮上)

                // After the game, a tap **anywhere** restarts (your finger is not on the button then)

                restart()
                ui_audio_beep(660, 90)
            } else if (y >= byy2 && y <= byy2 + 44 && x >= 12 && x <= 12 + bw2) {
                gFlagMode = 1 - gFlagMode
                if (gFlagMode == 1) { ui_audio_beep(900, 60) } else { ui_audio_beep(340, 60) }
            } else if (y >= byy2 && y <= byy2 + 44 && x >= 24 + bw2 && x <= 24 + bw2 * 2) {
                restart()
                ui_audio_beep(660, 90)
            } else {
                var c = (x - BX) / CELL
                var r = (y - BY) / CELL
                // `in a..b && in a..b` —— 修之前那个 `&&` 会被当成**范围的上界**(判定恒假)

                // the `&&` used to be swallowed into the range bound (making the test always false)

                if (r in 0..(ROWS - 1) && c in 0..(COLS - 1)) {
                    gCursor = r * COLS + c
                    tapCell(gCursor)
                }
            }
            draw()
            continue
        }

        if (t == 11) { relayout(); draw(); continue }   // MSG_WINDOWRESIZE

        if (t == 12) { continue }                       // MSG_WINDOWORIENT

    }

    ui_timer_kill(tid)
    ui_scr_set_keep_on(0)
    ui_win_close()

    // 无头验收用的确定性判据(画面只能肉眼看,这几个数能自动比)

    // Deterministic checkpoints for headless verification (graphics need eyes; these numbers compare)

    dumpBoard()
    printf("mine: mines=%d\n", MINES)
    // 一次只带一个参数:本前端的 `printf2/3/4/5` 有,但**单参**是唯一在例程里被钉住过的形状

    // one argument per call: the frontend does have `printf2/3/4/5`, but the single-argument shape is

    // the only one the examples have ever pinned

    printf("mine: revealed=%d\n", gRevealed)
    printf("mine: flags=%d\n", gFlags)
    printf("mine: dead=%d\n", gDead)
    printf("mine: won=%d\n", gWon)
    printf("mine: sec=%d\n", gSec)
    printf("mine: maxprims=%d\n", gMaxPrims)
    printf("mine: done\n")
}