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