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