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

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