dart/app/minesweeper.dart · 22628 字节 · 下载原文
// 扫雷(Minesweeper)—— Dart 版完整小游戏
// Minesweeper — a complete little game in the Dart edition
//
// 玩法与贪吃蛇刻意分属两个品类(一个实时动作,一个回合制推理),
// The gameplay is deliberately a different genre from the snake game (one is real-time action, the other turn-based deduction),
// 但两者共用同一套 Dart 前端能力清单:
// yet both share the same checklist of Dart frontend capabilities:
//
// · 棋盘是三个并行的 List<int>:地雷 mine / 翻开状态 open / 邻居数 cnt
// · The board is three parallel List<int>: mine for mines, open for reveal state, cnt for neighbour counts
// · 翻开空白格用栈做洪泛:stack.add 压栈、stack.last 取顶、stack.removeAt 弹栈
// · Revealing a blank cell floods with a stack: stack.add to push, stack.last to peek, stack.removeAt to pop
// · 剩余雷数与计时用 padLeft 补零对齐,标题用 toUpperCase 大写
// · The remaining-mine counter and the clock are zero-padded with padLeft, and the title is upper-cased with toUpperCase
// · 每走一步把格子序号记进 List<int>,退出时用 join 打出来,无头测试可以直接比
// · Every move appends the cell index to a List<int>, joined at exit so headless tests can compare directly
// · 颜色一律十六进制 0xAARRGGBB,分数与文字靠字符串插值拼
// · Colors are hexadecimal 0xAARRGGBB and scores and labels are built with string interpolation
//
// 操作(手机手柄与物理键盘共用同一套虚拟键码):
// Controls (the phone gamepad and a physical keyboard share the same virtual key codes):
// · 方向键 / 十字键:移动光标
// · Arrow keys / d-pad: move the cursor
// · A / 空格 / 回车:翻开光标处的格子
// · A / Space / Enter: dig the cell under the cursor
// · B / X:插旗或取消旗子
// · B / X: place or remove a flag
// · ESC:退出;结束画面上 A / 空格 / 回车 重开一局
// · ESC: quit; on the finished screen A / Space / Enter starts a new round
//
// 音效按开发指南 4.5 节用单音 ui_audio_beep:翻开 520Hz、插旗 880Hz、踩雷 131Hz、通关 1047Hz
// Sound follows section 4.5 of the guide and uses single-tone ui_audio_beep: 520Hz on reveal, 880Hz on flag, 131Hz on a mine, 1047Hz on a win
//
// 跑法:手机 vml run examples/dart/minesweeper.dart
// How to run: on the phone vml run examples/dart/minesweeper.dart
// 桌面 vmlcli Examples/dart/minesweeper.dart --frames /tmp/fr --input /tmp/in.txt
// on the desktop vmlcli Examples/dart/minesweeper.dart --frames /tmp/fr --input /tmp/in.txt
#include "../../../Lib/dart/vml_ui.dart"
import 'vml_ui.dart';
void main() {
// ── 全部状态都放 main 的局部变量(Dart 顶层可变状态读回来是脏的,见 demo_ui.dart 文件头 ①)──
// ── All state lives in locals of main (top-level mutable state reads back dirty in Dart, see demo_ui.dart header ①) ──
int sw = 0;
int sh = 0;
int i = 0;
int j = 0;
int r = 0;
int c = 0;
int lang = 0;
int state = 0;
int done = 0;
int score = 0;
int best = 0;
int cols = 8;
int rows = 8;
int mines = 8;
int tile = 34;
int bx = 0;
int by = 52;
int cx = 0;
int cur = 0;
int guard = 0;
int revealed = 0;
int flags = 0;
int moves = 0;
int won = 0;
int t0 = 0;
int elapsed = 0;
int tid = 0;
int key = 0;
int t = 0;
int cell = 0;
int cr = 0;
int cc = 0;
int nr = 0;
int nc = 0;
int dr = 0;
int dc = 0;
int ncell = 0;
int n = 0;
String msgA = "";
String msgB = "";
String num = "";
String rowText = "";
String title = "minesweeper";
List<int> mine = [];
List<int> open = [];
List<int> cnt = [];
List<int> stack = [];
List<int> log = [];
// 调色板:全部十六进制真彩,Alpha 在最前
// The palette: all hexadecimal true color, with alpha first
int cBg = 0xFF0E1116;
int cHidden = 0xFF3B4759;
int cHiddenTop = 0xFF54637A;
int cOpen = 0xFF1B2430;
int cMine = 0xFFF87171;
int cFlag = 0xFFFB923C;
int cText = 0xFFE5E7EB;
int cDim = 0xFF94A3B8;
int cGold = 0xFFFBBF24;
int cPanel = 0xFF1B2530;
int cWin = 0xFF4ADE80;
int cLose = 0xFFF87171;
int cN1 = 0xFF60A5FA;
int cN2 = 0xFF4ADE80;
int cN3 = 0xFFF87171;
int cN4 = 0xFFA78BFA;
int cN5 = 0xFFFB923C;
int cN6 = 0xFF22D3EE;
int cN7 = 0xFFE5E7EB;
int cN8 = 0xFF94A3B8;
lang = ui_scr_get_language();
title = "minesweeper.dart";
// 开窗时传 0:尺寸交给宿主(用真实可用区)。**开窗之后**再问一次真实画布 —— 开窗前问到的只是估算值。
// Open with 0: the host decides the size (the real usable area). Ask the **real** canvas only after the window is open -- before that the host can only estimate.
ui_win_open_ex(title, 0, 0, 1, 1);
sw = ui_scr_get_w();
sh = ui_scr_get_h();
if (sw <= 0) { sw = 360; }
if (sh <= 0) { sh = 620; }
cx = sw ~/ 2;
// 格子数按屏幕定:竖屏约 11x14,转屏后 tile 自动缩小
// The grid is sized from the screen: about 11x14 in portrait, with tile shrinking after rotation
cols = (sw - 12) ~/ 34;
rows = (sh - 88) ~/ 34;
if (cols < 4) { cols = 4; }
if (rows < 4) { rows = 4; }
if (cols > 30) { cols = 30; }
if (rows > 30) { rows = 30; }
mines = cols * rows ~/ 7;
if (mines < 2) { mines = 2; }
// 布雷:三个并行 List 一次建好,邻居数全表预计算(洪泛与显示共用同一份)
// Lay the mines: the three parallel lists are built once and every neighbour count is precomputed (the flood and the display share one copy)
mine.clear();
open.clear();
cnt.clear();
log.clear();
i = 0;
while (i < cols * rows) {
mine.add(0);
open.add(0);
cnt.add(0);
i = i + 1;
}
guard = 0;
i = 0;
while (i < mines && guard < 8000) {
j = ui_math_rand(cols * rows);
// 首击必安全:光标开局在 0 号格,它和它的邻居一格都不放雷
// The first dig is always safe: the cursor starts on cell 0, so neither it nor its neighbours holds a mine
if (j != 0 && j != 1 && j != cols && j != cols + 1) {
if (mine[j] == 0) {
mine[j] = 1;
i = i + 1;
}
}
guard = guard + 1;
}
r = 0;
while (r < rows) {
c = 0;
while (c < cols) {
n = 0;
dr = -1;
while (dr <= 1) {
dc = -1;
while (dc <= 1) {
if (dr != 0 || dc != 0) {
nr = r + dr;
nc = c + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
if (mine[nr * cols + nc] == 1) { n = n + 1; }
}
}
dc = dc + 1;
}
dr = dr + 1;
}
cnt[r * cols + c] = n;
c = c + 1;
}
r = r + 1;
}
cur = 0;
state = 0;
revealed = 0;
flags = 0;
moves = 0;
won = 0;
score = 0;
t0 = ui_time_get_ms();
tid = ui_timer_set(500, 1);
while (done == 0 && ui_win_is_closed() == 0) {
// ── 版面:每帧重算,转屏与手柄收放都能跟上 ──
// ── Layout: recomputed every frame so rotation and gamepad changes are tracked ──
sw = ui_scr_get_w();
sh = ui_scr_get_h();
if (sw <= 0) { sw = 360; }
if (sh <= 0) { sh = 620; }
cx = sw ~/ 2;
tile = (sw - 12) ~/ cols;
j = (sh - 88) ~/ rows;
if (j < tile) { tile = j; }
if (tile < 8) { tile = 8; }
bx = (sw - cols * tile) ~/ 2;
by = 52;
elapsed = (ui_time_get_ms() - t0) ~/ 1000;
ui_2d_clear(cBg);
// 顶部:标题 + 雷数 / 旗数 / 时间(padLeft 补零,数字跳动时不会左右晃)
// Top: title plus mines / flags / clock (zero-padded with padLeft so digits do not jitter)
msgA = title.toUpperCase();
ui_2d_text_styled(10, 14, msgA, cText, 15, 0, 1);
num = "$mines";
int left = mines - flags;
msgB = "$left";
msgA = "雷 " + num.padLeft(2, "0") + " 剩余 " + msgB.padLeft(2, "0");
String stime = "$elapsed";
msgA = msgA + " 时间 " + stime.padLeft(4, " ") + " 秒 分 " + "$score".padLeft(4, " ");
ui_2d_text(10, 36, msgA, cDim, 12, 0);
i = 0;
while (i < cols * rows) {
int x = bx + i % cols * tile;
int y = by + i ~/ cols * tile;
if (open[i] == 1) {
// 已翻开:暗底,数字按经典配色
// Revealed: a dark base with classic per-number colors
ui_2d_rect(x + 1, y + 1, tile - 2, tile - 2, cOpen, 1, 0, 3);
if (mine[i] == 1) {
ui_2d_circle(x + tile ~/ 2, y + tile ~/ 2, tile ~/ 5, cMine, 1, 0);
ui_2d_circle(x + tile ~/ 2, y + tile ~/ 2, tile ~/ 5 + 2, cMine, 0, 2);
} else if (cnt[i] > 0) {
int col = cN1;
if (cnt[i] == 2) { col = cN2; }
if (cnt[i] == 3) { col = cN3; }
if (cnt[i] == 4) { col = cN4; }
if (cnt[i] == 5) { col = cN5; }
if (cnt[i] == 6) { col = cN6; }
if (cnt[i] == 7) { col = cN7; }
if (cnt[i] >= 8) { col = cN8; }
int cv = cnt[i];
String d = "$cv";
ui_2d_text(x + tile ~/ 2, y + tile ~/ 2, d, col, tile ~/ 2, 1);
}
} else {
// 未翻开:亮底 + 左上高光,插旗的再画一面小旗
// Hidden: a light base with a top-left highlight, and a small flag when flagged
ui_2d_rect(x + 1, y + 1, tile - 2, tile - 2, cHidden, 1, 0, 3);
ui_2d_line(x + 3, y + 3, x + tile - 4, y + 3, cHiddenTop, 2);
ui_2d_line(x + 3, y + 3, x + 3, y + tile - 4, cHiddenTop, 2);
if (open[i] == 2) {
int fx = x + tile ~/ 2;
int fy = y + tile ~/ 2;
ui_2d_line(fx, fy - tile ~/ 4, fx, fy + tile ~/ 4, cFlag, 2);
ui_2d_rect(fx + 1, fy - tile ~/ 4, tile ~/ 4, tile ~/ 6, cFlag, 1, 0, 1);
}
}
i = i + 1;
}
// 光标:金色圆角描边
// The cursor: a golden rounded outline
cr = cur ~/ cols;
cc = cur % cols;
ui_2d_rect(bx + cc * tile + 1, by + cr * tile + 1, tile - 2, tile - 2, cGold, 0, 3, 4);
if (lang == 0) { msgB = "方向键移动 / A 翻开 / B 插旗 / ESC 退出"; } else { msgB = "Arrows move / A dig / B flag / ESC quit"; }
ui_2d_text(cx, sh - 20, msgB, cDim, 12, 1);
if (state == 1 || state == 2) {
// 结束面板:赢了绿框、踩雷红框
// The finished panel: a green frame on a win, a red frame on a mine
int fcol = cWin;
if (state == 2) { fcol = cLose; }
ui_2d_rect(bx, by + rows * tile ~/ 2 - 58, cols * tile, 116, cPanel, 1, 0, 12);
if (state == 1) {
if (lang == 0) { msgA = "通关"; } else { msgA = "CLEARED"; }
} else {
if (lang == 0) { msgA = "踩雷了"; } else { msgA = "BOOM"; }
}
ui_2d_text(cx, by + rows * tile ~/ 2 - 36, msgA, fcol, 20, 1);
String rnum = "$revealed";
String tnum = "$mines";
if (lang == 0) { msgA = "翻开 " + rnum + " 格 / 分数 " + "$score"; } else { msgA = "opened " + rnum + " / score " + "$score"; }
ui_2d_text(cx, by + rows * tile ~/ 2 - 6, msgA, cGold, 14, 1);
if (lang == 0) { msgA = "用时 " + "$elapsed" + " 秒 步数 " + "$moves"; } else { msgA = "time " + "$elapsed" + "s moves " + "$moves"; }
ui_2d_text(cx, by + rows * tile ~/ 2 + 16, msgA, cText, 12, 1);
if (lang == 0) { msgA = "A / 空格 / 回车 重开"; } else { msgA = "A / Space / Enter restart"; }
ui_2d_text(cx, by + rows * tile ~/ 2 + 40, msgA, cDim, 12, 1);
}
ui_2d_present();
t = ui_msg_fetch_wait(300);
if (t == 0) { continue; }
if (t == 9) { continue; }
if (t == 1) {
key = ui_msg_get_a();
if (key == 27) { done = 1; continue; }
if (state == 1 || state == 2) {
if (key == 32 || key == 13 || key == 65) {
// 重开:保留最高分,重新布雷
// Restart: keep the best score and lay fresh mines
mine.clear();
open.clear();
cnt.clear();
log.clear();
i = 0;
while (i < cols * rows) {
mine.add(0);
open.add(0);
cnt.add(0);
i = i + 1;
}
guard = 0;
i = 0;
while (i < mines && guard < 8000) {
j = ui_math_rand(cols * rows);
if (j != 0 && j != 1 && j != cols && j != cols + 1) {
if (mine[j] == 0) {
mine[j] = 1;
i = i + 1;
}
}
guard = guard + 1;
}
r = 0;
while (r < rows) {
c = 0;
while (c < cols) {
n = 0;
dr = -1;
while (dr <= 1) {
dc = -1;
while (dc <= 1) {
if (dr != 0 || dc != 0) {
nr = r + dr;
nc = c + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
if (mine[nr * cols + nc] == 1) { n = n + 1; }
}
}
dc = dc + 1;
}
dr = dr + 1;
}
cnt[r * cols + c] = n;
c = c + 1;
}
r = r + 1;
}
cur = 0;
state = 0;
revealed = 0;
flags = 0;
moves = 0;
won = 0;
score = 0;
t0 = ui_time_get_ms();
ui_audio_beep(523, 80);
}
continue;
}
// 方向键移动光标
// The arrow keys move the cursor
cr = cur ~/ cols;
cc = cur % cols;
if (key == 38) { if (cr > 0) { cur = cur - cols; } }
if (key == 40) { if (cr < rows - 1) { cur = cur + cols; } }
if (key == 37) { if (cc > 0) { cur = cur - 1; } }
if (key == 39) { if (cc < cols - 1) { cur = cur + 1; } }
if (key == 66 || key == 88 || key == 70) {
// 插旗 / 取消旗子
// Place or remove a flag
if (open[cur] == 0) {
open[cur] = 2;
flags = flags + 1;
moves = moves + 1;
log.add(cur);
ui_audio_beep(880, 30);
} else if (open[cur] == 2) {
open[cur] = 0;
flags = flags - 1;
moves = moves + 1;
log.add(cur);
ui_audio_beep(440, 30);
}
}
if (key == 32 || key == 13 || key == 65) {
if (open[cur] == 0) {
moves = moves + 1;
log.add(cur);
if (mine[cur] == 1) {
// 踩雷:所有雷显示出来,本局结束
// A mine: uncover every mine and end the round
open[cur] = 1;
revealed = revealed + 1;
i = 0;
while (i < cols * rows) {
if (mine[i] == 1) { open[i] = 1; }
i = i + 1;
}
state = 2;
ui_audio_beep(131, 400);
if (score > best) { best = score; }
} else {
// 翻开 + 空白格洪泛(栈:add 压、last 看、removeAt 弹)
// Reveal and flood the blank cells (a stack: add to push, last to peek, removeAt to pop)
open[cur] = 1;
revealed = revealed + 1;
if (cnt[cur] == 0) {
stack.clear();
stack.add(cur);
while (stack.length > 0) {
cell = stack.last;
stack.removeAt(stack.length - 1);
cr = cell ~/ cols;
cc = cell % cols;
dr = -1;
while (dr <= 1) {
dc = -1;
while (dc <= 1) {
if (dr != 0 || dc != 0) {
nr = cr + dr;
nc = cc + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
ncell = nr * cols + nc;
if (open[ncell] == 0) {
open[ncell] = 1;
revealed = revealed + 1;
if (cnt[ncell] == 0) { stack.add(ncell); }
}
}
}
dc = dc + 1;
}
dr = dr + 1;
}
}
}
score = revealed * 10;
ui_audio_beep(520, 25);
if (revealed == cols * rows - mines) {
state = 1;
won = 1;
score = score + 500 + (mines - flags) * 20;
if (score > best) { best = score; }
ui_audio_beep(1047, 300);
}
}
}
}
continue;
}
if (t == 10) { break; }
}
ui_timer_kill(tid);
ui_win_close();
// ── 收尾:一组确定的数值判据,供无头验证直接比对(图形只能肉眼看,这些数能自动比)──
// ── Wrap-up: deterministic numeric checkpoints for headless verification (graphics need eyes, these numbers do not) ──
print_str("mine: cols="); print_int(cols);
print_str("x"); println_int(rows);
print_str("mine: mines="); println_int(mines);
print_str("mine: revealed="); println_int(revealed);
print_str("mine: flags="); println_int(flags);
print_str("mine: moves="); println_int(moves);
print_str("mine: score="); println_int(score);
print_str("mine: best="); println_int(best);
print_str("mine: state="); println_int(state);
print_str("mine: won="); println_int(won);
print_str("mine: elapsed="); println_int(elapsed);
// 第一行雷图(0/1 拼成一串)—— 无头测试可以拿它与逐帧图对照,洪泛范围也能由它复算
// The first mine row as a 0/1 string: headless tests can check it against the frames and recompute the flood from it
rowText = mine.sublist(0, cols).join("");
print_str("mine: row0="); println_str(rowText);
print_str("mine: row0_len="); println_int(rowText.length);
print_str("mine: log="); println_str(log.join(","));
println_str("mine: done");
// 库函数自检:把这门语言刚修好的那批各用一遍,每一条都打印可断言的数字
// Library self-check: exercise every freshly fixed feature and print an assertable number for each
String A = " Hello,World ";
String B = A.trim();
print_str("chk.str_length="); println_int(B.length);
print_str("chk.str_contains="); println_int(B.contains("World") == 1 ? 1 : 0);
print_str("chk.str_indexOf="); println_int(B.indexOf("World"));
print_str("chk.str_lastIndexOf="); println_int(B.lastIndexOf("o"));
print_str("chk.str_substring="); println_str(B.substring(0, 5));
print_str("chk.str_upper="); println_str(B.toUpperCase());
print_str("chk.str_lower="); println_str(B.toLowerCase());
print_str("chk.str_startsWith="); println_int(B.startsWith("Hell") == 1 ? 1 : 0);
print_str("chk.str_endsWith="); println_int(B.endsWith("World") == 1 ? 1 : 0);
print_str("chk.str_trimLeft_len="); println_int(A.trimLeft().length);
print_str("chk.str_trimRight_len="); println_int(A.trimRight().length);
print_str("chk.str_replaceAll="); println_str(B.replaceAll("l", "L"));
print_str("chk.str_replaceFirst="); println_str(B.replaceFirst("l", "L"));
print_str("chk.str_padLeft="); println_str("7".padLeft(4, "0"));
print_str("chk.str_padRight="); println_str("7".padRight(4, "."));
print_str("chk.str_split_len="); println_int("a,b,c".split(",").length);
print_str("chk.str_codeUnitAt="); println_int(B.codeUnitAt(0));
print_str("chk.str_isEmpty="); println_int("".isEmpty ? 1 : 0);
print_str("chk.str_isNotEmpty="); println_int(B.isNotEmpty ? 1 : 0);
print_str("chk.str_compareTo="); println_int("abc".compareTo("abd"));
// 列表:增删查排各一遍
// Lists: one pass each over add, remove, query and sort
List<int> L = [5, 3, 9, 3];
print_str("chk.list_length="); println_int(L.length);
print_str("chk.list_first="); println_int(L.first);
print_str("chk.list_last="); println_int(L.last);
print_str("chk.list_contains="); println_int(L.contains(9) == 1 ? 1 : 0);
print_str("chk.list_indexOf="); println_int(L.indexOf(9));
print_str("chk.list_lastIndexOf="); println_int(L.lastIndexOf(3));
L.add(7);
print_str("chk.list_add_len="); println_int(L.length);
L.insert(0, 42);
print_str("chk.list_insert_head="); println_int(L[0]);
L.addAll([100, 101]);
print_str("chk.list_addAll_len="); println_int(L.length);
L.removeAt(0);
print_str("chk.list_removeAt_len="); println_int(L.length);
print_str("chk.list_remove="); println_int(L.remove(100) == 1 ? 1 : 0);
L.removeLast();
print_str("chk.list_removeLast_len="); println_int(L.length);
List<int> M = [4, 1, 3, 2];
M.sort();
print_str("chk.list_sort="); println_str(M.join(""));
M.reverse();
print_str("chk.list_reverse="); println_str(M.join(""));
print_str("chk.list_sublist="); println_str(M.sublist(1, 3).join(""));
String mj = M.join("-");
print_str("chk.list_join="); println_str(mj);
print_str("chk.list_join_len="); println_int(mj.length);
print_str("chk.list_isNotEmpty="); println_int(L.isNotEmpty ? 1 : 0);
L.clear();
print_str("chk.list_clear_len="); println_int(L.length);
print_str("chk.list_isEmpty="); println_int(L.isEmpty ? 1 : 0);
// 十六进制字面量与字符串插值
// Hexadecimal literals and string interpolation
print_str("chk.hex_ff102030="); println_int(0xFF102030);
print_str("chk.hex_00ff="); println_int(0x000000FF);
int nn = 12345;
int mm = -7;
String s1 = "n=$nn";
String s2 = "${nn}";
String s3 = "m=$mm";
String s4 = "a" + "$nn" + "b";
print_str("chk.interp_plain="); println_str(s1);
print_str("chk.interp_plain_len="); println_int(s1.length);
print_str("chk.interp_brace_len="); println_int(s2.length);
print_str("chk.interp_neg="); println_str(s3);
print_str("chk.interp_neg_len="); println_int(s3.length);
print_str("chk.interp_concat="); println_str(s4);
print_str("chk.interp_concat_len="); println_int(s4.length);
println_str("mine: selftest done");
}