csharp/app/snake.cs · 21553 字节 · 下载原文
// 贪吃蛇 —— 用 **C#** 写的手机游戏(不是 C)
// Snake — a phone game written in **C#** (not C)
//
// ✅ **状态:跑通了**(v0.96.190)。逐像素量:蛇头 316 像素(1 格)、蛇身 632(2 格)、
// ✅ **Status: it runs** (v0.96.190). Measured pixel by pixel: snake head 316 pixels (1 cell), snake body 632 (2 cells),
// 食物 316(1 格)—— 三个数都是精确的整格。
// food 316 (1 cell) — all three numbers are exactly whole cells.
//
// ## 卡住它的四条(patch 0010,全部在 C# 前端里)
// ## The four things that blocked it (patch 0010, all in the C# frontend)
//
// ① **带参方法"参数串槽"** —— 序言里"把实参读进 R0"那句写成了 `MOVE [R12+12+4i], R0`,
// ① **Parameters of methods with arguments "crossing slots"** — the prologue's "read the argument into R0" was written as `MOVE [R12+12+4i], R0`,
// 而 `MOVE dest, src` 的 dest 在前 ⇒ 那是**把 R0 存进调用方的实参槽**。局部变量于是
// and in `MOVE dest, src` dest comes first ⇒ that **stores R0 into the caller's argument slot**. The local variable then
// 拿到"调用那一刻 R0 恰好留着的值"(= 调用点最后求值的那个实参)。
// gets "whatever value R0 happened to hold at the moment of the call" (= the last argument evaluated at the call site).
// `a1(x)` 侥幸对,`a2(x,y)` 的 y 变成 x(两个矩形完全重叠,看着只有一个),
// `a1(x)` worked by luck; in `a2(x,y)` the y became x (the two rectangles overlapped exactly, so only one was visible),
// `a3(x,y,c)` 的 c 变成 x(近黑色,在黑底上看不见)。
// and in `a3(x,y,c)` the c became x (near-black, invisible on a black background).
// ② **读数组元素永远读到数组头** —— `add R1 R1 R0` 的 2 操作数形式是 `Rd = Rd + Rs`,
// ② **Array element reads always hit the array head** — the 2-operand form of `add R1 R1 R0` is `Rd = Rd + Rs`,
// 而 SHL/ADD 的立即数当 dest 时**结果被丢弃**(VM 只写 `dest.Type == REGISTER`)。
// and when an immediate is used as dest for SHL/ADD **the result is discarded** (the VM only writes when `dest.Type == REGISTER`).
// ③ **`a` 指向的不是数组块** —— 数组字面量结尾那句 `MOVE R0, LABEL ptrLabel` 取的是
// ③ **`a` does not point at the array block** — the `MOVE R0, LABEL ptrLabel` at the end of an array literal takes
// **标签地址**(LEA),不是标签里的值;`.Length` 那处也一样。
// the **label address** (LEA), not the value inside the label; the same goes for `.Length`.
// ④ **`static int[] sx = new int[N]` 从来没分配过** —— 类字段一律 `continue` 掉了,
// ④ **`static int[] sx = new int[N]` was never allocated** — class fields were uniformly `continue`d past,
// 数组块一个字都没留;而 `new int[N]` 的**长度在解析时就被丢掉**,所以连"少分配"
// leaving not a word of the array block; and the **length of `new int[N]` was dropped during parsing**, so it was not even
// 都谈不上,是"没分配 + 基址是 0"。
// "under-allocated" — it was "not allocated at all + base address 0".
//
// ⚠ 四条的**表象各不相同**(参数错位 / 读到数组头 / 读到 0 / 读到低内存垃圾),
// ⚠ The four **looked completely different** (shifted arguments / reading the array head / reading 0 / reading low-memory garbage),
// 但根子只有两类:**操作数顺序写反**(①②③)与**字段初始化没生成**(④)。
// but the root causes were only two kinds: **operand order reversed** (①②③) and **field initialization not generated** (④).
// 查它们唯一可靠的办法是**倒出生成的汇编读**(`vmlhost asm`),不是改写法试。
// The only reliable way to investigate them is to **dump the generated assembly and read it** (`vmlhost asm`), not to try different styles.
//
// 全 22 个前端的结论见 `docs/前端游戏能力评估.md`。
// For the conclusions across all 22 frontends see the frontend game-capability assessment doc under `docs/`.
//
// ## 为什么能这么写
// ## Why it can be written this way
//
// 手机那套 UI(开窗 / 绘图 / 输入 / 定时器)的实现是 C 写的(`Lib/shared/src/vmlui.c`
// The phone UI (window / drawing / input / timers) is implemented in C (`Lib/shared/src/vmlui.c`
// → `Lib/shared/vmlui.vml`),由 `vmltool.config.xml` 的 `<Language ... Libs="vmlui.vml">`
// → `Lib/shared/vmlui.vml`) and hooked to each frontend by `<Language ... Libs="vmlui.vml">` in `vmltool.config.xml`.
// 挂给每种前端。前端只要能**直接 `call` 那个标签**、且**实参逆序压栈**(包装函数读
// A frontend only needs to **`call` that label directly** with **arguments pushed in reverse order** (the wrapper reads
// `[R12+12]` = 最后压的那个 = C 的第一个参数),就能调 —— 实测 C# 四关全过
// `[R12+12]` = the last one pushed = C's first parameter) to call it — measured: C# passes all four gates
// (编译出图 / 循环 / 数组 / 函数),还过了"定时器 + 收消息 + 重画"的游戏心跳。
// (compiles and draws / loops / arrays / functions), and also the "timer + receive message + redraw" game heartbeat.
//
// ## 常量为什么是数字
// ## Why the constants are numbers
//
// 数值的**唯一真源是 `Lib/c/vml_ui.h`**(C 的宏)。C# 前端没有 include C 头文件的机制,
// The **single source of truth for these numbers is `Lib/c/vml_ui.h`** (the C macros). The C# frontend has no mechanism to include C headers,
// 所以这里复制了一份。⚠ **改那些宏时要同步这里**。
// so a copy lives here. ⚠ **When you change those macros, sync this copy**.
// 想彻底消掉这份重复,正解是用仓库自带的 `Lib/build_static.sh` 从 C 头生成各语言绑定
// To eliminate this duplication entirely, the right move is to generate per-language bindings from the C headers with the repo's own `Lib/build_static.sh`
// (它会产出 `Lib/<语言>/ext/<名字>.<扩展名>`),那是后面该做的一步。
// (it produces `Lib/<language>/ext/<name>.<ext>`); that is a later step.
//
// ## 操作
// ## Controls
//
// 方向键 / 屏幕手柄方向键转向;START 重开;SELECT 暂停;ESC 或返回箭头退出。
// Arrow keys / the on-screen gamepad's arrows steer; START restarts; SELECT pauses; ESC or the back arrow exits.
// 吃到食物 +10 分并加速;撞墙或撞到自己结束(会弹对话框,别让玩家猜)。
// Eating food is +10 points and a speed-up; hitting a wall or yourself ends the game (a dialog pops up — don't make the player guess).
// 显式引用 UI 库:`ui_*` 的声明在 `Lib/csharp/VmlUi.cs`(tools/GenLib 生成,别手改)。
// Explicit reference to the UI library: `ui_*` is declared in `Lib/csharp/VmlUi.cs` (generated by tools/GenLib; don't hand-edit).
using Vml.Ui;
class Snake
{
// ── 常量(源:Lib/c/vml_ui.h)──
// ── Constants (source: Lib/c/vml_ui.h) ──
const int MSG_KEYDOWN = 1;
const int MSG_TIMER = 9;
const int MSG_WINDOWCLOSE = 10;
const int KEY_ENTER = 13;
const int KEY_SELECT = 16;
const int KEY_ESCAPE = 27;
const int KEY_LEFT = 37;
const int KEY_UP = 38;
const int KEY_RIGHT = 39;
const int KEY_DOWN = 40;
const int DLG_INFO = 0;
const int ANCHOR_LEFT = 0;
const int ANCHOR_CENTER = 1;
const int BG = -15724520; // 0xFF101018 底色
// 0xFF101018 background
const int GRID_C = -15263713; // 0xFF17181F 网格
// 0xFF17181F grid
const int SNAKE_C = -11409298; // 0xFF51E86E 蛇身
// 0xFF51E86E snake body
const int HEAD_C = -63488; // 0xFFFF0800 蛇头
// 0xFFFF0800 snake head
const int FOOD_C = -131246; // 0xFFFDFF52 食物
// 0xFFFDFF52 food
const int TEXT_C = -6643536; // 0xFF9AA0B0 次要文字
// 0xFF9AA0B0 secondary text
const int WHITE = -1; // 0xFFFFFFFF
// ── 棋盘与蛇 ──
// ── Board and snake ──
const int CW = 20; // 列(格子数)
// columns (number of cells)
const int CH = 22; // 行
// rows
const int MAXLEN = 400; // 20*22 的上限
// the upper bound of 20*22
static int[] sx = new int[MAXLEN];
static int[] sy = new int[MAXLEN];
static int head; // 头在数组里的下标
// index of the head in the array
static int len; // 长度
// length
static int dx;
static int dy;
static int fx;
static int fy;
static int score;
static int best;
static int alive; // 1=在玩 0=结束
// 1=playing 0=over
static int paused;
static int stepMs; // 每步毫秒(越吃越快)
// milliseconds per step (the more you eat, the faster)
static int lang; // 界面语言:0=中文 1=英文(开局问一次宿主,见 Main)
// UI language: 0=Chinese 1=English (asked of the host once at the start, see Main)
static int acc; // 运动墙钟欠账(毫秒)—— 按墙钟补步,用户 2026-10-04 口径
// Motion wall-clock debt in ms — steps are caught up from the wall clock (user's 2026-10-04 rule)
static int lastMs; // 上次补步取的墙钟时刻(进主循环时初始化)
// wall-clock moment of the last catch-up (initialized on entering the main loop)
// ── 版面(开窗时算一次,画与命中判定共用)──
// ── Layout (computed once when the window opens, shared by drawing and hit-testing) ──
static int sw;
static int sh;
static int cell; // 格子边长(dp)
// cell side length (dp)
static int ox; // 棋盘左上角
// top-left corner of the board
static int oy;
static int hudY; // 分数行
// score row
static void layout()
{
int byw = sw - 8; // 左右各留 4
// 4 left on each side
int byh = sh - 46; // 顶部让出 40 给分数行
// leave 40 at the top for the score row
cell = byw / CW;
if (byh / CH < cell) cell = byh / CH;
if (cell < 4) cell = 4;
ox = (sw - cell * CW) / 2;
oy = 40;
hudY = 8;
}
static void placeFood()
{
int tries = 0;
while (tries < 500)
{
int c = ui_math_rand(CW);
int r = ui_math_rand(CH);
if (occupied(c, r) == 0) { fx = c; fy = r; return; }
tries = tries + 1;
}
// 兜底:找不到空位就放 (0,0)(棋盘满了本来也就快赢了)
// Fallback: if no free spot is found, place it at (0,0) (a full board means you were about to win anyway)
fx = 0; fy = 0;
}
static int occupied(int c, int r)
{
int i = 0;
while (i < len)
{
int k = head - i;
while (k < 0) k = k + MAXLEN;
if (sx[k] == c && sy[k] == r) return 1;
i = i + 1;
}
return 0;
}
static void reset()
{
len = 3;
head = 2;
sx[0] = 8; sy[0] = 10;
sx[1] = 7; sy[1] = 10;
sx[2] = 6; sy[2] = 10;
dx = 1; dy = 0;
score = 0;
alive = 1;
paused = 0;
stepMs = 170;
placeFood();
}
static void drawCell(int c, int r, int color)
{
ui_2d_rect(ox + c * cell, oy + r * cell, cell - 1, cell - 1, color, 1, 0, 2);
}
static void draw()
{
ui_2d_clear(BG);
// 棋盘格(淡网格 —— 让玩家看得出格子在哪,不然蛇的移动会显得"跳")
// Board cells (a faint grid — so the player can see where the cells are; otherwise the snake's movement looks like it "jumps")
int c = 0;
while (c < CW)
{
int r = 0;
while (r < CH)
{
if ((c + r) % 2 == 0) drawCell(c, r, GRID_C);
r = r + 1;
}
c = c + 1;
}
drawCell(fx, fy, FOOD_C);
int i = 0;
while (i < len)
{
int k = head - i;
while (k < 0) k = k + MAXLEN;
if (i == 0) drawCell(sx[k], sy[k], HEAD_C);
else drawCell(sx[k], sy[k], SNAKE_C);
i = i + 1;
}
ui_2d_text(8, hudY, lang == 0 ? "得分" : "Score", TEXT_C, 13, ANCHOR_LEFT);
ui_2d_text(56, hudY, numToStr(score), WHITE, 15, ANCHOR_LEFT);
ui_2d_text(sw / 2, hudY, lang == 0 ? "最高" : "Best", TEXT_C, 13, ANCHOR_CENTER);
ui_2d_text(sw / 2 + 44, hudY, numToStr(best), WHITE, 15, ANCHOR_LEFT);
if (paused != 0) ui_2d_text(sw / 2, sh / 2, lang == 0 ? "暂停(SELECT 继续)" : "Paused (SELECT)", FOOD_C, 16, ANCHOR_CENTER);
ui_2d_present();
}
// 数字 → 字符串。**不能借标准库的字符串转换**:那是 C 的运行库,别的前端不一定有;
// Number → string. **Cannot borrow the standard library's string conversion**: that is C's runtime library and other frontends don't necessarily have it;
// 自己按位拆反而哪门语言都能照抄。
// splitting digits by hand is something any language can copy.
static string numToStr(int v)
{
if (v == 0) return "0";
string s = "";
int neg = 0;
if (v < 0) { neg = 1; v = 0 - v; }
while (v > 0)
{
int d = v % 10;
s = digit(d) + s;
v = v / 10;
}
if (neg != 0) s = "-" + s;
return s;
}
static string digit(int d)
{
if (d == 0) return "0";
if (d == 1) return "1";
if (d == 2) return "2";
if (d == 3) return "3";
if (d == 4) return "4";
if (d == 5) return "5";
if (d == 6) return "6";
if (d == 7) return "7";
if (d == 8) return "8";
return "9";
}
// 一步推进。返回 1 表示画面变了(要重画)
// One step of the game. Returning 1 means the picture changed (a redraw is needed)
static int step()
{
if (alive == 0 || paused != 0) return 0;
int nc = sx[head] + dx;
int nr = sy[head] + dy;
// 撞墙
// Hit a wall
if (nc < 0 || nc >= CW || nr < 0 || nr >= CH) { gameOver(); return 1; }
// 撞自己(尾巴那一格下一步会让开,所以只查到 len-1)
// Hit itself (the tail cell will move away on the next step, so only scan up to len-1)
int i = 0;
while (i < len - 1)
{
int k = head - i;
while (k < 0) k = k + MAXLEN;
if (sx[k] == nc && sy[k] == nr) { gameOver(); return 1; }
i = i + 1;
}
int eat = 0;
if (nc == fx && nr == fy) eat = 1;
// 前进:头指针前移一格;不吃就把尾巴缩掉(靠 len 控制,不用真删数据)
// Advance: move the head pointer forward one cell; if nothing is eaten shrink the tail (controlled by len, no real data deletion)
head = head + 1;
if (head >= MAXLEN) head = 0;
sx[head] = nc;
sy[head] = nr;
if (eat != 0)
{
if (len < MAXLEN) len = len + 1;
score = score + 10;
if (score > best) best = score;
if (stepMs > 70) stepMs = stepMs - 6; // 越吃越快,但留个下限
// the more you eat the faster, but with a floor
// 音效:单音 ui_audio_beep(v0.96.509 从音序器换回来 ——
// Sound: single-tone ui_audio_beep (switched back from the sequencer in v0.96.509 —
// 那一版多声部叠加 / 长音拖尾在真机上破音)
// that version's multi-voice stacking / long-note tails broke up on real devices)
ui_audio_beep(1047, 165);
placeFood();
}
return 1;
}
static void gameOver()
{
alive = 0;
// 音效:单音 ui_audio_beep;**结局音取最低音**(吃到 1047 / 撞到 131,差得开)
// Sound: single-tone ui_audio_beep; **the ending tone takes the lowest note** (1047 on eating / 131 on a crash — far enough apart)
ui_audio_beep(131, 320);
draw(); // 先把终局画面画出来
// draw the final frame first
if (ui_dlg_msg(lang == 0 ? "贪吃蛇" : "Snake", lang == 0 ? "撞到了,这一局结束。\n再来一局?(选「否」退出)" : "You crashed. Round over.\nPlay again? (choose \"No\" to quit)", DLG_INFO) != 0) { ui_win_close(); return; }
reset();
}
static void turn(int nx, int ny)
{
// 不能原地掉头(那会立刻撞自己)
// Cannot reverse in place (that would immediately hit itself)
if (nx + dx == 0 && ny + dy == 0) return;
dx = nx;
dy = ny;
}
static void Main()
{
// 界面语言:开局问一次宿主要中文还是英文(0=中文 1=英文),之后整局按它分支。
// UI language: ask the host once at the start whether it wants Chinese or English (0=Chinese 1=English), then branch on it for the whole game.
// ⚠ 别在每帧里调 —— 那是一次 syscall。
// ⚠ Don't call it in every frame — that's a syscall.
lang = ui_scr_get_language();
// 开窗:尺寸传 0 ⇒ 交给宿主给真实可用区(开窗**之前**问到的只是估算值)
// Opening: pass size 0 ⇒ the host supplies the real available area (a **pre-open** read is only an estimate)
ui_win_open(lang == 0 ? "贪吃蛇" : "Snake", 0, 0);
// 尺寸:开窗**之后**再问一次 —— 这时 `ui_scr_get_w/h` 给的是实测画布(真值)
// Size: re-read **after** the open — `ui_scr_get_w/h` now report the measured canvas (the real value)
sw = ui_scr_get_w();
sh = ui_scr_get_h();
if (sw <= 0) sw = 360;
if (sh <= 0) sh = 620;
layout();
ui_scr_set_keep_on(1);
reset();
draw();
int tid = ui_timer_set(stepMs, 0);
int curMs = stepMs;
acc = 0;
lastMs = ui_time_get_ms(); // 墙钟起点:进主循环时取(不在模块级/全局初始化里调 syscall)
// Wall-clock origin: taken when entering the main loop (no syscall at module level or in a global initializer)
while (ui_win_is_closed() == 0)
{
int t = ui_msg_fetch_wait(0);
// 这一帧该走几步:**按墙钟欠账**(各平台定时器分辨率不同 ⇒ 不能拿"收到几拍"当时间)。
// How many steps this frame: **by wall-clock debt** (timer resolution differs per platform, so
// 用户 2026-10-04 口径:所有游戏时钟取系统毫秒数。
// "how many ticks arrived" is not a measure of time).
// User rule of 2026-10-04: every game clock reads system milliseconds.
int now2 = ui_time_get_ms();
acc = acc + (now2 - lastMs);
lastMs = now2;
if (acc > stepMs * 8) acc = 0; // 卡太久别一次补一大串(防瞬移)
// Do not catch up in one huge burst after a long stall (anti-teleport)
while (acc >= stepMs) { acc = acc - stepMs; if (step() != 0) draw(); }
if (t == 0) continue;
if (t == MSG_WINDOWCLOSE) break;
if (t == MSG_TIMER)
{
// 定时器**只负责唤醒**(推进在上面按墙钟欠账走)
// The timer **only wakes us up** — the advance happens above, by wall-clock debt
// 提速后要把定时器换掉(重复定时器的间隔是建立时定死的)
// After speeding up you must replace the timer (a repeating timer's interval is fixed when it is created)
if (stepMs != curMs)
{
ui_timer_kill(tid);
curMs = stepMs;
tid = ui_timer_set(curMs, 0);
}
continue;
}
if (t == MSG_KEYDOWN)
{
int k = ui_msg_get_a();
if (k == KEY_ESCAPE) break;
if (k == KEY_LEFT) turn(-1, 0);
else if (k == KEY_RIGHT) turn(1, 0);
else if (k == KEY_UP) turn(0, -1);
else if (k == KEY_DOWN) turn(0, 1);
else if (k == KEY_ENTER) { reset(); draw(); }
else if (k == KEY_SELECT) { if (paused != 0) paused = 0; else paused = 1; draw(); }
}
}
ui_timer_kill(tid);
ui_scr_set_keep_on(0);
ui_win_close();
}
}