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

java/app/sokoban.java · 34823 字节 · 下载原文

// 推箱子 —— 用 **Java** 写的手机游戏

// Sokoban — a phone game written in **Java**

//

// 玩法:手指划 / 点网格把小人推向箱子,把**所有**箱子推到目标点上就过关;共 3 关。

// Gameplay: drag / tap the grid to walk the player into the boxes; push **every** box onto a goal to clear the level; 3 levels.

// 屏幕下方是十字方向键 + 灵退 + 重来三个按钮;关卡过关后点屏幕任意处进下一关。

// The bottom of the screen holds a D-pad plus Undo and Reset; after a clear, tap anywhere to go on.

// 接物理键盘时可用方向键 / WASD 移动、U 或 Z 灵退、R 重来、N 跳过本关、ESC 退出。

// With a physical keyboard: arrows / WASD to move, U or Z to undo, R to reset, N to skip a level, ESC to quit.

//

// ◆ 手机那套 UI

// ◆ The phone UI

//

// 开窗 / 绘图 / 输入 / 定时器是 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">` 挂上来。

// hooked up by `<Language Name="java" Libs="vmlui.vml,tty.vml">` in `vmltool.config.xml`.

//

// ◆ 五条写法要求(不是偏好,都是这个前端逼出来的)

// ◆ Five style requirements (not preferences — every one of them is forced by this front end)

//

//   ① 调库函数必须声明成 `static native` —— `asm()` 已从 Java 前端移除

//   ① Library calls must be declared `static native` — `asm()` has been removed from the Java frontend

//      (`CodeGenerator.Expressions.cs:447` 注明「仅限 C/ObjC/C++」),不声明就发 `CALL method_xxx`。

//      (`CodeGenerator.Expressions.cs:447` notes "C/ObjC/C++ only"); without it the call becomes `CALL method_xxx`.

//   ② 颜色写**负数十进制**(`-65536` = `0xFFFF0000`)—— 能跑就别动。~~避开十六进制字面量在词法层的解读差异~~

//   ② Colors are **negative decimals** (`-65536` = `0xFFFF0000`) — leave working code alone. ~~dodging the lexer's differing view of hex literals~~

//      —— 2026-10-04 复验十六进制**已修**(`0xFF101018` 直接得 -15724520、`0xFF` 得 255);新写的代码可以直接用十六进制。

//      -- re-verified 2026-10-04: hex is **fixed** (`0xFF101018` gives -15724520 directly, `0xFF` gives 255); new code may use hex directly.

//   ③④⑤ **这三条踩到的缺陷在 2026-09-30 当天全部修掉了**(台账 `FRONTEND_DEFECTS.md` 的 Java 小节):

//   ③④⑤ **All three defects hit here were fixed on 2026-09-30** (the Java section of `FRONTEND_DEFECTS.md`):

//         · `static int K = 7;` 读出 1028(地址)—— 真身:数据段标量读取用了"取地址";

// `static int K = 7;` read back 1028 (an address) -- the data-section scalar read used "take the address";

//         · `static int[] A = new int[N]` **一个词都不占** ⇒ `A[i]` 从 `A[0]` 起就踩字符串字面量

// `static int[] A = new int[N]` reserved **not one word**, so `A[i]` stomped the string literals from `A[0]` on

//           —— 那正是 `catch.java` 截图里「得分 / 最高」变成 `N` / `4` 的原因;

// -- that is exactly why `catch.java` rendered its labels as `N` / `4`;

//         · `new int[N]` 的**堆分配**把 `syscall #40` 的返回值丢掉了(`MOVE R0, R1` 覆盖成 R1),

// and the `new int[N]` **heap allocation** threw away `syscall #40`'s return value (`MOVE R0, R1` overwrote it),

//           于是数组落在无关内存上、元素也不是 0。三个是一族,见台账。

//         (`static int K = 7;` read back 1028 — the data-section scalar read used "take the address";

//          `static int[] A = new int[N]` reserved **not one word**, so `A[i]` stomped the string literals from `A[0]` on —

//          that is exactly why `catch.java` rendered "得分"/"最高" as `N`/`4`; and the `new int[N]` **heap allocation**

//          threw away `syscall #40`'s return value (`MOVE R0, R1` overwrote it with R1), so arrays landed on unrelated

//          memory and were not zeroed. One family; see the ledger.)

//

//   本文件**保持"状态装在 `main` 的局部数组里、一路当参数传"的写法**:它是修之前写的、修之后又逐条复验过

// This file **keeps the "state in `main`'s local array, passed down as a parameter" style**: it was written before

//   (三个关卡 + 撤销 + 触摸方向键都有脚本化判据),而且这个写法本身**一直是对的**、也仍然是好写法。

//   This file **keeps the "state in `main`'s local array, passed down as a parameter" style**: it was written before the

//   fixes and re-verified after them (scripted checks for all three levels, undo, and the touch D-pad), and that style

//   was always correct and still is.

//   ⚠ **想照"修完之后的自然 Java 写法"写**(`static int[] S` 当状态、`static final int` 当常量、`S.length`)

// **For the natural post-fix Java style** (`static int[] S` for state, `static final int` constants) see

//   ⇒ 看同目录的 **`pong.java`**,那个是照修完的前端写的。

//   ⚠ **For the natural post-fix Java style** (`static int[] S` for state, `static final int` constants, `S.length`)

//   see **`pong.java`** in the same directory — it is written against the repaired front end.

//

// ◆ 顺带被这份代码用到的、**能过**的前端特性

// ◆ Front-end features this code exercises on purpose, **all of which pass**

//

//   `while` / `if` / `&&` `||` `!` / 三目 / 算术与位运算(`+ - * / % & | <<`)/ 一元 `-`

//   `while` / `if` / `&&` `||` `!` / ternary / arithmetic and bit operators (`+ - * / % & | <<`) / unary `-`

//   **一维数组**(局部、当参数、读改写)/ **返回 `String` 的方法** / `break` / `continue`

//   **one-dimensional arrays** (local, as parameter, read-modify-write) / **a method returning `String`** / `break` / `continue`

//

// ⚠ 特性的**准不准**别凭印象:下面这张表是逐条实测的(一条一个最小探针),

// Do not go by impression about which features work: the table below was measured one minimal probe at a time,

//   完整台账在 `FRONTEND_DEFECTS.md` 的 Java 小节。

// ⚠ Do not go by impression about which features work: the table below is measured feature by feature

//   (one minimal probe each). The full ledger is the Java section of `FRONTEND_DEFECTS.md`.

//   · **已经修好、现在可以用了**:`this.x`、`static` 标量字段、`static int[] A`、`new int[N]`、

// **Fixed and now usable**: `this.x`, static scalar fields, `static int[] A`, `new int[N]`,

//     `.length`、增强 `for (... : ...)`、`String[] a = new String[n]`、嵌套类、`~x`、负数字面量当操作数

//   · **Fixed and now usable**: `this.x`, `static` scalar fields, `static int[] A`, `new int[N]`,

//     `.length`, enhanced `for (... : ...)`, `String[] a = new String[n]`, nested classes, `~x`, negative literals as operands

//   · **语言专用库建起来之后又能用了**(`Lib/shared/src/builtins_java.c`):`ArrayList`/`HashMap`/

//     `StringBuilder`、`String.format`/`split`/`length()`/`charAt()`、`Integer.parseInt`、

//     `"n=" + 7` 的 int→String 拼接 —— ⚠ 前提是 `modules.json` + `vmltool.config.xml` 挂上它。

//   · **Usable again once the language library exists** (`Lib/shared/src/builtins_java.c`):

//     `ArrayList`/`HashMap`/`StringBuilder`, `String.format`/`split`/`length()`/`charAt()`,

//     `Integer.parseInt`, int→String `"n=" + 7` — requires the module to be hooked up in the config.

//   · ⚠ **仍然不能用**(台账里仍是 🔴):实例字段与自定义类的方法派发(没有对象模型)、

// **Still unusable** (still red in the ledger): instance fields and method dispatch on user classes,

//     `try`-`catch`/`throw new`、`enum`、可变参数 `int...`(静默算错)、`extends`、`int[][]` 的 `.length`

//   · ⚠ **Still unusable** (still 🔴 in the ledger): instance fields and method dispatch on user classes (no object model),

//     `try`-`catch`/`throw new`, `enum`, varargs `int...` (silently wrong), `extends`, `.length` on `int[][]`

//     ✅ 2026-10-04 复验**仍成立**:`throw new Exception(...)` 报「未定义的函数 'Exception'」;`enum Dir` 报「未声明的变量 'Dir'」;

//     ✅ Re-verified 2026-10-04, **still holds**: `throw new Exception(...)` errors "undefined function 'Exception'"; `enum Dir` errors "undeclared variable 'Dir'";

//        `sum(int... xs)` 编得过但 `sum(1,2,3,4)` 得 **0**(应 10);`class Der extends Base` 报「未定义的函数 'Der'」;

//        `sum(int... xs)` compiles but `sum(1,2,3,4)` gives **0** (should be 10); `class Der extends Base` errors "undefined function 'Der'";

//        `int[][]` 的 `.length` 得 **0**(`g[1][2]` 读回 7 是对的)。

//        `.length` on `int[][]` gives **0** (`g[1][2]` reads back 7 correctly).

//

// 跑法:手机 `vml run examples/java/sokoban.java`;

// How to run: phone `vml run examples/java/sokoban.java`;

//       桌面 `vmlcli Examples/java/sokoban.java --screen 411x726 --frames /tmp/fr`

//       desktop `vmlcli Examples/java/sokoban.java --screen 411x726 --frames /tmp/fr`


#include "../../../Lib/java/vml_ui.java"
public class Sokoban {

    // ── 窗体 / 绘图 / 输入 / 声音(源:Lib/c/vml_ui.h;这里只能写字面量)──

    // ── Window / drawing / input / sound (source: Lib/c/vml_ui.h; literals only here) ──

    static native int  ui_scr_get_w();
    static native int  ui_scr_get_h();
    static native int  ui_scr_get_language();
    static native int  ui_win_open_ex(String t, int w, int h, int rot, int gamepad);
    static native int  ui_win_is_closed();
    static native int  ui_win_close();
    static native void ui_2d_clear(int c);
    static native void ui_2d_rect(int x, int y, int w, int h, int c, int fill, int lw, int r);
    static native void ui_2d_circle(int cx, int cy, int r, int c, int fill, int lw);
    static native void ui_2d_line(int x1, int y1, int x2, int y2, int c, int lw);
    static native void ui_2d_text(int x, int y, String s, int c, int size, int anchor);
    static native void ui_2d_present();
    static native int  ui_msg_fetch_wait(int timeout_ms);
    static native int  ui_msg_get_a();
    static native int  ui_msg_get_b();
    static native int  ui_timer_set(int interval_ms, int tag);
    static native int  ui_timer_kill(int id);
    static native void ui_audio_beep(int freq, int ms);
    static native void ui_scr_set_keep_on(int on);

    /* 状态数组 S 的下标(全是字面量,见文件头 ③)
       The S slot map (all literals; see note 3 in the file header)
       0 关卡(1..3)      1 玩家x      2 玩家y      3 步数        4 推动次数
       0 level (1..3); 1 player x; 2 player y; 3 steps; 4 pushes
       5 cleared flag; 6 undo depth; 7 direction held (-1 = none); 8 hold counter
       9 which control is held (0 none, 1..4 the D-pad, 5 undo, 6 reset); 10 touch x; 11 touch y
       12 clear animation counter; 13 frame counter; 14 screen width; 15 screen height; 16 was the last move a failure?
       32..111 the 8x10 grid (0 empty / 1 wall / 2 box / 3 goal / 4 box already on a goal)
       112..175 the undo stack (each entry = direction + 4 * whether it pushed), up to 64 moves */
    // 数组长度 192 = 标量 32 + 网格 80 + 撤销栈 64 + 余量 16

    // Array length 192 = 32 scalars + 80 grid + 64 undo + 16 slack


    // ── 纯逻辑:网格读写 ──

    // ── Pure logic: grid access ──

    // grid index = 32 + y*8 + x (8 = GW and 10 = GH, written as literals)

    static int cell(int[] S, int x, int y) {
        return S[32 + y * 8 + x];
    }

    static void setCell(int[] S, int x, int y, int v) {
        S[32 + y * 8 + x] = v;
    }

    static void zero(int[] S, int n) {
        int i = 0;
        while (i < n) { S[i] = 0; i = i + 1; }
    }

    static int isWall(int[] S, int x, int y) {
        if (x < 0 || x > 7 || y < 0 || y > 9) { return 1; }
        if (cell(S, x, y) == 1) { return 1; }
        return 0;
    }

    static int isBox(int[] S, int x, int y) {
        int c = cell(S, x, y);
        if (c == 2 || c == 4) { return 1; }
        return 0;
    }

    // direction d: 0 up, 1 down, 2 left, 3 right. dx/dy come from if chains rather than arrays (see the header)

    static int dxOf(int d) {
        if (d == 2) { return -1; }
        if (d == 3) { return 1; }
        return 0;
    }

    static int dyOf(int d) {
        if (d == 0) { return -1; }
        if (d == 1) { return 1; }
        return 0;
    }

    // ── 关卡 ──

    // ── Levels ──

    // 三关都是手排的 8x10 图;边界墙用循环画,内部墙 / 箱子 / 目标点逐格点。

    // All three levels are hand-laid 8x10 maps; the border ring is drawn with loops, interior walls / boxes / goals cell by cell.

    static void loadLevel(int[] S, int lv) {
        int i = 0;
        int x = 0;
        int y = 0;

        S[0] = lv;
        S[3] = 0;
        S[4] = 0;
        S[5] = 0;
        S[6] = 0;
        S[7] = -1;
        S[8] = 0;
        S[9] = 0;
        S[12] = 0;

        i = 32;
        while (i < 112) { S[i] = 0; i = i + 1; }

        // 边界墙

        // Border ring

        x = 0;
        while (x < 8) { setCell(S, x, 0, 1); setCell(S, x, 9, 1); x = x + 1; }
        y = 0;
        while (y < 10) { setCell(S, 0, y, 1); setCell(S, 7, y, 1); y = y + 1; }

        if (lv == 1) {
            // 1 个箱子:目标点在正上方,往上推 3 下

            // One box: the goal is straight above, push it up three times

            setCell(S, 3, 2, 3);
            setCell(S, 3, 5, 2);
            S[1] = 3; S[2] = 6;
            return;
        }

        if (lv == 2) {
            // 2 个箱子 + 中间一道短墙(x=3,4 y=4)

            // Two boxes plus a short wall in the middle (x=3,4 at y=4)

            setCell(S, 2, 2, 3);
            setCell(S, 5, 2, 3);
            setCell(S, 3, 4, 1);
            setCell(S, 4, 4, 1);
            setCell(S, 2, 6, 2);
            setCell(S, 5, 6, 2);
            S[1] = 3; S[2] = 8;
            return;
        }

        // 第 3 关:3 个箱子 + 一处拐角墙(x=5 y=5)

        // Level 3: three boxes plus a corner wall (x=5 at y=5)

        setCell(S, 2, 1, 3);
        setCell(S, 3, 1, 3);
        setCell(S, 4, 1, 3);
        setCell(S, 5, 5, 1);
        setCell(S, 2, 3, 2);
        setCell(S, 4, 3, 2);
        setCell(S, 3, 7, 2);
        S[1] = 3; S[2] = 8;
    }

    // ── 走一步 / 撤销 ──

    // ── Move / undo ──

    static int boxOnGoal(int[] S, int x, int y) {
        if (cell(S, x, y) == 4) { return 1; }
        return 0;
    }

    // returns 0 nothing moved / 1 moved / 2 pushed a box / 3 pushed it straight onto a goal

    static int step(int[] S, int d) {
        int dx = dxOf(d);
        int dy = dyOf(d);
        int px = S[1];
        int py = S[2];
        int nx = px + dx;
        int ny = py + dy;
        int pushed = 0;

        if (isWall(S, nx, ny) == 1) { return 0; }

        if (isBox(S, nx, ny) == 1) {
            int bx = nx + dx;
            int by = ny + dy;
            if (isWall(S, bx, by) == 1) { return 0; }
            if (isBox(S, bx, by) == 1) { return 0; }
            if (cell(S, bx, by) == 3) { setCell(S, bx, by, 4); } else { setCell(S, bx, by, 2); }
            if (cell(S, nx, ny) == 4) { setCell(S, nx, ny, 3); } else { setCell(S, nx, ny, 0); }
            pushed = 1;
            S[4] = S[4] + 1;
        }

        S[1] = nx;
        S[2] = ny;
        S[3] = S[3] + 1;

        if (S[6] < 64) {
            S[112 + S[6]] = d + 4 * pushed;
            S[6] = S[6] + 1;
        }

        if (pushed == 1) {
            if (boxOnGoal(S, nx + dx, ny + dy) == 1) { return 3; }
            return 2;
        }
        return 1;
    }

    static void undo(int[] S) {
        int rec = 0;
        int d = 0;
        int pushed = 0;
        int dx = 0;
        int dy = 0;
        int px = 0;
        int py = 0;

        if (S[6] <= 0) { return; }
        S[6] = S[6] - 1;
        rec = S[112 + S[6]];
        d = rec % 4;
        pushed = rec / 4;
        dx = dxOf(d);
        dy = dyOf(d);
        px = S[1];
        py = S[2];

        if (pushed == 1) {
            // 箱子从 (px+dx,py+dy) 退回小人现在站的那一格

            // The box goes from (px+dx,py+dy) back to the cell the player stands on

            if (cell(S, px, py) == 3) { setCell(S, px, py, 4); } else { setCell(S, px, py, 2); }
            if (cell(S, px + dx, py + dy) == 4) { setCell(S, px + dx, py + dy, 3); } else { setCell(S, px + dx, py + dy, 0); }
            S[4] = S[4] - 1;
        }

        S[1] = px - dx;
        S[2] = py - dy;
        if (S[3] > 0) { S[3] = S[3] - 1; }
    }

    // 还有没有"没在目标点上的箱子"

    // Is any box still off a goal?

    static int allBoxesHome(int[] S) {
        int i = 32;
        while (i < 112) {
            if (S[i] == 2) { return 0; }
            i = i + 1;
        }
        return 1;
    }

    // ── 画数字 ──

    // ── Drawing digits ──

    // ⚠ 数字一位一位画 —— 这是本仓既有的写法,**能跑就别动**。

    // ⚠ Digits are drawn one at a time -- the established style here; **leave working code alone**.

    //   ~~`int` → `String` 的 `+` 拼接在本前端是坏的(拼出来是空串)~~ —— 2026-10-04 复验**已修**(`"n=" + 7` 得 `n=7`);

    //   ~~`int` → `String` via `+` is broken here (it yields an empty string)~~ -- **fixed**, re-verified 2026-10-04 (`"n=" + 7` gives `n=7`);

    //   新写的代码可以直接用 `"" + v` 拼。

    //   new code may concatenate directly with `"" + v`.

    static String dg(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 void num(int v, int x, int y, int c, int size) {
        String a = "0";
        String b = "0";
        String e = "0";
        if (v < 0) { v = 0; }
        if (v > 999) { v = 999; }
        if (v > 99) {
            a = dg(v / 100);
            b = dg((v / 10) % 10);
            e = dg(v % 10);
            ui_2d_text(x, y, a, c, size, 0);
            ui_2d_text(x + size / 2, y, b, c, size, 0);
            ui_2d_text(x + size, y, e, c, size, 0);
            return;
        }
        if (v > 9) {
            a = dg(v / 10);
            b = dg(v % 10);
            ui_2d_text(x, y, a, c, size, 0);
            ui_2d_text(x + size / 2, y, b, c, size, 0);
            return;
        }
        a = dg(v);
        ui_2d_text(x, y, a, c, size, 0);
    }

    // ── 排版 ──

    // ── Layout ──

    // 几何全部由屏宽推出来(唯一真源 = S[14]/S[15]),一处写死的画布尺寸都没有。

    // All geometry is derived from the screen width (the single source of truth is S[14]/S[15]); no canvas size is written down.

    static int cellSize(int[] S) {
        int v = (S[14] - 24) / 8;
        if (v < 12) { v = 12; }
        return v;
    }

    static int gridX0(int[] S) {
        return (S[14] - cellSize(S) * 8) / 2;
    }

    static int gridY0(int[] S) {
        return 108;
    }

    static int padSize(int[] S) {
        int v = S[14] / 10;
        if (v < 30) { v = 30; }
        return v;
    }

    static int padCx(int[] S) {
        return S[14] / 4;
    }

    static int padCy(int[] S) {
        return S[15] - 84;
    }

    static int undoX(int[] S) {
        return S[14] / 2 + 8;
    }

    static int undoY(int[] S) {
        return S[15] - 150;
    }

    static int undoW(int[] S) {
        return S[14] / 2 - 24;
    }

    // 触摸点落在哪个控件上:0 空 1 上 2 下 3 左 4 右 5 撤销 6 重来 7 网格

    // Which control a touch landed on: 0 none, 1 up 2 down 3 left 4 right 5 undo 6 reset 7 grid

    static int hitTest(int[] S, int x, int y) {
        int s = padSize(S);
        int cx = padCx(S);
        int cy = padCy(S);
        int h = s / 2;
        if (x > cx - h && x < cx + h && y > cy - s - h && y < cy - s + h) { return 1; }
        if (x > cx - h && x < cx + h && y > cy + s - h && y < cy + s + h) { return 2; }
        if (x > cx - s - h && x < cx - s + h && y > cy - h && y < cy + h) { return 3; }
        if (x > cx + s - h && x < cx + s + h && y > cy - h && y < cy + h) { return 4; }
        if (x > undoX(S) && x < undoX(S) + undoW(S) && y > undoY(S) && y < undoY(S) + 48) { return 5; }
        if (x > undoX(S) && x < undoX(S) + undoW(S) && y > undoY(S) + 58 && y < undoY(S) + 106) { return 6; }
        if (y > gridY0(S) && y < gridY0(S) + cellSize(S) * 10 && x > gridX0(S) && x < gridX0(S) + cellSize(S) * 8) { return 7; }
        return 0;
    }

    // 点网格 ⇒ 朝那一格走一步(哪一边差得多就往哪边走)

    // Tapping the grid steps toward that cell (along the axis with the larger delta)

    static void tapGrid(int[] S, int x, int y) {
        int cs = cellSize(S);
        int gx = (x - gridX0(S)) / cs;
        int gy = (y - gridY0(S)) / cs;
        int ax = 0;
        int ay = 0;
        int r = 0;
        if (gx < 0 || gx > 7 || gy < 0 || gy > 9) { return; }
        ax = gx - S[1];
        ay = gy - S[2];
        if (ax < 0) { ax = 0 - ax; }
        if (ay < 0) { ay = 0 - ay; }
        if (ax == 0 && ay == 0) { return; }
        if (ax >= ay) {
            if (gx > S[1]) { r = step(S, 3); } else { r = step(S, 2); }
        } else {
            if (gy > S[2]) { r = step(S, 1); } else { r = step(S, 0); }
        }
        beepFor(S, r);
    }

    // 一次事件只发一个音(ui_audio_beep 是单通道的)

    // One sound per event (ui_audio_beep is single-channel)

    static void beepFor(int[] S, int r) {
        if (r == 3) { ui_audio_beep(1319, 90); return; }
        if (r == 2) { ui_audio_beep(880, 55); return; }
        if (r == 1) { ui_audio_beep(660, 35); return; }
        ui_audio_beep(196, 70);
    }

    static void doControl(int[] S, int r) {
        int res = 0;
        if (r == 5) {
            if (S[6] > 0) { undo(S); ui_audio_beep(392, 50); } else { ui_audio_beep(196, 60); }
            return;
        }
        if (r == 6) {
            loadLevel(S, S[0]);
            ui_audio_beep(262, 90);
            return;
        }
        if (r >= 1 && r <= 4) {
            res = step(S, r - 1);
            beepFor(S, res);
            return;
        }
    }

    // ── 画一帧 ──

    // ── Draw one frame ──

    static void draw(int[] S) {
        int lang = S[16];
        int sw = S[14];
        int sh = S[15];
        int cs = cellSize(S);
        int gx0 = gridX0(S);
        int gy0 = gridY0(S);
        int s = padSize(S);
        int cx = padCx(S);
        int cy = padCy(S);
        int h = s / 2;
        int x = 0;
        int y = 0;
        int c = 0;
        int i = 0;
        String t = "0";

        ui_2d_clear(-15724520);

        // 顶栏

        // Top bar

        ui_2d_text(sw / 2, 14, lang == 0 ? "推箱子" : "Sokoban", -1513232, 20, 1);
        ui_2d_text(12, 52, lang == 0 ? "关卡" : "Level", -6643536, 13, 0);
        num(S[0], 58, 52, -8096, 13);
        ui_2d_text(96, 52, lang == 0 ? "步数" : "Moves", -6643536, 13, 0);
        num(S[3], 148, 52, -1513232, 13);
        ui_2d_text(sw - 12, 52, lang == 0 ? "推到点上的箱数" : "Boxes home", -6643536, 13, 2);

        // 网格

        // The grid

        y = 0;
        while (y < 10) {
            x = 0;
            while (x < 8) {
                c = cell(S, x, y);
                if (c == 1) {
                    ui_2d_rect(gx0 + x * cs, gy0 + y * cs, cs - 1, cs - 1, -12829620, 1, 0, 3);
                    ui_2d_rect(gx0 + x * cs, gy0 + y * cs, cs - 1, 4, -11513750, 1, 0, 2);
                } else {
                    ui_2d_rect(gx0 + x * cs, gy0 + y * cs, cs - 1, cs - 1, -14474450, 1, 0, 3);
                    if (c == 3 || c == 4) {
                        ui_2d_circle(gx0 + x * cs + cs / 2, gy0 + y * cs + cs / 2, cs / 6 + 2, -2052032, 1, 0);
                    }
                    if (c == 2) {
                        ui_2d_rect(gx0 + x * cs + 4, gy0 + y * cs + 4, cs - 9, cs - 9, -4161472, 1, 0, 4);
                        ui_2d_rect(gx0 + x * cs + 8, gy0 + y * cs + 8, cs - 17, cs - 17, -2069424, 1, 0, 3);
                    }
                    if (c == 4) {
                        ui_2d_rect(gx0 + x * cs + 4, gy0 + y * cs + 4, cs - 9, cs - 9, -10436512, 1, 0, 4);
                        ui_2d_rect(gx0 + x * cs + 8, gy0 + y * cs + 8, cs - 17, cs - 17, -11483016, 1, 0, 3);
                    }
                }
                x = x + 1;
            }
            y = y + 1;
        }

        // 小人

        // The player

        ui_2d_circle(gx0 + S[1] * cs + cs / 2, gy0 + S[2] * cs + cs / 2, cs / 2 - 5, -11409298, 1, 0);
        ui_2d_circle(gx0 + S[1] * cs + cs / 2 - 4, gy0 + S[2] * cs + cs / 3, 2, -1513232, 1, 0);
        ui_2d_circle(gx0 + S[1] * cs + cs / 2 + 4, gy0 + S[2] * cs + cs / 3, 2, -1513232, 1, 0);

        // 方向键

        // D-pad

        ui_2d_rect(cx - h, cy - s - h, s, s, -13684930, 1, 0, 6);
        ui_2d_rect(cx - h, cy + s - h, s, s, -13684930, 1, 0, 6);
        ui_2d_rect(cx - s - h, cy - h, s, s, -13684930, 1, 0, 6);
        ui_2d_rect(cx + s - h, cy - h, s, s, -13684930, 1, 0, 6);
        ui_2d_text(cx, cy - s - 2, "^", -1513232, 18, 1);
        ui_2d_text(cx, cy + s + 6, "v", -1513232, 18, 1);
        ui_2d_text(cx - s, cy + 6, "<", -1513232, 18, 1);
        ui_2d_text(cx + s, cy + 6, ">", -1513232, 18, 1);

        // 撤销 / 重来

        // Undo / reset

        ui_2d_rect(undoX(S), undoY(S), undoW(S), 48, -13684930, 1, 0, 8);
        ui_2d_text(undoX(S) + undoW(S) / 2, undoY(S) + 24, lang == 0 ? "撤销" : "UNDO", -12529440, 15, 1);
        ui_2d_rect(undoX(S), undoY(S) + 58, undoW(S), 48, -13684930, 1, 0, 8);
        ui_2d_text(undoX(S) + undoW(S) / 2, undoY(S) + 82, lang == 0 ? "重来" : "RESET", -2069424, 15, 1);

        // 提示行

        // Hint line

        ui_2d_text(12, sh - 20, lang == 0 ? "点网格 / 划方向走 · U 撤销 · R 重来" : "Tap grid / D-pad · U undo · R reset", -6643536, 11, 0);

        // 通关横幅

        // Clear banner

        if (S[5] == 1) {
            ui_2d_rect(0, gy0 + cellSize(S) * 4 - 30, sw, 92, -15066588, 1, 0, 0);
            ui_2d_text(sw / 2, gy0 + cellSize(S) * 4 - 12, lang == 0 ? "过关!" : "Level clear!", -8096, 24, 1);
            if (S[0] < 3) {
                ui_2d_text(sw / 2, gy0 + cellSize(S) * 4 + 22, lang == 0 ? "点一下进下一关" : "Tap for the next level", -1513232, 14, 1);
            } else {
                ui_2d_text(sw / 2, gy0 + cellSize(S) * 4 + 22, lang == 0 ? "三关全部通关!点一下重来" : "All three levels done! Tap to replay", -1513232, 14, 1);
            }
        }

        // 通关闪烁的小方框(帧号驱动,不需要额外状态)

        // A blinking pip while cleared (driven by the frame counter, no extra state needed)

        if (S[5] == 1 && S[13] % 20 < 10) {
            ui_2d_rect(sw / 2 - 5, gy0 - 14, 10, 10, -8096, 1, 0, 2);
        }

        ui_2d_present();
    }

    public static void main(String[] args) {
        int[] S = new int[192];
        int i = 0;
        int sw = 0;
        int sh = 0;
        int lang = 0;
        int tid = 0;
        int t = 0;
        int k = 0;
        int h = 0;
        int running = 1;

        zero(S, 192);
        S[16] = ui_scr_get_language();
        S[7] = -1;
        lang = S[16];

        // 开窗:尺寸传 0 ⇒ 交给宿主给真实可用区(开窗**之前**问到的只是估算值)

        // Opening: pass size 0 ⇒ the host supplies the real available area (a **pre-open** read is only an estimate)

        ui_win_open_ex(lang == 0 ? "推箱子" : "Sokoban", 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();
        sh = ui_scr_get_h();
        if (sw <= 0) { sw = 360; }
        if (sh <= 0) { sh = 620; }
        S[14] = sw;
        S[15] = sh;

        loadLevel(S, 1);
        draw(S);

        tid = ui_timer_set(50, 0);

        while (running == 1 && ui_win_is_closed() == 0) {
            t = ui_msg_fetch_wait(200);
            if (t == 10) { running = 0; }
            if (t == 12) { S[15] = ui_scr_get_h(); S[14] = ui_scr_get_w(); draw(S); }
            if (t == 11) { S[14] = ui_msg_get_a(); S[15] = ui_msg_get_b(); draw(S); }

            if (t == 1) {
                k = ui_msg_get_a();
                if (k == 27) { running = 0; }
                if (S[5] == 1) {
                    // 通关画面上按任意键进下一关

                    // On the clear banner any key advances

                    if (S[0] < 3) { loadLevel(S, S[0] + 1); } else { loadLevel(S, 1); }
                    draw(S);
                } else {
                    if (k == 37 || k == 65) { beepFor(S, step(S, 2)); draw(S); }
                    if (k == 39 || k == 68) { beepFor(S, step(S, 3)); draw(S); }
                    if (k == 38 || k == 87) { beepFor(S, step(S, 0)); draw(S); }
                    if (k == 40 || k == 83) { beepFor(S, step(S, 1)); draw(S); }
                    if (k == 85 || k == 90) {
                        if (S[6] > 0) { undo(S); ui_audio_beep(392, 50); } else { ui_audio_beep(196, 60); }
                        draw(S);
                    }
                    if (k == 82) { loadLevel(S, S[0]); ui_audio_beep(262, 90); draw(S); }
                    if (k == 78) {
                        if (S[0] < 3) { loadLevel(S, S[0] + 1); } else { loadLevel(S, 1); }
                        ui_audio_beep(523, 80);
                        draw(S);
                    }
                }
            }

            if (t == 6) {
                // 按下:命中哪个控件就记下来(按住会重复),点网格就直接走一步

                // Press: remember which control was hit (holding repeats); a grid tap steps at once

                S[10] = ui_msg_get_a();
                S[11] = ui_msg_get_b();
                h = hitTest(S, S[10], S[11]);
                if (S[5] == 1) {
                    if (S[0] < 3) { loadLevel(S, S[0] + 1); } else { loadLevel(S, 1); }
                    ui_audio_beep(1047, 120);
                    draw(S);
                } else {
                    if (h == 7) {
                        tapGrid(S, S[10], S[11]);
                        draw(S);
                        S[9] = 0;
                    } else {
                        S[9] = h;
                        S[8] = 0;
                        doControl(S, h);
                        draw(S);
                    }
                }
            }

            if (t == 7) {
                // 划动:只在按着的时候跟手;跟到别的控件上就换控件

                // Drag: follows the finger only while held; moving onto another control switches to it

                if (S[5] == 0) {
                    S[10] = ui_msg_get_a();
                    S[11] = ui_msg_get_b();
                    h = hitTest(S, S[10], S[11]);
                    if (h != S[9]) {
                        S[9] = h;
                        S[8] = 0;
                        if (h >= 1 && h <= 4) { doControl(S, h); draw(S); }
                        if (h == 7) { tapGrid(S, S[10], S[11]); draw(S); }
                    }
                }
            }

            if (t == 8) {
                S[9] = 0;
                S[8] = 0;
            }

            if (t == 9) {
                S[13] = S[13] + 1;
                // 按住方向键 / 撤销键:每 5 拍重复一次

                // Holding a D-pad / undo key repeats every 5 ticks

                if (S[9] >= 1 && S[9] <= 5) {
                    S[8] = S[8] + 1;
                    if (S[8] % 5 == 0) { doControl(S, S[9]); }
                }
                if (S[5] == 1) { S[12] = S[12] + 1; }
                // 过关判定只在这里做一次,避免一步多次判定

                // The clear check runs exactly here, so one step cannot be counted twice

                if (S[5] == 0 && allBoxesHome(S) == 1 && S[4] > 0) {
                    S[5] = 1;
                    ui_audio_beep(1047, 220);
                }
                draw(S);
            }
        }

        ui_timer_kill(tid);
        ui_scr_set_keep_on(0);
        ui_win_close();

        // 给无头验证留确定性判据(图形只能肉眼看,这几个数能自动比)

        // Deterministic checkpoints for headless verification (graphics can only be eyeballed, but these numbers can be compared automatically)

        System.out.print("sokoban: level=");   System.out.println(S[0]);
        System.out.print("sokoban: moves=");   System.out.println(S[3]);
        System.out.print("sokoban: pushes=");  System.out.println(S[4]);
        System.out.print("sokoban: solved=");  System.out.println(S[5]);
        System.out.print("sokoban: undos=");   System.out.println(S[6]);
        System.out.print("sokoban: player=");
        System.out.print(S[1]);
        System.out.print(",");
        System.out.println(S[2]);
        System.out.print("sokoban: boxes-home="); System.out.println(allBoxesHome(S));
        System.out.print("sokoban: frames=");  System.out.println(S[13]);
        System.out.println("sokoban: done");
    }
}