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