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