java/app/snake.java · 37758 字节 · 下载原文
// 贪吃蛇 —— 用 **Java** 写的手机游戏
// Snake — a phone game written in **Java**
//
// 玩法:蛇一直往前爬,吃到食物长一节、快一档;撞墙或咬到自己就结束。
// Gameplay: the snake keeps crawling; each food makes it one link longer and one notch faster;
// hitting a wall or biting itself ends the game.
// 操作:方向键 / WASD 转向;点屏幕(或按住拖动)= 朝**手指相对蛇头**的方向拐;
// Controls: arrows / WASD to steer; tap or drag the screen = turn toward the **finger relative to the head**;
// SELECT 暂停 / 回车重开 / ESC 退出;棋盘铺满(蛇占满全屏)= 通关。
// SELECT pauses / Enter restarts / ESC quits; filling the whole board (no free cell left) = you win.
//
// ◆ 手机那套 UI 是怎么引来的(这一节是本文件与 pong/sokoban 最大的不同)
// ◆ How the phone UI is pulled in (the biggest difference from pong/sokoban)
//
// 开窗 / 绘图 / 输入 / 定时器是 C 写的(`Lib/shared/src/vmlui.c` → `vmlui.vml`),
// Window / drawing / input / timers are written in C (`Lib/shared/src/vmlui.c` → `vmlui.vml`),
// 由 `vmltool.config.xml` 的 `<Language Name="java" Libs="vmlui.vml,tty.vml">` 挂给 Java 前端。
// hooked to the Java frontend by `<Language Name="java" Libs="vmlui.vml,tty.vml">` in `vmltool.config.xml`.
//
// 文件开头一句 `import vml.ui.*;` 就是"引用外部库"的样子 —— **不需要再手写一坨 `static native` 声明**
// The single `import vml.ui.*;` at the top is the whole "reference the library" gesture — **no `static native` block**
// (`pong.java` / `sokoban.java` 里那一大段是历史遗留,见下面的"实测")。
// (the long block in `pong.java` / `sokoban.java` is legacy; see the measurements below).
//
// 它为什么能跑(**2026-10-01 实测,两条 A/B 对照**):
// Why it works (**measured 2026-10-01, two A/B comparisons**):
// · 直接调 `ui_2d_rect(…)` 会被前端编成 `CALL method_ui_rect`;
// · a plain `ui_2d_rect(…)` call is emitted as `CALL method_ui_rect`;
// · 链接器(`VMLAssembler/LibraryLinker.cs` 的"情况 1b")会剥掉 `method_` 前缀,
// · the linker ("case 1b" in `VMLAssembler/LibraryLinker.cs`) strips the `method_` prefix,
// 再按 `ui_ → vmlui` 这张前缀表解析到 `lib_vmlui_ui_rect` —— 于是**名字对上了、调用直接就通**。
// then resolves it through the `ui_ → vmlui` prefix table to `lib_vmlui_ui_rect` — the name lines up and the call just works.
// · 判据:同一段绘图代码,「import + 不声明」与「static native 声明」两种写法跑出来的
// · Judge: for the same drawing code, the "import + no declarations" form and the "static native" form produce
// `--trace-draw` 逐条**完全相同**(clear/rect/circle/line/text 五条,字节一致)。
// byte-identical `--trace-draw` output (all five calls: clear/rect/circle/line/text).
// · 另一条对照:`#include "vml_ui.java"`(把声明头**拼进类体**,走共享 C 预处理器)也能编能跑 ——
// · Second comparison: `#include "vml_ui.java"` (splicing a declaration header **into the class body** through the
// shared C preprocessor) also compiles and runs —
// 但那条要求随包发一份头文件,所以本文件选了更省事的 `import`。
// but that one needs a header file shipped alongside, so this file picked the cheaper `import`.
//
// ⚠ **代价(必须知道)**:这条"不声明也能调"的路**与"用户方法名被链接器劫持"是同一段代码**。
// ⚠ **The cost (worth knowing)**: this "call without declaring" path **is the same code that hijacks user method names**.
// 自定义方法只要与库里某个名字重名(实测 `itoa` / `sum` / `add`),调用点就会被改指到库函数上。
// Any user method sharing a name with a library symbol (measured: `itoa`, `sum`, `add`) gets its call site
// redirected to the library function.
// 本文件的对策:**所有自己的方法一律带 `sk` 前缀**(`skDraw` / `skStep` …),绝不与库名撞车。
// This file's defence: **every method of its own carries the `sk` prefix** (`skDraw` / `skStep` …), never colliding.
//
// ⚠ 状态一律放**一个堆数组** `S`:`main` 的栈帧恒为 256 个字、程序里局部量一多就会越界踩到调用方
// ⚠ All state lives in **one heap array** `S`: `main`'s frame is a fixed 256 words, and a pile of locals overruns it
// (台账"局部变量的帧是固定的"那条)。尺寸与下标表见下面 `S` 的注释。
// (the ledger's "the local frame is fixed" entry). Sizes and slot map are in the comment on `S` below.
//
// 跑法:手机 `vml run examples/java/snake.java`;
// How to run: phone `vml run examples/java/snake.java`;
// 桌面 `vmlcli Examples/java/snake.java --screen 395x578 --frames /tmp/fr`
// desktop `vmlcli Examples/java/snake.java --screen 395x578 --frames /tmp/fr`
import vml.ui.*;
public class Snake {
// ── 消息类型(与 `VmlUiProtocol.cs` 的 VmlMsgType 同值)──
// ── Message types (same values as VmlMsgType in `VmlUiProtocol.cs`) ──
static final int MSG_KEYDOWN = 1;
static final int MSG_TOUCHDOWN = 6;
static final int MSG_TOUCHMOVE = 7;
static final int MSG_TOUCHUP = 8;
static final int MSG_TIMER = 9;
static final int MSG_CLOSE = 10;
static final int MSG_RESIZE = 11;
static final int MSG_ORIENT = 12;
// ── 键码就是 Win32 虚拟键值(手柄与物理键盘同一套)──
// ── Key codes are Win32 virtual-key values (the gamepad and a physical keyboard share them) ──
static final int KEY_ESC = 27;
static final int KEY_ENTER = 13;
static final int KEY_SELECT = 16;
static final int KEY_SPACE = 32;
static final int KEY_LEFT = 37;
static final int KEY_UP = 38;
static final int KEY_RIGHT = 39;
static final int KEY_DOWN = 40;
static final int KEY_A = 65;
static final int KEY_D = 68;
static final int KEY_S = 83;
static final int KEY_W = 87;
// ── 方向。NONE 是"没有待转方向"的哨兵 ──
// ── Directions. NONE is the sentinel for "no pending turn" ──
static final int DIR_UP = 0;
static final int DIR_DOWN = 1;
static final int DIR_LEFT = 2;
static final int DIR_RIGHT = 3;
static final int DIR_NONE = 4;
// ── 棋盘:14 列 × 18 行(手机上 395×578 实测格边长 27px,桌面 480×640 是 31px)──
// ── Board: 14 columns x 18 rows (measured 27px cells at 395x578 on a phone, 31px at 480x640 on the desktop) ──
static final int NC = 14;
static final int NR = 18;
// ── 数组分区:0..31 标量 / 32..283 棋盘格子 / 284..539 蛇身环缓冲 ──
// ── Array layout: 0..31 scalars / 32..283 board cells / 284..539 the snake's ring buffer ──
static final int B0 = 32;
static final int R0 = 284;
static final int RLEN = 256;
/* S 的下标表(全是本文件的私有约定)
The S slot map (every entry is private to this file)
0 状态(0 玩 1 死 2 暂停 3 通关) 1 当前方向 2 待转方向(DIR_NONE = 无)
State 0 play / 1 dead / 2 paused / 3 cleared; 1 current dir; 2 pending dir (DIR_NONE = none)
3 分数(吃到的颗数) 4 长度 5 头在环里的下标
3 score (food eaten); 4 length; 5 ring index of the head
6 尾在环里的下标 7 几拍走一格 8 拍计数器
6 ring index of the tail; 7 ticks per cell; 8 the tick counter
9 棋盘宽(px) 10 棋盘高(px) 11 格边长
9 board width in px; 10 board height in px; 11 cell size
12 棋盘左上 x 13 棋盘左上 y 14 屏宽 15 屏高
12 board origin x; 13 board origin y; 14 screen width; 15 screen height
16 界面语言(0 中文 1 英文) 17 死亡后的拍数 18 走过的格数
16 UI language (0 Chinese, 1 English); 17 ticks since death; 18 cells travelled
19 第几局 20 最大长度 21 开机自检结果(0 = 全过)
19 games played; 20 longest length; 21 boot self-check result (0 = all pass)
22 食物格在 S 里的下标 23 头格在 S 里的下标 24 帧号
22 S index of the food cell; 23 S index of the head cell; 24 frame counter
25 每帧重画的次数(与 24 同值,留着读日志用) 26 撞墙次数 27 咬到自己次数
25 redraws (same as 24, kept for the log); 26 wall hits; 27 self bites
28 自检静音位(只有开机自检那一拍是 1) 29..31 余量
28 self-check mute flag (1 only during the boot self-check); 29..31 slack */
// 数组长度 548 = 标量 32 + 棋盘 252 + 环 256 + 余量 8
// Array length 548 = 32 scalars + 252 board + 256 ring + 8 slack
static int[] S = new int[548];
// 颜色一律写**负数十进制**(`-65536` = 0xFFFF0000),避开十六进制字面量在词法层的解读差异
// Colors are **negative decimals** (`-65536` = 0xFFFF0000), dodging the lexer's view of hex literals
static final int C_BG = -15855594; // 0xFF0E1016
static final int C_BOARD = -15328734; // 0xFF161A22
static final int C_GRID = -14472650; // 0xFF232A36
static final int C_TEXT = -1512206; // 0xFFE8ECF2
static final int C_MUTED = -7695453; // 0xFF8A93A3
static final int C_HEAD = -11870592; // 0xFF4ADE80
static final int C_BODY = -13722021; // 0xFF2E9E5B
static final int C_BODY2 = -13126806; // 0xFF37B36A
static final int C_FOOD = -495247; // 0xFFF87171
static final int C_GOLD = -340971; // 0xFFFACC15
static final int C_DANGER = -8446691; // 0xFF7F1D1D
// ── 极简几何工具(Math.abs 一直可用;名字带 sk 前缀避开库名)──
// ── Tiny geometry helpers (Math.abs has always worked; the sk prefix avoids library names) ──
static int skAbs(int v) {
if (v < 0) { return 0 - v; }
return v;
}
static int skDx(int d) {
if (d == DIR_LEFT) { return -1; }
if (d == DIR_RIGHT) { return 1; }
return 0;
}
static int skDy(int d) {
if (d == DIR_UP) { return -1; }
if (d == DIR_DOWN) { return 1; }
return 0;
}
// 方向 a 与 b 是不是正好相反(相反就是"反折",必须挡住:不然蛇会一头撞进自己的脖子)
// Whether a and b are exactly opposite (an opposite turn would fold the snake onto its own neck, so it is blocked)
static int skOpposite(int a, int b) {
if (a == DIR_UP && b == DIR_DOWN) { return 1; }
if (a == DIR_DOWN && b == DIR_UP) { return 1; }
if (a == DIR_LEFT && b == DIR_RIGHT) { return 1; }
if (a == DIR_RIGHT && b == DIR_LEFT) { return 1; }
return 0;
}
// 中英文字:`ui_scr_get_language()` 给 0 = 中文、1 = 英文
// Chinese/English text: `ui_scr_get_language()` returns 0 for Chinese, 1 for English
static String skL(String zh, String en) {
if (S[16] == 0) { return zh; }
return en;
}
// 一个整数 → 字符串。**不能写 `"n=" + n`**(int→String 的 `+` 在本前端拼不出数,台账里那条 🔴),
// An int -> String helper. **`"n=" + n` cannot be used** (int->String `+` does not produce a number here; the ledger's red entry),
// 所以自己一位一位拼;拼接走库里的 `java_string_concat`,两段以内是安全的。
// so digits are joined one at a time; the join goes through `java_string_concat`, which is safe for two segments per call.
static String skDig(int v) {
if (v == 0) { return "0"; }
if (v == 1) { return "1"; }
if (v == 2) { return "2"; }
if (v == 3) { return "3"; }
if (v == 4) { return "4"; }
if (v == 5) { return "5"; }
if (v == 6) { return "6"; }
if (v == 7) { return "7"; }
if (v == 8) { return "8"; }
if (v == 9) { return "9"; }
return "?";
}
static String skNum(int v) {
if (v == 0) { return "0"; }
int neg = 0;
if (v < 0) { neg = 1; v = 0 - v; }
String s = "";
while (v > 0) {
s = skDig(v % 10) + s;
v = v / 10;
}
if (neg == 1) { s = "-" + s; }
return s;
}
// 日志走 stdout —— **只在事件点上打**(吃到 / 死掉 / 重开),不是每帧都打。
// Logging goes to stdout — **only at event points** (eat / die / restart), not every frame.
static void skLog(String tag, int v) {
System.out.print(tag);
System.out.println(v);
}
// ── 排版:全部由屏宽屏高推出来,一处写死的画布尺寸都没有 ──
// ── Layout: everything is derived from the reported screen size, no hard-coded canvas ──
static void skLayout() {
int hud = 52;
int foot = 36;
int bw = S[14] - 16;
int bh = S[15] - hud - foot;
int cw = bw / NC;
int chh = bh / NR;
int cell = cw;
if (chh < cell) { cell = chh; }
if (cell < 6) { cell = 6; }
S[11] = cell;
S[9] = cell * NC;
S[10] = cell * NR;
S[12] = (S[14] - S[9]) / 2;
S[13] = hud + (bh - S[10]) / 2;
}
// 某一格(格坐标)在 S 里的下标
// Index into S for a board cell (grid coordinates)
static int skCellAt(int x, int y) {
return B0 + y * NC + x;
}
// 走到 (nx,ny) 会撞上什么:0 = 进得去 / 1 = 出界 / 2 = 咬到自己
// What (nx,ny) holds for the head: 0 = free to enter / 1 = off-board / 2 = its own body
// ⚠ **尾巴那一格例外**:这一拍尾巴会先腾出来,头正好进得去 —— 标准贪吃蛇的宽容规则,
// ⚠ **The tail cell is the exception**: it vacates this very tick, so the head fits — the forgiving rule
// 也是"贴着尾巴跑"能成立的原因。**分类**(1 与 2)要分开,因为两者发的音、记的账不同。
// that makes "chasing your tail" work. The two kinds are separate because they beep and count differently.
static int skBlocked(int nx, int ny) {
if (nx < 0 || nx >= NC || ny < 0 || ny >= NR) { return 1; }
int t = B0 + ny * NC + nx;
if (S[t] == 1) {
if (t != S[R0 + S[6]]) { return 2; }
}
return 0;
}
// 每吃一颗快一档:5 拍走一格 → 最快 2 拍走一格(一拍 33ms)
// Each food speeds the snake up: 5 ticks per cell down to 2 (one tick = 33ms)
static int skSpeed() {
int v = 5 - S[3] / 4;
if (v < 2) { v = 2; }
return v;
}
// 放食物:随机找空格;棋盘快满时随机效率极低,所以撞满 400 次就顺序扫一遍兜底。
// Place food: pick a random free cell; when the board is nearly full randomness gets slow, so after 400 misses it scans.
static void skNewFood() {
int i = 0;
int tries = 0;
if (NC * NR - S[4] <= 0) {
S[0] = 3;
ui_audio_beep(1047, 320);
skLog("snake: win len=", S[4]);
return;
}
while (tries < 400) {
i = ui_math_rand(NC * NR);
if (S[B0 + i] == 0) {
S[B0 + i] = 2;
S[22] = B0 + i;
return;
}
tries = tries + 1;
}
i = 0;
while (i < NC * NR) {
if (S[B0 + i] == 0) {
S[B0 + i] = 2;
S[22] = B0 + i;
return;
}
i = i + 1;
}
}
// 开局第一颗食物**放在蛇头同一行、正前方 4~6 格**(列随机):玩家第一下不用动手就吃得到,
// The first food goes **on the head's row, 4-6 cells ahead** (random column): the player scores without steering,
// 而"一路向右必然经过它"也让"吃得对不对"有确定性判据(与随机到哪一列无关)。
// and "going right always passes through it" makes "did eating work" deterministic regardless of the random column.
static void skFirstFood() {
int c = 9 + ui_math_rand(3);
S[skCellAt(c, NR / 2)] = 2;
S[22] = skCellAt(c, NR / 2);
}
// 开一局:棋盘清零、蛇摆成三节、朝右、第一颗食物固定在正前方
// Start a game: clear the board, lay a three-link snake facing right, park the first food straight ahead
static void skNewGame() {
int i = 0;
while (i < NC * NR) { S[B0 + i] = 0; i = i + 1; }
i = 0;
while (i < RLEN) { S[R0 + i] = 0; i = i + 1; }
S[0] = 0;
S[1] = DIR_RIGHT;
S[2] = DIR_NONE;
S[3] = 0;
S[4] = 3;
S[5] = 2;
S[6] = 0;
S[7] = 5;
S[8] = 0;
S[17] = 0;
S[18] = 0;
S[19] = S[19] + 1;
S[B0 + (NR / 2) * NC + 3] = 1;
S[B0 + (NR / 2) * NC + 4] = 1;
S[B0 + (NR / 2) * NC + 5] = 1;
S[R0 + 0] = B0 + (NR / 2) * NC + 3;
S[R0 + 1] = B0 + (NR / 2) * NC + 4;
S[R0 + 2] = B0 + (NR / 2) * NC + 5;
skFirstFood();
skLog("snake: newgame len=", S[4]);
}
// 撞墙。`S[28]` 是**自检静音位**(只有开机自检那一下会置 1)——
// Hit a wall. `S[28]` is the **self-check mute flag** (set only during the boot self-check) —
// 自检不该在玩家还没开局时先放一声"死亡音",那会把音效日志搅浑。
// a self-check must not fire a death tone before the player even starts; it would muddy the audio log.
static void skHitWall() {
S[0] = 1;
S[26] = S[26] + 1;
if (S[28] == 0) {
ui_audio_beep(131, 320);
skLog("snake: dead-wall score=", S[3]);
}
}
// 咬到自己
// Bit itself
static void skBiteSelf() {
S[0] = 1;
S[27] = S[27] + 1;
if (S[28] == 0) {
ui_audio_beep(131, 320);
skLog("snake: dead-self score=", S[3]);
}
}
// 转向:反折挡住;待转方向只留最近一次(一拍内连按两个键时只有最后一个算数)
// Turn: opposite turns are blocked; only the latest pending direction survives (two taps in one tick -> the last one wins)
static void skTurn(int d) {
if (S[0] != 0) { return; }
if (skOpposite(d, S[1]) == 1) { return; }
S[2] = d;
}
static void skTogglePause() {
if (S[0] == 0) { S[0] = 2; ui_audio_beep(587, 60); return; }
if (S[0] == 2) { S[0] = 0; ui_audio_beep(587, 60); return; }
}
// 手指点哪儿、蛇就往哪儿拐:以**蛇头格心**为原点取主方向(十字分区),
// The finger steers: take the dominant axis from the **head's cell centre** (a cross-shaped split),
// 离头不足一格就当没点(那种抖动不该改变方向),反折交给 skTurn 挡。
// ignore anything closer than one cell (jitter must not steer), and let skTurn reject opposite turns.
static void skAim(int tx, int ty) {
int hc = S[R0 + S[5]];
int hx = S[12] + ((hc - B0) % NC) * S[11] + S[11] / 2;
int hy = S[13] + ((hc - B0) / NC) * S[11] + S[11] / 2;
int dx = tx - hx;
int dy = ty - hy;
if (skAbs(dx) + skAbs(dy) < S[11]) { return; }
if (skAbs(dx) > skAbs(dy)) {
if (dx > 0) { skTurn(DIR_RIGHT); } else { skTurn(DIR_LEFT); }
return;
}
if (dy > 0) { skTurn(DIR_DOWN); } else { skTurn(DIR_UP); }
}
// 一拍。返回 1 = 这一拍棋盘真的变了(用来决定要不要重画:手机上一帧要把整幅场景光栅化一遍,很贵)
// One tick. Returns 1 when the board really changed (used to decide on a redraw: a frame re-rasterises the
// whole scene on a phone and is expensive)
static int skStep() {
if (S[0] != 0) { return 0; }
S[8] = S[8] + 1;
if (S[8] % S[7] != 0) { return 0; }
int d = S[2];
if (d != DIR_NONE) {
if (skOpposite(d, S[1]) == 0) { S[1] = d; }
S[2] = DIR_NONE;
}
int hc = S[R0 + S[5]];
int hx = (hc - B0) % NC;
int hy = (hc - B0) / NC;
int nx = hx + skDx(S[1]);
int ny = hy + skDy(S[1]);
int tcell = S[R0 + S[6]];
int blk = skBlocked(nx, ny);
if (blk != 0) {
if (blk == 1) { skHitWall(); } else { skBiteSelf(); }
return 1;
}
int ncell = B0 + ny * NC + nx;
int grow = 0;
if (S[ncell] == 2) { grow = 1; }
if (grow == 0) {
S[tcell] = 0;
S[6] = (S[6] + 1) % RLEN;
}
S[5] = (S[5] + 1) % RLEN;
S[R0 + S[5]] = ncell;
S[ncell] = 1;
S[18] = S[18] + 1;
S[23] = ncell;
if (grow == 1) {
S[3] = S[3] + 1;
S[4] = S[4] + 1;
S[7] = skSpeed();
if (S[4] > S[20]) { S[20] = S[4]; }
ui_audio_beep(988, 60);
skLog("snake: eat score=", S[3]);
skNewFood();
}
return 1;
}
// ── 绘制 ──
// ── Drawing ──
static void skBanner(String title, String sub, int col) {
int cy = S[13] + S[10] / 2;
ui_2d_rect(S[12], cy - 52, S[9], 104, C_BOARD, 1, 0, 8);
ui_2d_line(S[12], cy - 52, S[12] + S[9], cy - 52, col, 2);
ui_2d_line(S[12], cy + 52, S[12] + S[9], cy + 52, col, 2);
ui_2d_text(S[12] + S[9] / 2, cy - 34, title, col, 22, 1);
ui_2d_text(S[12] + S[9] / 2, cy + 6, sub, C_TEXT, 13, 1);
}
static void skDraw() {
int lang = S[16];
int cell = S[11];
int i = 0;
int k = 0;
int n = S[4];
int ci = 0;
int cx = 0;
int cy = 0;
int px = 0;
int py = 0;
int col = 0;
S[24] = S[24] + 1;
ui_2d_clear(C_BG);
// HUD:分数 / 长度 / 速度条
// HUD: score / length / a speed bar
ui_2d_text(12, 10, skL("得分", "Score"), C_MUTED, 12, 0);
ui_2d_text(12, 26, skNum(S[3]), C_TEXT, 20, 0);
ui_2d_text(S[14] / 2 - 30, 10, skL("长度", "Length"), C_MUTED, 12, 0);
ui_2d_text(S[14] / 2 - 30, 26, skNum(S[4]), C_TEXT, 20, 0);
ui_2d_text(S[14] / 2 + 40, 10, skL("速度", "Speed"), C_MUTED, 12, 0);
ui_2d_rect(S[14] / 2 + 40, 30, 80, 8, C_GRID, 1, 0, 3);
ui_2d_rect(S[14] / 2 + 40, 30, 80 * (6 - S[7]) / 4, 8, C_GOLD, 1, 0, 3);
// 棋盘:外框 + 底 + 网格线
// Board: an outer frame, the floor, then the grid lines
ui_2d_rect(S[12] - 3, S[13] - 3, S[9] + 6, S[10] + 6, C_GRID, 1, 0, 6);
ui_2d_rect(S[12], S[13], S[9], S[10], C_BOARD, 1, 0, 4);
i = 1;
while (i < NC) {
ui_2d_line(S[12] + i * cell, S[13], S[12] + i * cell, S[13] + S[10], C_GRID, 1);
i = i + 1;
}
i = 1;
while (i < NR) {
ui_2d_line(S[12], S[13] + i * cell, S[12] + S[9], S[13] + i * cell, C_GRID, 1);
i = i + 1;
}
// 食物
// The food
if (S[22] >= 0) {
cx = (S[22] - B0) % NC;
cy = (S[22] - B0) / NC;
px = S[12] + cx * cell;
py = S[13] + cy * cell;
ui_2d_circle(px + cell / 2, py + cell / 2, cell / 3, C_FOOD, 1, 0);
ui_2d_circle(px + cell / 2 - cell / 8, py + cell / 2 - cell / 7, cell / 9, C_GOLD, 1, 0);
}
// 蛇身:从**尾**开始顺着环走一遍,最后一节是头
// The snake: walk the ring from the **tail**, the last entry is the head
k = 0;
while (k < n) {
ci = S[R0 + (S[6] + k) % RLEN];
cx = (ci - B0) % NC;
cy = (ci - B0) / NC;
px = S[12] + cx * cell;
py = S[13] + cy * cell;
if (k == n - 1) {
ui_2d_rect(px + 1, py + 1, cell - 2, cell - 2, C_HEAD, 1, 0, cell / 3);
// 眼睛画在**朝向**那一侧,一眼看得出蛇往哪边走
// The eye sits on the **facing** side so the direction of travel is obvious
ui_2d_circle(px + cell / 2 + skDx(S[1]) * cell / 4, py + cell / 2 + skDy(S[1]) * cell / 4,
cell / 6, C_BG, 1, 0);
} else {
col = C_BODY;
if (k % 2 == 0) { col = C_BODY2; }
ui_2d_rect(px + 2, py + 2, cell - 4, cell - 4, col, 1, 0, cell / 4);
}
k = k + 1;
}
// 暂停 / 死亡 / 通关
// Paused / dead / cleared
if (S[0] == 2) {
skBanner(skL("暂停", "Paused"), skL("SELECT 或空格继续", "SELECT or Space to resume"), C_GOLD);
}
if (S[0] == 1) {
skBanner(skL("撞了!", "Crashed!"),
skL("点屏幕或回车再来一局", "Tap or press Enter for a new game"), C_DANGER);
}
if (S[0] == 3) {
skBanner(skL("棋盘塞满了!", "Board filled!"),
skL("点屏幕或回车再来一局", "Tap or press Enter for a new game"), C_HEAD);
}
// 底部键位提示
// Bottom key hints
// 提示分**两行**写:一行在 320px 宽的窄屏上会被切掉两头(实测)
// The hints go on **two lines**: a single line gets clipped at both ends on a 320px-wide screen (measured)
ui_2d_text(S[14] / 2, S[15] - 27,
skL("方向键/WASD 转向 · 点屏幕往那边拐",
"Arrows/WASD steer - tap to turn"), C_MUTED, 10, 1);
ui_2d_text(S[14] / 2, S[15] - 14,
skL("SELECT 暂停 · 回车重开 · ESC 退出",
"SELECT pause - Enter restart - ESC quit"), C_MUTED, 10, 1);
ui_2d_present();
}
/* 自检:摆一个**合成的**局面(三节蛇绕成"头正对着自己身体"),断言 skBlocked 的分类判据。
Self-check: lay out a **synthetic** position (a three-link snake with its head facing its own body) and
为什么要有它:撞墙能靠玩出来(一路向右撞死),但"咬到自己"要先把蛇喂到 5 节以上、
assert the classification judgements of skBlocked. Why it exists: a wall crash can be played out (just go
而第二颗食物是随机的 —— 靠玩刷不出来。这一段把最容易写错的那条规则(**尾巴格例外**)
right), but biting itself needs 5+ links and the second food is random, so playing cannot produce it
变成开机能判的:结果存进 S[21],非 0 就说明有判据不成立。
repeatably. This turns the easiest-to-get-wrong rule (**the tail-cell exception**) into something decidable
它先跑后清:紧接着的 skNewGame() 会把棋盘整个抹掉,所以不会污染开局。
at boot: the result lands in S[21], and a non-zero value means a judgement failed.
It runs before, and is wiped by, skNewGame(), so the opening position is never polluted. */
static int skSelfCheck() {
int bad = 0;
int i = 0;
while (i < NC * NR) { S[B0 + i] = 0; i = i + 1; }
// 尾 (4,4) → 中 (5,4) → 头 (6,4)
// tail (4,4) -> middle (5,4) -> head (6,4)
S[skCellAt(4, 4)] = 1;
S[skCellAt(5, 4)] = 1;
S[skCellAt(6, 4)] = 1;
S[R0 + 0] = skCellAt(4, 4);
S[R0 + 1] = skCellAt(5, 4);
S[R0 + 2] = skCellAt(6, 4);
S[4] = 3;
S[5] = 2;
S[6] = 0;
// 尾格:这一拍会腾出来 ⇒ 进得去
// the tail cell: it vacates this tick -> enterable
if (skBlocked(4, 4) != 0) { bad = bad + 1; }
// 中段身体 ⇒ 咬到自己
// a mid-body cell -> a self bite
if (skBlocked(5, 4) != 2) { bad = bad + 2; }
// 头自己那一格 ⇒ 咬到自己
// the head's own cell -> a self bite
if (skBlocked(6, 4) != 2) { bad = bad + 4; }
// 空地 ⇒ 进得去
// an empty cell -> enterable
if (skBlocked(5, 5) != 0) { bad = bad + 8; }
// 左墙 ⇒ 出界
// the left wall -> off-board
if (skBlocked(-1, 4) != 1) { bad = bad + 16; }
// 右墙 ⇒ 出界
// the right wall -> off-board
if (skBlocked(NC, 4) != 1) { bad = bad + 32; }
// 下墙 ⇒ 出界
// the bottom wall -> off-board
if (skBlocked(5, NR) != 1) { bad = bad + 64; }
// 再**走一遍真实的 skStep**:五节蛇横躺在第 4 行,头 (6,4) 朝**左**(自己的脖子)⇒
// Then run the **real skStep** once: a five-link snake lying on row 4, head (6,4) facing **left** (its own neck) ⇒
// 下一拍必须判"咬到自己"(这一条只有它能验:玩的时候要 5 节以上才咬得到,
// the next tick must call it a bite. Only this can check it: in play a bite needs 5+ links,
// 而第 2 颗食物是随机的 —— 靠玩刷不出确定局面)。
// and the second food is random, so playing cannot produce a repeatable position.
i = 0;
while (i < NC * NR) { S[B0 + i] = 0; i = i + 1; }
S[skCellAt(2, 4)] = 1;
S[skCellAt(3, 4)] = 1;
S[skCellAt(4, 4)] = 1;
S[skCellAt(5, 4)] = 1;
S[skCellAt(6, 4)] = 1;
S[R0 + 0] = skCellAt(2, 4);
S[R0 + 1] = skCellAt(3, 4);
S[R0 + 2] = skCellAt(4, 4);
S[R0 + 3] = skCellAt(5, 4);
S[R0 + 4] = skCellAt(6, 4);
S[0] = 0;
S[1] = DIR_LEFT;
S[2] = DIR_NONE;
S[3] = 0;
S[4] = 5;
S[5] = 4;
S[6] = 0;
S[7] = 1;
S[8] = 0;
S[26] = 0;
S[27] = 0;
S[28] = 1;
skStep();
// 应当判死
// it must be a death
if (S[0] != 1) { bad = bad + 128; }
// 而且记的是"咬到自己"那一笔
// and the bookkeeping must say "bit itself"
if (S[27] != 1) { bad = bad + 256; }
// 不该记成"撞墙"
// it must not be counted as a wall hit
if (S[26] != 0) { bad = bad + 512; }
S[28] = 0;
S[26] = 0;
S[27] = 0;
return bad;
}
public static void main(String[] args) {
int t = 0;
int k = 0;
int tid = 0;
int sw = 0;
int sh = 0;
int nowMs = 0; // 墙钟采样
// Wall-clock sample
int acc = 0; // 运动墙钟欠账(毫秒)—— 按墙钟补步,用户 2026-10-04 口径
// Motion wall-clock debt in ms — step by wall clock (user rule, 2026-10-04)
int lastMs = 0; // 上次补步取的墙钟时刻(进主循环时初始化)
// Wall-clock time of the last step (initialized on entering the main loop)
S[16] = ui_scr_get_language();
S[21] = skSelfCheck();
// 开窗:尺寸传 0 ⇒ 交给宿主给真实可用区(开窗**之前**问到的只是估算值)
// Opening: pass size 0 ⇒ the host supplies the real available area (a **pre-open** read is only an estimate)
ui_win_open_ex(skL("贪吃蛇", "Snake"), 0, 0, 0, 0);
ui_scr_set_keep_on(1);
// 尺寸:开窗**之后**再问一次 —— 这时 `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();
if (sw <= 0) { sw = 360; }
sh = ui_scr_get_h();
if (sh <= 0) { sh = 620; }
S[14] = sw;
S[15] = sh;
skLayout();
skNewGame();
if (S[21] != 0) {
// 自检不过就用**一声闷响**报出来(频率与"死亡"撞车不合适,所以用中间那个音区)
// A failed self-check is announced with a **low thud** (a middle register, deliberately not the death tone)
ui_audio_beep(330, 300);
}
skDraw();
skLog("snake: selftest=", S[21]);
tid = ui_timer_set(33, 0);
acc = 0;
lastMs = ui_time_get_ms(); // 墙钟起点:进主循环时取(不在模块级调 syscall)
// Wall-clock origin: taken when entering the main loop (no syscall at module level)
while (ui_win_is_closed() == 0) {
t = ui_msg_fetch_wait(120);
// 这一帧该走几步:**按墙钟欠账**(各平台定时器分辨率不同 ⇒ 不能拿"收到几拍"当时间)。
// 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.
nowMs = ui_time_get_ms();
acc = acc + (nowMs - lastMs);
lastMs = nowMs;
if (acc > 33 * 8) acc = 0; // 卡太久别一次补一大串(防瞬移)
// Do not catch up in one huge burst after a long stall (anti-teleport)
while (acc >= 33) { acc = acc - 33; if (skStep() == 1) { skDraw(); } }
if (t == MSG_CLOSE) { break; }
// 屏幕变了要**重新排版**:窗口宽高就是画布坐标空间,格边长是按它算出来的。
// A screen change must **re-layout**: the window size *is* the canvas coordinate space the cell size derives from.
// ⚠ RESIZE 用**消息里带的新尺寸**、ORIENT 才回问 `ui_scr_get_w/h` —— 桌面宿主
// ⚠ RESIZE uses the size **carried in the message**; only ORIENT re-queries `ui_scr_get_w/h` — the desktop host
// 的 `ScreenArea()` 只在 `--screen` 时变过一次,`resize` 不更新它(与 `pong.java` 同一套写法)。
// only ever changes its `ScreenArea()` from `--screen`; a `resize` event does not (same shape as `pong.java`).
if (t == MSG_RESIZE) {
if (ui_msg_get_a() > 0) { S[14] = ui_msg_get_a(); }
if (ui_msg_get_b() > 0) { S[15] = ui_msg_get_b(); }
skLayout();
skDraw();
}
if (t == MSG_ORIENT) {
if (ui_scr_get_w() > 0) { S[14] = ui_scr_get_w(); }
if (ui_scr_get_h() > 0) { S[15] = ui_scr_get_h(); }
skLayout();
skDraw();
}
if (t == MSG_KEYDOWN) {
k = ui_msg_get_a();
if (k == KEY_ESC) { break; }
if (k == KEY_ENTER) {
skNewGame();
ui_audio_beep(440, 60);
skDraw();
}
if (k == KEY_SELECT || k == KEY_SPACE) { skTogglePause(); skDraw(); }
if (k == KEY_UP || k == KEY_W) { skTurn(DIR_UP); }
if (k == KEY_DOWN || k == KEY_S) { skTurn(DIR_DOWN); }
if (k == KEY_LEFT || k == KEY_A) { skTurn(DIR_LEFT); }
if (k == KEY_RIGHT || k == KEY_D) { skTurn(DIR_RIGHT); }
}
if (t == MSG_TOUCHDOWN || t == MSG_TOUCHMOVE) {
if (S[0] == 1 || S[0] == 3) {
if (t == MSG_TOUCHDOWN) {
skNewGame();
ui_audio_beep(440, 60);
skDraw();
}
} else {
if (S[0] == 2) { skTogglePause(); skDraw(); }
skAim(ui_msg_get_a(), ui_msg_get_b());
}
}
if (t == MSG_TIMER) {
// 定时器**只负责唤醒**(推进在上面按墙钟欠账走)
// The timer **only wakes us up** — the advance happens above, by wall-clock debt
}
}
ui_timer_kill(tid);
ui_scr_set_keep_on(0);
ui_win_close();
// 收尾报告(给脚本化验收用):一局里发生了什么,一次看全
// Closing report (for scripted acceptance): what happened, in one place
skLog("snake: frames=", S[24]);
skLog("snake: games=", S[19]);
skLog("snake: score=", S[3]);
skLog("snake: len=", S[4]);
skLog("snake: maxlen=", S[20]);
skLog("snake: moves=", S[18]);
skLog("snake: state=", S[0]);
skLog("snake: wall=", S[26]);
skLog("snake: self=", S[27]);
skLog("snake: selftest=", S[21]);
System.out.println("snake: done");
}
}