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

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