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

dart/app/snake.dart · 20437 字节 · 下载原文

// 贪吃蛇(Snake)—— Dart 版完整小游戏

// Snake — a complete little game in the Dart edition

//

// 这一份既是能玩的游戏,也是 Dart 前端能力的活体清单:

// This file is both a playable game and a living checklist of what the Dart frontend can do:

//

//   · 画布尺寸每帧按 ui_scr_get_w / ui_scr_get_h 现算,转屏后自动重排(不写死坐标)

//   · The canvas is laid out every frame from ui_scr_get_w / ui_scr_get_h and reflows after rotation (no hardcoded coordinates)

//   · 颜色一律写十六进制 0xAARRGGBB(真彩),不退回负数十进制

//   · Colors are always written as hexadecimal 0xAARRGGBB (true color), never negative decimals

//   · 分数 / 长度用字符串插值拼出来,再用 padLeft 对齐成固定宽度

//   · Score and length are built with string interpolation and aligned to a fixed width with padLeft

//   · 蛇身是一个 List<int>:insert 进新头、removeAt 掉尾巴、contains 判自撞、indexOf 判食物

//   · The body is a List<int>: insert the new head, removeAt the tail, contains for self-collision, indexOf for food

//   · 每局结束把成绩压进 List<int>,用 sublist 取副本 + sort/reverse 排成本场前三

//   · Each finished round is pushed into a List<int>, and sublist + sort/reverse rank the session best three

//

// 操作(手机手柄与物理键盘共用同一套虚拟键码):

// Controls (the phone gamepad and a physical keyboard share the same virtual key codes):

//   · 方向键 / 十字键:转向,不会 180 度掉头

//   · Arrow keys / d-pad: steer, with no 180-degree reversal

//   · A / 空格 / 回车:暂停与继续

//   · A / Space / Enter: pause and resume

//   · B / ESC:退出

//   · B / ESC: quit

//   · 撞墙或撞到自己就结束;结束画面上按 A / 空格 / 回车重开一局

//   · Hitting a wall or yourself ends the round; A / Space / Enter on the game-over screen starts a new one

//

// 一条有意为之的开局设计:每局第一个食物固定放在蛇头那一行的尽头,

// One deliberate opening rule: the first food of a round is fixed at the far end of the head row,

// 所以开局第一口必定吃到(玩家一进来就有明确目标,也让无头回归测试有一个确定的分数判据)。

// so the opening bite always lands (the player gets a goal at once, and headless tests get a deterministic score checkpoint).

//

// 音效按开发指南 4.5 节用单音 ui_audio_beep:吃到 880Hz、撞死 131Hz、重开 523Hz(一个事件一个音)

// Sound follows section 4.5 of the guide and uses single-tone ui_audio_beep: 880Hz on eat, 131Hz on death, 523Hz on restart

//

// 跑法:手机 vml run examples/dart/snake.dart

// How to run: on the phone vml run examples/dart/snake.dart

//       桌面 vmlcli Examples/dart/snake.dart --frames /tmp/fr --input /tmp/in.txt

//       on the desktop vmlcli Examples/dart/snake.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 lang = 0;
  int state = 0;
  int done = 0;
  int score = 0;
  int best = 0;
  int eaten = 0;
  int dir = 1;
  int want = 1;
  int food = 0;
  int speed = 140;
  int steps = 0;
  int keys = 0;
  int played = 0;
  int tid = 0;
  int cols = 8;
  int rows = 8;
  int tile = 26;
  int bx = 0;
  int by = 54;
  int cx = 0;
  int guard = 0;
  int len = 0;
  int newhead = 0;
  int crash = 0;
  int grow = 0;
  int tail = 0;
  int hr = 0;
  int hc = 0;
  int key = 0;
  int t = 0;
  int acc = 0;      // 运动墙钟欠账(毫秒)—— 按墙钟补步,用户 2026-10-04 口径

                    // motion wall-clock debt in ms -- steps are caught up from the wall clock (user's 2026-10-04 rule)

  int lastMs = 0;   // 上次补步取的墙钟时刻(进主循环时初始化)

                    // wall-clock time of the last catch-up step (initialized when entering the main loop)

  int nowMs = 0;    // 墙钟采样

                    // wall-clock sample

  String hud = "";
  String num = "";
  String rank = "";
  String msgA = "";
  String msgB = "";
  String col2 = "";

  List<int> body = [];
  List<int> rounds = [];
  List<int> top3 = [];

  // 调色板:全部十六进制真彩,Alpha 在最前

  // The palette: all hexadecimal true color, with alpha first

  int cBg = 0xFF0E1116;
  int cBoard = 0xFF141A22;
  int cGrid = 0xFF1E2733;
  int cSnake = 0xFF4ADE80;
  int cHead = 0xFF86EFAC;
  int cFood = 0xFFF87171;
  int cText = 0xFFE5E7EB;
  int cDim = 0xFF94A3B8;
  int cGold = 0xFFFBBF24;
  int cPanel = 0xFF1B2530;
  int cPause = 0xFF60A5FA;

  lang = ui_scr_get_language();
  // 开窗时传 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("snake.dart", 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;

  // 格子数按屏幕定:竖屏约 14x18,转横屏时 tile 自动缩小,坐标不写死

  // The grid is sized from the screen: roughly 14x18 in portrait, with tile shrinking in landscape and no hardcoded coordinates

  cols = (sw - 12) ~/ 26;
  rows = (sh - 88) ~/ 26;
  if (cols < 8) { cols = 8; }
  if (rows < 8) { rows = 8; }
  if (cols > 40) { cols = 40; }
  if (rows > 48) { rows = 48; }

  // 开第一局:蛇贴着顶行从左边出发,头在右、尾在左

  // Start the first round: the snake sets off along the top row, head at the right and tail at the left

  body.clear();
  body.add(2);
  body.add(1);
  body.add(0);
  dir = 1;
  want = 1;
  score = 0;
  eaten = 0;
  state = 0;
  speed = 140;
  steps = 0;
  food = cols - 1;
  tid = ui_timer_set(speed, 1);
  acc = 0;
  lastMs = ui_time_get_ms();   // 墙钟起点:进主循环时取(不在模块级调 syscall)

                               // wall-clock origin: sampled when entering the main loop (never call syscalls at module level)


  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 < 6) { tile = 6; }
    bx = (sw - cols * tile) ~/ 2;
    by = 54;

    ui_2d_clear(cBg);

    // 顶部两行:标题 + 分数(分数 padLeft 成 6 格宽,跳动时不会左右晃)

    // Top two rows: title plus score (the score is padded to six columns so it does not jitter)

    if (lang == 0) { msgA = "贪吃蛇"; } else { msgA = "SNAKE"; }
    ui_2d_text_styled(10, 14, msgA, cHead, 17, 0, 1);
    num = "$score";
    hud = num.padLeft(6, " ");
    len = body.length;
    col2 = "$len";
    String nbest = "$best";
    if (lang == 0) {
      msgA = "得分" + hud + "   长度 " + col2.padLeft(3, " ") + "   最高 " + nbest.padLeft(6, " ");
    } else {
      msgA = "SCORE" + hud + "   LEN " + col2.padLeft(3, " ") + "   BEST " + nbest.padLeft(6, " ");
    }
    ui_2d_text(10, 36, msgA, cText, 12, 0);

    // 棋盘底与网格

    // The board plate and its grid

    ui_2d_rect(bx - 2, by - 2, cols * tile + 4, rows * tile + 4, cBoard, 1, 0, 8);
    i = 0;
    while (i <= cols) {
      ui_2d_line(bx + i * tile, by, bx + i * tile, by + rows * tile, cGrid, 1);
      i = i + 1;
    }
    j = 0;
    while (j <= rows) {
      ui_2d_line(bx, by + j * tile, bx + cols * tile, by + j * tile, cGrid, 1);
      j = j + 1;
    }

    // 食物:红圆

    // The food: a red circle

    int fr = food ~/ cols;
    int fc = food % cols;
    ui_2d_circle(bx + fc * tile + tile ~/ 2, by + fr * tile + tile ~/ 2, tile ~/ 2 - 3, cFood, 1, 0);

    // 蛇身从尾画到头,头最后画所以压在最上面

    // The body is drawn from tail to head, so the head ends up on top

    i = body.length - 1;
    while (i >= 0) {
      int cr = body[i] ~/ cols;
      int cc = body[i] % cols;
      int col = cSnake;
      if (i == 0) { col = cHead; }
      ui_2d_rect(bx + cc * tile + 2, by + cr * tile + 2, tile - 4, tile - 4, col, 1, 0, 5);
      i = i - 1;
    }

    // 底部提示行

    // The hint line at the bottom

    if (lang == 0) { msgB = "方向键转向 / A 暂停 / B 退出"; } else { msgB = "Arrows steer / A pause / B quit"; }
    ui_2d_text(cx, sh - 20, msgB, cDim, 12, 1);

    if (state == 2) {
      ui_2d_rect(bx, by + rows * tile ~/ 2 - 26, cols * tile, 52, cPanel, 1, 0, 10);
      if (lang == 0) { msgA = "已暂停"; } else { msgA = "PAUSED"; }
      ui_2d_text(cx, by + rows * tile ~/ 2, msgA, cPause, 20, 1);
    }

    if (state == 1) {
      // 结束面板:本场前三名用 sublist 取副本、sort 升序、reverse 降序

      // The game-over panel: the session best three via sublist copy, sort ascending, reverse descending

      top3 = rounds.sublist(0, rounds.length);
      top3.sort();
      top3.reverse();
      if (top3.length > 3) { top3 = top3.sublist(0, 3); }
      rank = top3.join(" / ");
      ui_2d_rect(bx, by + rows * tile ~/ 2 - 58, cols * tile, 116, cPanel, 1, 0, 12);
      if (lang == 0) { msgA = "撞上了"; } else { msgA = "GAME OVER"; }
      ui_2d_text(cx, by + rows * tile ~/ 2 - 34, msgA, cFood, 20, 1);
      String numA = "$score";
      if (lang == 0) { msgA = "本局得分 " + numA; } else { msgA = "score " + numA; }
      ui_2d_text(cx, by + rows * tile ~/ 2 - 4, msgA, cGold, 16, 1);
      if (lang == 0) { msgA = "本场前三 " + rank; } else { msgA = "best 3  " + rank; }
      ui_2d_text(cx, by + rows * tile ~/ 2 + 20, msgA, cText, 13, 1);
      if (lang == 0) { msgA = "A / 空格 / 回车 重开"; } else { msgA = "A / Space / Enter  restart"; }
      ui_2d_text(cx, by + rows * tile ~/ 2 + 42, msgA, cDim, 12, 1);
    }

    ui_2d_present();

    t = ui_msg_fetch_wait(300);
    if (t == 0) { continue; }

    if (t == 9) {
      // 这一帧该走几步:**按墙钟欠账**(各平台定时器分辨率不同 ⇒ 不能拿"收到几拍"当时间)。

      // how many steps this frame takes: **driven by the wall-clock deficit** (timer resolution differs per platform => never treat "ticks received" as time).

      // 用户 2026-10-04 口径:所有游戏时钟取系统毫秒数。定时器只负责唤醒。

      // user's 2026-10-04 rule: all game clocks read system milliseconds; the timer only wakes the loop.

      nowMs = ui_time_get_ms();
      acc = acc + (nowMs - lastMs);
      lastMs = nowMs;
      if (acc > speed * 8) { acc = 0; }   // 卡太久别一次补一大串(防瞬移)

                                          // do not catch up in one huge burst after a long stall (prevents teleporting)

      while (acc >= speed) {
        acc = acc - speed;
        steps = steps + 1;
        // 只在玩的状态推进:暂停与结束画面都不动

        // Only advance while playing: the paused and game-over screens stay put

        if (state != 0) { continue; }
        dir = want;
        hr = body[0] ~/ cols;
        hc = body[0] % cols;
        if (dir == 0) { hr = hr - 1; }
        if (dir == 1) { hc = hc + 1; }
        if (dir == 2) { hr = hr + 1; }
        if (dir == 3) { hc = hc - 1; }
        crash = 0;
        if (hc < 0) { crash = 1; }
        if (hc >= cols) { crash = 1; }
        if (hr < 0) { crash = 1; }
        if (hr >= rows) { crash = 1; }
        if (crash == 0) {
          newhead = hr * cols + hc;
          grow = 0;
          if (newhead == food) { grow = 1; }
          tail = body[body.length - 1];
          if (body.contains(newhead) == 1) {
            // 不吃东西时尾巴会让出一格,走进那一格不算自撞

            // When not growing the tail vacates its cell, so entering it is not a self-collision

            if (grow == 0) {
              if (newhead != tail) { crash = 1; }
            } else {
              crash = 1;
            }
          }
          if (crash == 0) {
            body.insert(0, newhead);
            if (grow == 1) {
              score = score + 10;
              eaten = eaten + 1;
              ui_audio_beep(880, 45);
              food = ui_math_rand(cols * rows);
              guard = 0;
              while (body.contains(food) == 1 && guard < 500) {
                food = ui_math_rand(cols * rows);
                guard = guard + 1;
              }
            } else {
              body.removeAt(body.length - 1);
            }
          }
        }
        if (crash == 1) {
          ui_audio_beep(131, 320);
          state = 1;
          rounds.add(score);
          played = played + 1;
          if (score > best) { best = score; }
        } else {
          // 难度阶梯按"走了多少格"升,与吃到什么无关,因此可复现

          // The difficulty ladder climbs with the number of steps taken, not with what was eaten, so it is reproducible

          j = 140 - (steps ~/ 12) * 6;
          if (j < 80) { j = 80; }
          if (j != speed) {
            speed = j;
            ui_timer_kill(tid);
            tid = ui_timer_set(speed, 1);
          }
        }
      }
      continue;
    }

    if (t == 1) {
      key = ui_msg_get_a();
      keys = keys + 1;
      if (state == 0) {
        if (key == 38) { if (dir != 2) { want = 0; } }
        if (key == 40) { if (dir != 0) { want = 2; } }
        if (key == 37) { if (dir != 1) { want = 3; } }
        if (key == 39) { if (dir != 3) { want = 1; } }
        if (key == 32 || key == 13 || key == 65) { state = 2; }
        if (key == 27 || key == 66) { done = 1; }
      } else if (state == 1) {
        if (key == 32 || key == 13 || key == 65) {
          // 重开:只重置这一局,本场榜单与最高分留着

          // Restart: reset only this round, keeping the session ranking and the best score

          ui_audio_beep(523, 80);
          body.clear();
          body.add(2);
          body.add(1);
          body.add(0);
          dir = 1;
          want = 1;
          score = 0;
          eaten = 0;
          state = 0;
          steps = 0;
          speed = 140;
          food = cols - 1;
          ui_timer_kill(tid);
          tid = ui_timer_set(speed, 1);
        }
        if (key == 27 || key == 66) { done = 1; }
      } else if (state == 2) {
        if (key == 32 || key == 13 || key == 65) { state = 0; }
        if (key == 27 || key == 66) { done = 1; }
      }
      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("snake: rounds="); println_int(played);
  print_str("snake: steps="); println_int(steps);
  print_str("snake: keys="); println_int(keys);
  print_str("snake: eaten="); println_int(eaten);
  print_str("snake: score="); println_int(score);
  print_str("snake: best="); println_int(best);
  print_str("snake: cols="); print_int(cols);
  print_str("x"); println_int(rows);
  print_str("snake: body="); println_int(body.length);
  println_str("snake: 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_len="); println_int(B.substring(0, 5).length);
  print_str("chk.str_upper_len="); println_int(B.toUpperCase().length);
  print_str("chk.str_lower_len="); println_int(B.toLowerCase().length);
  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_len="); println_int(B.replaceAll("l", "L").length);
  print_str("chk.str_replaceFirst_len="); println_int(B.replaceFirst("l", "L").length);
  print_str("chk.str_padLeft="); println_int("7".padLeft(4, "0").length);
  print_str("chk.str_padRight="); println_int("7".padRight(4, ".").length);
  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_first="); println_int(M[0]);
  M.reverse();
  print_str("chk.list_reverse_first="); println_int(M[0]);
  print_str("chk.list_sublist_len="); println_int(M.sublist(1, 3).length);
  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 n = 12345;
  int m = -7;
  String s1 = "n=$n";
  String s2 = "${n}";
  String s3 = "m=$m";
  String s4 = "a" + "$n" + "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("snake: selftest done");
}