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

java/app/minesweeper.java · 34620 字节 · 下载原文

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

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

//

// 玩法:9×12 的雷区里埋着 15 颗雷。点开一格:是空地就自动摊开一片(泛洪),

// Gameplay: 15 mines hide in a 9x12 field. Open a cell: an empty one floods the whole area open,

//       是数字就告诉你周围 8 格里有几颗雷,踩到雷就结束;**把所有非雷格都翻开就算赢**。

//       a number tells you how many of the 8 neighbours are mines, stepping on one ends the game,

//       and **revealing every non-mine cell wins**.

// 操作:**轻点 = 翻开**、**按住 0.4 秒 = 插旗/取消旗**;接键盘时方向键移光标、

// Controls: **tap = open**, **hold for 0.4s = flag/unflag**; with a keyboard the arrows move a cursor,

//       空格 = 翻开、F = 插旗、(H = 提示,翻开一格安全的)、回车 = 重开、ESC = 退出。

//       Space = open, F = flag, (H = hint, opens a safe cell), Enter = restart, ESC = quit.

//

// ◆ 两条设计上的硬约束(不是偏好)

// ◆ Two hard design decisions (not preferences)

//

// ① **第一下点哪都安全**:雷是**首次点击之后**才埋的,而且避开点中那格及其八邻域

// ① **The first tap is always safe**: mines are placed **after** the first tap, avoiding that cell and its 8 neighbours

//    —— 所以第一下必然摊开一片,玩家不会一点就死(经典扫雷的规则)。

//    — so the first tap always floods something open; nobody dies on move one (the classic rule).

// ② **泛洪用显式栈、不用递归**:本前端的局部变量帧是固定的(台账那条),递归会把栈打穿;

// ② **The flood fill uses an explicit stack, never recursion**: this frontend's local frame is fixed (see the ledger),

//    而且同一格最多入栈一次(入栈时先标成 3)⇒ 栈深不会超过格子数。

//    and each cell is pushed at most once (marked 3 on push), so the stack can never exceed the cell count.

//

// ◆ 引 UI 库的方式

// ◆ How the UI library is pulled in

//

// 一句 `import vml.ui.*;` 就够,**不用手写 `static native` 声明块** —— 原理与判据写在

// A single `import vml.ui.*;` is enough, **no `static native` block needed** — the mechanism and its evidence are in

// `snake.java` 的文件头(两条 A/B 对照:trace-draw 逐条一致;`#include` 也行但要随包发头文件)。

// the header of `snake.java` (two A/B comparisons: identical trace-draw; `#include` also works but needs a shipped header).

// ⚠ 自己的方法一律带 `ms` 前缀 —— 名字与库函数撞车会被链接器劫持(台账 Java 节那条),

// ⚠ Every method of its own carries the `ms` prefix — a name colliding with a library symbol gets hijacked by the

//   `itoa` 那一族。

//   linker (the ledger's Java section, the `itoa` family).

//

// 跑法:手机 `vml run examples/java/minesweeper.java`;

// How to run: phone `vml run examples/java/minesweeper.java`;

//       桌面 `vmlcli Examples/java/minesweeper.java --screen 395x578 --frames /tmp/fr`

//       desktop `vmlcli Examples/java/minesweeper.java --screen 395x578 --frames /tmp/fr`


import vml.ui.*;

public class Minesweeper {

    // ── 消息类型 / 键码(与 `VmlUiProtocol.cs` 同一套)──

    // ── Message types / key codes (the same set as `VmlUiProtocol.cs`) ──

    static final int MSG_KEYDOWN = 1;
    static final int MSG_TOUCHDOWN = 6;
    static final int MSG_TOUCHMOVE = 7;
    static final int MSG_TOUCHUP = 8;
    static final int MSG_TIMER = 9;
    static final int MSG_CLOSE = 10;
    static final int MSG_RESIZE = 11;
    static final int MSG_ORIENT = 12;

    static final int KEY_ESC = 27;
    static final int KEY_ENTER = 13;
    static final int KEY_SPACE = 32;
    static final int KEY_LEFT = 37;
    static final int KEY_UP = 38;
    static final int KEY_RIGHT = 39;
    static final int KEY_DOWN = 40;
    static final int KEY_F = 70;
    static final int KEY_H = 72;

    // ── 雷区:9 列 × 12 行、15 颗雷(手机上 395×578 实测格边长 39px)──

    // ── The field: 9 columns x 12 rows, 15 mines (measured 39px cells at 395x578 on a phone) ──

    static final int MC = 9;
    static final int MR = 12;
    static final int MINES = 15;

    // ── 数组分区:0..31 标量 / 32..139 雷 / 140..247 计数 / 248..355 状态 / 356..611 泛洪栈 ──

    // ── Layout: 0..31 scalars / 32..139 mine / 140..247 count / 248..355 state / 356..611 flood stack ──

    static final int M0 = 32;
    static final int N0 = 140;
    static final int V0 = 248;
    static final int T0 = 356;

    /* S 的下标表(全是本文件的私有约定)
       The S slot map (every entry is private to this file)
       0 状态(0 玩 1 输 2 赢)   1 已翻开格数   2 雷总数(=胜利阈值用)  3 旗数
       0 state (0 play / 1 lost / 2 won); 1 cells opened; 2 total mines (the win threshold); 3 flags
       4 首次点击发生过没有      5 按下的墙钟时刻ms(-1 = 没按)  6 按下的格(-1 = 无)
       4 has the first tap happened; 5 wall-clock ms when pressed (-1 = not held); 6 the cell pressed (-1 = none)
       7 拍计数器              8 已过秒数      9 光标格        10 上一次翻开摊开了几格
       7 the tick counter; 8 seconds elapsed; 9 the cursor cell; 10 cells opened by the last flood
       11 泛洪次数             12 踩中的格(-1 无)          13 提示用过几次
       11 flood count; 12 the cell stepped on (-1 = none); 13 hints used
       14 棋盘宽(px)           15 棋盘高(px)   16 格边长       17 棋盘左上 x
       14 board width in px; 15 board height in px; 16 cell size; 17 board origin x
       18 棋盘左上 y           19 屏宽         20 屏高         21 界面语言(0 中 1 英)
       18 board origin y; 19 screen width; 20 screen height; 21 UI language (0 Chinese, 1 English)
       22 帧号(每次重画 +1)    23 长按判定阈值(毫秒)  24 胜利后停表标志  25 自检结果
       22 frame counter (+1 per redraw); 23 long-press threshold in ms; 24 clock stopped flag; 25 self-check result
       26 自检静音位           27 秒表墙钟起点ms   28..31 余量
       26 self-check mute flag; 27 stopwatch wall-clock origin in ms; 28..31 slack
       32..139   每格有没有雷(0/1)
       32..139  whether a cell holds a mine (0/1)
       140..247  每格周围雷数(0..8)
       140..247  the mine count around a cell (0..8)
       248..355  每格状态(0 未翻 / 1 已翻 / 2 插旗 / 3 在泛洪栈里)
       248..355  per-cell state (0 hidden / 1 open / 2 flagged / 3 queued on the flood stack)
       356..611  泛洪栈(最多 108 格, 一格只入栈一次)
       356..611  the flood stack (at most 108 cells, each pushed exactly once) */
    // 数组长度 640 = 标量 32 + 三张表 324 + 栈 256 + 余量 28

    // Array length 640 = 32 scalars + 324 for the three tables + 256 stack + 28 slack

    static int[] S = new int[640];

    // 颜色一律写**负数十进制**(`-65536` = 0xFFFF0000)

    // Colors are **negative decimals** (`-65536` = 0xFFFF0000)

    static final int C_BG = -15855594;      // 0xFF0E1016

    static final int C_PANEL = -15328734;   // 0xFF161A22

    static final int C_HID = -13024422;     // 0xFF39435A

    static final int C_HID2 = -11905424;    // 0xFF4A5670

    static final int C_OPEN = -14801872;    // 0xFF1E2430

    static final int C_TEXT = -1512206;     // 0xFFE8ECF2

    static final int C_MUTED = -7695453;    // 0xFF8A93A3

    static final int C_MINE = -1096636;     // 0xFFEF4444

    static final int C_FLAG = -680437;      // 0xFFF59E0B

    static final int C_WIN = -14498466;     // 0xFF22C55E

    static final int C_GOLD = -340971;      // 0xFFFACC15

    static final int C_CURSOR = -10193781;  // 0xFF64748B

    // 数字 1..8 各自的颜色(经典扫雷的配色习惯:1 蓝 2 绿 3 红 …)

    // A colour per digit 1..8 (the classic minesweeper habit: 1 blue, 2 green, 3 red, ...)

    static final int C_N1 = -10443270;      // 0xFF60A5FA

    static final int C_N2 = -11870592;      // 0xFF4ADE80

    static final int C_N3 = -495247;        // 0xFFF87171

    static final int C_N4 = -4160260;       // 0xFFC084FC

    static final int C_N5 = -278748;        // 0xFFFBBF24

    static final int C_N6 = -14494738;      // 0xFF22D3EE

    static final int C_N7 = -1512206;       // 0xFFE8ECF2

    static final int C_N8 = -7035976;       // 0xFF94A3B8


    // ── 小工具 ──

    // ── Tiny helpers ──

    static int msIdx(int x, int y) {
        return y * MC + x;
    }

    static int msMine(int i) {
        return S[M0 + i];
    }

    static void msSetMine(int i, int v) {
        S[M0 + i] = v;
    }

    static int msCnt(int i) {
        return S[N0 + i];
    }

    static int msRev(int i) {
        return S[V0 + i];
    }

    static void msSetRev(int i, int v) {
        S[V0 + i] = v;
    }

    // 八邻域的偏移,用 if 链取(本前端的数组字面量够用,但两个小表写成 if 链更省事)

    // The eight-neighbour offsets, via if chains (array literals work, but two tiny tables are simpler this way)

    static int msDx8(int k) {
        if (k == 0) { return -1; }
        if (k == 1) { return 0; }
        if (k == 2) { return 1; }
        if (k == 3) { return -1; }
        if (k == 4) { return 1; }
        if (k == 5) { return -1; }
        if (k == 6) { return 0; }
        if (k == 7) { return 1; }
        return 0;
    }

    static int msDy8(int k) {
        if (k < 3) { return -1; }
        if (k < 5) { return 0; }
        return 1;
    }

    static int msCol(int i) {
        return i % MC;
    }

    static int msRow(int i) {
        return i / MC;
    }

    static String msDig(int v) {
        if (v == 0) { return "0"; }
        if (v == 1) { return "1"; }
        if (v == 2) { return "2"; }
        if (v == 3) { return "3"; }
        if (v == 4) { return "4"; }
        if (v == 5) { return "5"; }
        if (v == 6) { return "6"; }
        if (v == 7) { return "7"; }
        if (v == 8) { return "8"; }
        if (v == 9) { return "9"; }
        return "?";
    }

    // 整数 → 字符串:**不能写 `"n=" + n`**(int→String 的 `+` 在本前端拼不出数),一位一位拼。

    // int -> String: **`"n=" + n` does not work here**, so digits are joined one at a time.

    static String msNum(int v) {
        if (v == 0) { return "0"; }
        int neg = 0;
        if (v < 0) { neg = 1; v = 0 - v; }
        String s = "";
        while (v > 0) {
            s = msDig(v % 10) + s;
            v = v / 10;
        }
        if (neg == 1) { s = "-" + s; }
        return s;
    }

    static String msL(String zh, String en) {
        if (S[21] == 0) { return zh; }
        return en;
    }

    static void msLog(String tag, int v) {
        System.out.print(tag);
        System.out.println(v);
    }

    // ── 排版:格边长由"屏宽屏高能放多大"推出来,一处写死的画布尺寸都没有 ──

    // ── Layout: the cell size comes from "how big the screen can hold", with no hard-coded canvas ──

    static void msLayout() {
        int hud = 64;
        int foot = 34;
        int bw = S[19] - 16;
        int bh = S[20] - hud - foot;
        int cw = bw / MC;
        int chh = bh / MR;
        int cell = cw;
        if (chh < cell) { cell = chh; }
        if (cell < 12) { cell = 12; }
        S[16] = cell;
        S[14] = cell * MC;
        S[15] = cell * MR;
        S[17] = (S[19] - S[14]) / 2;
        S[18] = hud + (bh - S[15]) / 2;
    }

    // 像素 → 格;出界返回 -1(命中判定与绘制**共用这一份**几何,不会"看着在格上、点下去没反应")

    // Pixel -> cell; -1 when outside (hit testing and drawing **share this one geometry**)

    static int msCellFromPx(int px, int py) {
        if (px < S[17] || py < S[18]) { return -1; }
        int x = (px - S[17]) / S[16];
        int y = (py - S[18]) / S[16];
        if (x < 0 || x >= MC || y < 0 || y >= MR) { return -1; }
        return msIdx(x, y);
    }

    // 数 (x,y) 周围八格的雷

    // Count the mines around (x,y)

    static int msCountAround(int x, int y) {
        int n = 0;
        int k = 0;
        while (k < 8) {
            int nx = x + msDx8(k);
            int ny = y + msDy8(k);
            if (nx >= 0 && nx < MC && ny >= 0 && ny < MR) {
                if (msMine(msIdx(nx, ny)) == 1) { n = n + 1; }
            }
            k = k + 1;
        }
        return n;
    }

    // 布雷:避开**首次点到的那格及其八邻域** ⇒ 第一下必然摊开一片,不可能一点就死

    // Place the mines, avoiding **the first-tapped cell and its 8 neighbours** so the first tap always opens an area

    static void msPlaceMines(int sx, int sy) {
        int placed = 0;
        int guard = 0;
        int i = 0;
        while (placed < MINES && guard < 8000) {
            i = ui_math_rand(MC * MR);
            guard = guard + 1;
            if (msMine(i) == 1) { continue; }
            int x = msCol(i);
            int y = msRow(i);
            int dx = x - sx;
            int dy = y - sy;
            if (dx < 0) { dx = 0 - dx; }
            if (dy < 0) { dy = 0 - dy; }
            if (dx <= 1 && dy <= 1) { continue; }
            msSetMine(i, 1);
            placed = placed + 1;
        }
        i = 0;
        while (i < MC * MR) {
            S[N0 + i] = msCountAround(msCol(i), msRow(i));
            i = i + 1;
        }
    }

    // 泛洪:从 start 摊开。**显式栈**,且同一格只入栈一次(入栈即标 3)⇒ 栈深 ≤ 格子数

    // Flood fill from `start`. An **explicit stack**, and each cell is pushed at most once (marked 3 on push),

    // so the depth can never exceed the cell count

    static void msFlood(int start) {
        int top = 0;
        S[T0] = start;
        top = 1;
        msSetRev(start, 3);
        while (top > 0) {
            top = top - 1;
            int i = S[T0 + top];
            if (msRev(i) != 3) { continue; }
            msSetRev(i, 1);
            S[1] = S[1] + 1;
            S[10] = S[10] + 1;
            if (msCnt(i) == 0) {
                int x = msCol(i);
                int y = msRow(i);
                int k = 0;
                while (k < 8) {
                    int nx = x + msDx8(k);
                    int ny = y + msDy8(k);
                    if (nx >= 0 && nx < MC && ny >= 0 && ny < MR) {
                        int ni = msIdx(nx, ny);
                        if (msRev(ni) == 0 && msMine(ni) == 0) {
                            msSetRev(ni, 3);
                            S[T0 + top] = ni;
                            top = top + 1;
                        }
                    }
                    k = k + 1;
                }
            }
        }
        S[11] = S[11] + 1;
    }

    // `S[26]` = 自检静音位(只有开机自检那一下是 1)—— 自检不该在玩家开局前先放一声"通关号角",

    // `S[26]` is the self-check mute flag (1 only during the boot self-check): a self-check must not blow the

    // 也不该往事件日志里写一条玩家没干过的"赢了"。

    // victory fanfare, nor log a "won" the player never earned.

    static void msCheckWin() {
        if (S[0] == 0 && S[1] >= MC * MR - MINES) {
            S[0] = 2;
            S[24] = 1;
            if (S[26] == 0) {
                ui_audio_beep(1047, 320);
                msLog("ms: win seconds=", S[8]);
            }
        }
    }

    // 翻开一格。返回 1 = 棋盘变了

    // Open a cell. Returns 1 when the board changed

    static int msOpen(int x, int y) {
        if (S[0] != 0) { return 0; }
        if (x < 0 || x >= MC || y < 0 || y >= MR) { return 0; }
        int i = msIdx(x, y);
        if (msRev(i) != 0) { return 0; }
        if (S[4] == 0) {
            msPlaceMines(x, y);
            S[4] = 1;
        }
        S[10] = 0;
        if (msMine(i) == 1) {
            S[0] = 1;
            S[12] = i;
            if (S[26] == 0) {
                ui_audio_beep(131, 320);
                msLog("ms: lose at=", i);
            }
            return 1;
        }
        msFlood(i);
        msCheckWin();
        msLog("ms: open=", S[10]);
        return 1;
    }

    static void msFlag(int i) {
        if (S[0] != 0) { return; }
        if (i < 0) { return; }
        if (msRev(i) == 0) {
            msSetRev(i, 2);
            S[3] = S[3] + 1;
            ui_audio_beep(660, 40);
            return;
        }
        if (msRev(i) == 2) {
            msSetRev(i, 0);
            S[3] = S[3] - 1;
            ui_audio_beep(520, 40);
        }
    }

    /* 提示:随机翻开一格**确定不是雷**的未翻格。它是"卡住了怎么办"的出口,
       Hint: open a random unrevealed cell that is **known not to be a mine**. It is the way out when stuck,
       也是**通关判据的确定性入口** —— 连按总能赢,所以"赢没赢"这件事不依赖运气。
       and it is also the **deterministic entry point for the win verdict** — mashing it always wins, so the
       ⚠ 它走的还是 msOpen 那条真路(泛洪 / 胜负判定都在里面),不是另写一条"直接判赢"。
       outcome stops depending on luck. ⚠ It still goes through the real msOpen path (flood fill and the win
       check included), rather than a separate "just declare a win" shortcut. */
    static void msHint() {
        if (S[0] != 0) { return; }
        int tries = 0;
        while (tries < 600) {
            int i = ui_math_rand(MC * MR);
            if (msRev(i) == 0 && msMine(i) == 0) {
                S[13] = S[13] + 1;
                msOpen(msCol(i), msRow(i));
                return;
            }
            tries = tries + 1;
        }
        int i = 0;
        while (i < MC * MR) {
            if (msRev(i) == 0 && msMine(i) == 0) {
                S[13] = S[13] + 1;
                msOpen(msCol(i), msRow(i));
                return;
            }
            i = i + 1;
        }
    }

    // 开一局:三张表清零、计时归零、光标摆正中。**雷是空的** —— 等第一次点击才埋(见 msPlaceMines)

    // Start a game: clear the three tables, reset the clock, centre the cursor. **No mines yet** — they are placed

    // on the first tap (see msPlaceMines)

    static void msNewGame() {
        int i = 0;
        while (i < MC * MR) {
            S[M0 + i] = 0;
            S[N0 + i] = 0;
            S[V0 + i] = 0;
            i = i + 1;
        }
        S[0] = 0;
        S[1] = 0;
        S[2] = MINES;
        S[3] = 0;
        S[4] = 0;
        S[5] = -1;
        S[6] = -1;
        S[7] = 0;
        S[8] = 0;
        S[27] = ui_time_get_ms();   /* 秒表墙钟起点(场景时钟按毫秒差算秒,用户 2026-10-04 口径) */
        // Stopwatch wall-clock origin (the scene clock turns ms deltas into seconds; user rule, 2026-10-04)

        S[9] = msIdx(MC / 2, MR / 2);
        S[10] = 0;
        S[11] = 0;
        S[12] = -1;
        S[13] = 0;
        S[24] = 0;
        msLog("ms: newgame mines=", MINES);
    }

    // ── 绘制 ──

    // ── Drawing ──

    static int msNumColor(int n) {
        if (n == 1) { return C_N1; }
        if (n == 2) { return C_N2; }
        if (n == 3) { return C_N3; }
        if (n == 4) { return C_N4; }
        if (n == 5) { return C_N5; }
        if (n == 6) { return C_N6; }
        if (n == 7) { return C_N7; }
        return C_N8;
    }

    static void msBanner(String title, String sub, int col) {
        int cy = S[18] + S[15] / 2;
        ui_2d_rect(S[17] - 6, cy - 54, S[14] + 12, 108, C_PANEL, 1, 0, 8);
        ui_2d_line(S[17] - 6, cy - 54, S[17] + S[14] + 6, cy - 54, col, 2);
        ui_2d_line(S[17] - 6, cy + 54, S[17] + S[14] + 6, cy + 54, col, 2);
        ui_2d_text(S[17] + S[14] / 2, cy - 34, title, col, 22, 1);
        ui_2d_text(S[17] + S[14] / 2, cy + 4, sub, C_TEXT, 13, 1);
    }

    static void msDraw() {
        int cell = S[16];
        int i = 0;
        int x = 0;
        int y = 0;
        int px = 0;
        int py = 0;
        int st = 0;
        int n = 0;

        S[22] = S[22] + 1;
        ui_2d_clear(C_BG);

        // HUD:剩余雷数 / 已翻开 / 时间

        // HUD: mines left / cells opened / the clock

        ui_2d_text(12, 10, msL("剩余雷数", "Mines"), C_MUTED, 12, 0);
        ui_2d_text(12, 26, msNum(S[2] - S[3]), C_TEXT, 20, 0);
        ui_2d_text(S[19] / 2 - 28, 10, msL("已翻开", "Opened"), C_MUTED, 12, 0);
        ui_2d_text(S[19] / 2 - 28, 26, msNum(S[1]), C_TEXT, 20, 0);
        ui_2d_text(S[19] / 2 + 46, 10, msL("时间", "Time"), C_MUTED, 12, 0);
        ui_2d_text(S[19] / 2 + 46, 26, msNum(S[8]), C_TEXT, 20, 0);

        // 雷区

        // The field

        ui_2d_rect(S[17] - 3, S[18] - 3, S[14] + 6, S[15] + 6, C_PANEL, 1, 0, 6);

        i = 0;
        while (i < MC * MR) {
            x = msCol(i);
            y = msRow(i);
            px = S[17] + x * cell;
            py = S[18] + y * cell;
            st = msRev(i);
            if (st == 1) {
                ui_2d_rect(px + 1, py + 1, cell - 2, cell - 2, C_OPEN, 1, 0, 2);
                n = msCnt(i);
                if (n > 0) {
                    ui_2d_text(px + cell / 2, py + cell / 2 - cell / 4, msNum(n), msNumColor(n), cell / 2, 1);
                }
            } else {
                ui_2d_rect(px + 1, py + 1, cell - 2, cell - 2, C_HID, 1, 0, 2);
                ui_2d_rect(px + 2, py + 2, cell - 4, 3, C_HID2, 1, 0, 1);
                // 输了就把雷全亮出来(插旗的格保留旗)

                // On a loss every mine is exposed (flagged cells keep their flag)

                if (S[0] == 1 && msMine(i) == 1 && st == 0) {
                    ui_2d_circle(px + cell / 2, py + cell / 2, cell / 5, C_MINE, 1, 0);
                }
                if (st == 2) {
                    // 旗:一根杆 + 一面三角(用两个图元拼,不用图片资源)

                    // A flag: a pole plus a triangle (two primitives, no image assets)

                    ui_2d_line(px + cell / 2, py + cell / 5, px + cell / 2, py + cell * 4 / 5, C_MUTED, 2);
                    ui_2d_rect(px + cell / 2, py + cell / 5, cell / 3, cell / 4, C_FLAG, 1, 0, 2);
                }
            }
            // 键盘光标

            // The keyboard cursor

            if (S[9] == i && S[0] == 0) {
                ui_2d_rect(px, py, cell - 1, cell - 1, C_CURSOR, 0, 2, 3);
            }
            i = i + 1;
        }

        // 踩中的那格额外标一下(否则一片红点里看不出是踩了哪一颗)

        // Mark the cell that was stepped on (otherwise the red dots do not say which one it was)

        if (S[0] == 1 && S[12] >= 0) {
            px = S[17] + msCol(S[12]) * cell;
            py = S[18] + msRow(S[12]) * cell;
            ui_2d_rect(px + 2, py + 2, cell - 4, cell - 4, C_MINE, 1, 0, 3);
        }

        if (S[0] == 1) {
            msBanner(msL("踩雷了!", "Boom!"),
                     msL("点屏幕或回车再来一局", "Tap or press Enter for a new game"), C_MINE);
        }
        if (S[0] == 2) {
            msBanner(msL("全部排干净!", "Cleared!"),
                     msL("点屏幕或回车再来一局", "Tap or press Enter for a new game"), C_WIN);
        }

        // 提示分**两行**写:一行在 320px 宽的窄屏上会被切掉两头(实测)

        // The hints go on **two lines**: a single line gets clipped at both ends on a 320px-wide screen (measured)

        ui_2d_text(S[19] / 2, S[20] - 30,
                msL("轻点翻开 · 按住 0.4 秒插旗",
                    "Tap = open - hold 0.4s = flag"), C_MUTED, 10, 1);
        ui_2d_text(S[19] / 2, S[20] - 16,
                msL("方向键+空格/F · H 提示 · 回车重开",
                    "Arrows+Space/F - H hint - Enter restart"), C_MUTED, 10, 1);

        ui_2d_present();
    }

    /* 自检:扫雷最容易错的是"泛洪摊开"与"胜负判据",而这两件事**都在没有随机性的合成局面上**
       Self-check: the two easiest things to get wrong here are the flood fill and the win verdict, and **both are
       就能判:先把算子(计数 / 泛洪 / 胜负)在合成棋盘上跑一遍,结果存进 S[25]。
       decidable on a synthetic board with no randomness at all**: run the operators (counting / flooding / the win
       ⚠ 它**不是**"直接把状态设成赢" —— 走的是 msFlood + msCheckWin 的真身。
       verdict) once and store the result in S[25]. ⚠ It does **not** just set the state to "won" — it goes through
       the real msFlood and msCheckWin. */
    static int msSelfCheck() {
        int bad = 0;
        int i = 0;
        S[26] = 1;
        while (i < MC * MR) {
            S[M0 + i] = 0;
            S[N0 + i] = 0;
            S[V0 + i] = 0;
            i = i + 1;
        }
        // 合成局面:(0,0) 与 (0,1) 埋雷,其余空地。断言 (1,1) 的周围数是 2 ——

        // Synthetic board: mines at (0,0) and (0,1), everything else empty. Assert (1,1) counts 2 —

        // 这一条同时验"八邻域枚举"与"边界不漏"((0,0) 只有三个邻居)。

        // this one checks the neighbour enumeration *and* the border handling ((0,0) has only three neighbours).

        msSetMine(msIdx(0, 0), 1);
        msSetMine(msIdx(0, 1), 1);
        i = 0;
        while (i < MC * MR) {
            S[N0 + i] = msCountAround(msCol(i), msRow(i));
            i = i + 1;
        }
        // (1,1) 紧挨着两颗雷:(0,0) 与 (0,1)

        // (1,1) touches both mines, at (0,0) and (0,1)

        if (msCnt(msIdx(1, 1)) != 2) { bad = bad + 1; }
        // (1,0) 也在左上角,八邻域里同样是那两颗

        // (1,0) sits in the same corner and also sees both of them

        if (msCnt(msIdx(1, 0)) != 2) { bad = bad + 2; }
        // 盘中央:周围一颗雷都没有

        // the middle of the board: no mine anywhere near

        if (msCnt(msIdx(5, 5)) != 0) { bad = bad + 4; }
        // 右下角:边界收得住,邻居里同样没有雷

        // the bottom-right corner: the border arithmetic holds up and there is no mine nearby

        if (msCnt(msIdx(8, 11)) != 0) { bad = bad + 8; }
        // 泛洪:从 (5,5) 摊开 —— 全盘只有两颗雷、且都在左上角,所以**除那两格外全都会翻开**

        // Flood from (5,5): only two mines exist, up in the top-left, so **every cell but those two gets opened**

        S[1] = 0;
        S[0] = 0;
        S[4] = 1;
        msFlood(msIdx(5, 5));
        if (S[1] != MC * MR - 2) { bad = bad + 16; }
        // 雷格**不许**被泛洪翻开(它是 `msOpen` 那一层的责任,但泛洪自己也不该越界推它)

        // A mine cell must **not** be flooded open (that is msOpen's job, but the flood must not push it either)

        if (msRev(msIdx(0, 0)) != 0) { bad = bad + 32; }
        if (msRev(msIdx(0, 1)) != 0) { bad = bad + 1024; }
        // 胜负:93 格全翻开 ⇒ 判赢

        // The verdict: 93 cells open ⇒ a win

        S[1] = MC * MR - MINES;
        S[24] = 0;
        msCheckWin();
        if (S[0] != 2) { bad = bad + 64; }
        if (S[24] != 1) { bad = bad + 128; }
        // 反方向:少一格就不该判赢

        // The other way: one cell short must not count as a win

        S[0] = 0;
        S[1] = MC * MR - MINES - 1;
        S[24] = 0;
        msCheckWin();
        if (S[0] != 0) { bad = bad + 256; }
        if (S[24] != 0) { bad = bad + 512; }
        S[0] = 0;
        S[24] = 0;
        S[26] = 0;
        return bad;
    }

    public static void main(String[] args) {
        int t = 0;
        int k = 0;
        int tid = 0;
        int now = 0;

        S[21] = ui_scr_get_language();
        // 长按判定:**按墙钟毫秒**,0.4 秒 = 400ms(用户 2026-10-04 口径:时长按毫秒算,不数拍)

        // the long-press threshold, in wall-clock ms: 0.4 s = 400 ms (user's 2026-10-04 rule: durations in ms, not ticks)

        S[23] = 400;
        S[25] = msSelfCheck();

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

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

        ui_win_open_ex(msL("扫雷", "Minesweeper"), 0, 0, 0, 0);
        ui_scr_set_keep_on(1);

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

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

        if (ui_scr_get_w() > 0) { S[19] = ui_scr_get_w(); } else { S[19] = 360; }
        if (ui_scr_get_h() > 0) { S[20] = ui_scr_get_h(); } else { S[20] = 620; }
        msLayout();
        msNewGame();
        if (S[25] != 0) {
            // 自检不过:一声闷响(与"踩雷"的低音区分开)

            // a failed self-check: one low thud (kept clear of the "stepped on a mine" tone)

            ui_audio_beep(330, 300);
        }
        msDraw();
        msLog("ms: selftest=", S[25]);

        tid = ui_timer_set(40, 0);

        while (ui_win_is_closed() == 0) {
            t = ui_msg_fetch_wait(140);

            if (t == MSG_CLOSE) { break; }

            if (t == MSG_RESIZE) {
                if (ui_msg_get_a() > 0) { S[19] = ui_msg_get_a(); }
                if (ui_msg_get_b() > 0) { S[20] = ui_msg_get_b(); }
                msLayout();
                msDraw();
            }

            if (t == MSG_ORIENT) {
                if (ui_scr_get_w() > 0) { S[19] = ui_scr_get_w(); }
                if (ui_scr_get_h() > 0) { S[20] = ui_scr_get_h(); }
                msLayout();
                msDraw();
            }

            if (t == MSG_KEYDOWN) {
                k = ui_msg_get_a();
                if (k == KEY_ESC) { break; }
                if (k == KEY_ENTER) {
                    msNewGame();
                    ui_audio_beep(440, 60);
                    msDraw();
                }
                if (k == KEY_H) {
                    msHint();
                    msDraw();
                }
                if (k == KEY_F) {
                    msFlag(S[9]);
                    msDraw();
                }
                if (k == KEY_SPACE) {
                    if (msOpen(msCol(S[9]), msRow(S[9])) == 1) { msDraw(); }
                }
                if (k == KEY_LEFT && S[9] % MC > 0) { S[9] = S[9] - 1; msDraw(); }
                if (k == KEY_RIGHT && S[9] % MC < MC - 1) { S[9] = S[9] + 1; msDraw(); }
                if (k == KEY_UP && S[9] >= MC) { S[9] = S[9] - MC; msDraw(); }
                if (k == KEY_DOWN && S[9] < MC * (MR - 1)) { S[9] = S[9] + MC; msDraw(); }
            }

            // 触摸:**轻点 = 翻开、按住 ≥0.4 秒 = 插旗**。按下时先记住是哪一格与按下的墙钟时刻,

            // Touch: **tap = open, hold >= 0.4s = flag**. On press the cell and the tick are remembered,

            // 抬手时按**按住了多久**分派 —— 长按不许顺手也翻开(那是最容易写错的一处)。

            // and the release dispatches on **how long it was held** — a long press must not also open the cell.

            if (t == MSG_TOUCHDOWN) {
                now = msCellFromPx(ui_msg_get_a(), ui_msg_get_b());
                if (S[0] != 0) {
                    msNewGame();
                    ui_audio_beep(440, 60);
                    msDraw();
                } else {
                    S[5] = ui_time_get_ms();   // 记住按下的墙钟时刻(长按时长按毫秒算)

                    // Remember the wall-clock time of the press (a long press is measured in ms)

                    S[6] = now;
                    if (now >= 0) { S[9] = now; }
                }
            }
            if (t == MSG_TOUCHUP) {
                now = msCellFromPx(ui_msg_get_a(), ui_msg_get_b());
                if (now >= 0 && S[5] >= 0) {
                    if (ui_time_get_ms() - S[5] >= S[23]) { msFlag(now); } else { msOpen(msCol(now), msRow(now)); }
                    msDraw();
                }
                S[5] = -1;
                S[6] = -1;
            }

            if (t == MSG_TIMER) {
                // 定时器**只负责唤醒**;场景时钟(秒表)**按墙钟毫秒算秒**(用户 2026-10-04 口径:

                // The timer **only wakes us up**; the scene clock (stopwatch) **derives seconds from

                // 「过了几秒」和「走了几步」是两个独立的量)。赢了/输了就停表(`S[0] != 0` 不再更新)。

                // wall-clock ms** (user rule of 2026-10-04: "how many seconds passed" and "how many steps

                // were taken" are two independent quantities). The stopwatch stops on win/lose (no update once S[0] != 0).

                if (S[0] == 0) {
                    now = (ui_time_get_ms() - S[27]) / 1000;
                    if (now != S[8]) { S[8] = now; msDraw(); }
                }
            }
        }

        ui_timer_kill(tid);
        ui_scr_set_keep_on(0);
        ui_win_close();

        // 收尾报告(给脚本化验收用)

        // Closing report (for scripted acceptance)

        msLog("ms: frames=", S[22]);
        msLog("ms: state=", S[0]);
        msLog("ms: opened=", S[1]);
        msLog("ms: flags=", S[3]);
        msLog("ms: seconds=", S[8]);
        msLog("ms: floods=", S[11]);
        msLog("ms: hints=", S[13]);
        msLog("ms: loseCell=", S[12]);
        msLog("ms: selftest=", S[25]);
        System.out.println("ms: done");
    }
}