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

java/app/pong.java · 28980 字节 · 下载原文

// 双人乒乓 —— 用 **Java** 写的手机游戏

// Two-player Pong — a phone game written in **Java**

//

// 玩法:左右两块挡板把球挡回去,球每被挡一次就**快一档**;先到 7 分的一方赢。

// Gameplay: the two paddles bounce the ball back; every paddle hit makes the ball **one notch faster**; first to 7 wins.

// 触摸:**上半屏**拖动 = 左边那块板(P1),**下半屏**拖动 = 右边那块板(P2)—— 两个人在同一块屏上各占一半。

// Touch: drag in the **top half** for the left paddle (P1), the **bottom half** for the right paddle (P2) —

//        two players share one screen, one half each.

// 半屏里的手指位置按**整块球场的高度**等比映射(半个屏推满 = 打满全场),所以一个人也能玩两个板。

// The finger position inside a half maps **proportionally onto the whole court's height** (pushing through a half

// sweeps the full court), so a single player can drive both paddles too.

// 键盘兜底:`W`/`S` = 左边,`↑`/`↓` = 右边;回车 = 重开一局,ESC = 退出。

// Keyboard fallback: `W`/`S` = left, `↑`/`↓` = right; Enter = new match, ESC = 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`.

//

// ◆ 这份例程是**照修完的前端写的**(2026-09-30,`FRONTEND_DEFECTS.md` 的 Java 小节)

// ◆ This example is written **against the repaired front end** (2026-09-30, the Java section of `FRONTEND_DEFECTS.md`)

//

// 同目录的 `sokoban.java` 用的是"状态装局部数组、一路当参数传"的写法 —— 那是被当时的

// `sokoban.java` in the same directory uses "state in a local array, passed down as a parameter" — that was

// 前端缺口逼出来的(`static int[] A = new int[N]` 一个词都不占、元素直接踩字符串字面量)。

// forced by the gaps of the day (`static int[] A = new int[N]` reserved nothing and its elements stomped string literals).

// 那几条修掉之后,**自然的 Java 写法就能用了**,这个文件刻意把它们用上、当成活的验收:

// With those fixed, **the natural Java style works**, and this file deliberately uses it as a live acceptance test:

//

//   · `static int[] S` 当状态(真占 N+1 个词、元素清零、下标读写正确)

// · `static int[] S` as state (really reserves N+1 words, elements zeroed, indexing correct)

//   · `static final int` 当常量(字面量初值会落进数据段、读回来是**值**不是地址)

// · `static final int` constants (a literal initialiser lands in the data section and reads back as the **value**, not an address)

//   · `S.length`(VML 数组头里的元素个数)

// · `S.length` (the element count in the VML array header)

//   · `new int[N]` 真的分配、**元素是 0**(Java 语义)

//   · `new int[N]` really allocates and **the elements are 0** (Java semantics)

//

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

//     (`add` / `get` / `put` / `size` / `contains` / `remove` / `clear`)、`StringBuilder`

//     (`append` / `toString`)、`String.format` / `split` / `length()` / `charAt()` /

//     `equals`、`Integer.parseInt`、以及 **`"n=" + 7` 这种 int→String 的拼接**。

// `equals`, `Integer.parseInt`, and **int-to-String concatenation such as `"n=" + 7`**.

//     ⚠ 生效前提是 `Lib/modules.json` + `vmltool.config.xml` 把 `builtins_java` 挂上(协调者统一落)。

//   · **Once the language-specific library (`Lib/shared/src/builtins_java.c`) exists**, these work again:

//     `ArrayList`/`HashMap` (`add`/`get`/`put`/`size`/`contains`/`remove`/`clear`), `StringBuilder`

//     (`append`/`toString`), `String.format`/`split`/`length()`/`charAt()`/`equals`, `Integer.parseInt`,

//     and **int→String concatenation like `"n=" + 7`**. Requires `builtins_java` to be hooked up in

//     `Lib/modules.json` + `vmltool.config.xml` (the coordinator lands that).

//

// ⚠ **仍然不能用**的(实测,见台账):**实例字段**(类还没有对象布局)、

// ⚠ **Still unusable** (measured; see the ledger): **instance fields** (classes have no object layout yet),

//   自定义类的**方法派发**(`p.f()` 会被链接器按名字劫持到 `lib_*`)、

// method dispatch on user classes (`p.f()` gets hijacked by the linker to `lib_*`),

//   `try`-`catch`、`enum`、可变参数 `int...`、`int[][]` 的 `.length`。

//   `try`-`catch`, `enum`, varargs `int...`, and `.length` on `int[][]`.

//

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

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

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

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


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

    // ── 窗体 / 绘图 / 输入 / 声音(源:Lib/c/vml_ui.h)──

    // Window / drawing / input / sound (source: Lib/c/vml_ui.h)

    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);
    static native int  ui_key_is_down(int key);

    // ── 常量:字面量初值会落进数据段,读回来是值(修完那条之后才敢这么写)──

    // ── Constants: the literal initialiser lands in the data section and reads back as the value ──

    static final int MSG_KEYDOWN   = 1;      // VML_MSG_KEYDOWN

    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;

    static final int KEY_ESC     = 27;
    static final int KEY_ENTER   = 13;
    static final int KEY_UP      = 38;
    static final int KEY_DOWN    = 40;
    static final int KEY_W       = 87;
    static final int KEY_S       = 83;

    // 颜色一律写负数十进制(见 sokoban.java 文件头)

    // Colors are negative decimals throughout (see the header of sokoban.java)

    static final int C_BG      = -15724520;   // 0xFF101018

    static final int C_COURT   = -15066588;   // 0xFF1A1A24

    static final int C_LINE    = -12829620;   // 0xFF3C3C4C

    static final int C_TEXT    = -1513232;    // 0xFFE8E8F0

    static final int C_MUTED   = -6643536;    // 0xFF9AA0B0

    static final int C_P1      = -11890471;   // 0xFF4A90D9 蓝

    // 0xFF4A90D9 blue

    static final int C_P2      = -2069424;    // 0xFFE06C50 橙

    // 0xFFE06C50 orange

    static final int C_BALL    = -8096;       // 0xFFFFE060

    static final int C_ZONE1   = -15724390;   // 0xFF10101A 上半屏(略偏蓝)

    // 0xFF10101A the top half (a touch more blue)

    static final int C_ZONE2   = -15724276;   // 0xFF10101C 下半屏(略偏橙)

    // 0xFF10101C the bottom half (a touch more orange)

    static final int C_GOLD    = -8096;
    static final int C_WIN     = -11409298;   // 0xFF51E86E


    // 手感

    // Feel

    static final int PADDLE_H   = 78;    // 挡板高(px)

    // paddle height in px

    static final int PADDLE_W   = 12;
    static final int PADDLE_STEP = 14;   // 键盘按住时每拍挪多少 px

    // px moved per tick while a key is held down

    static final int BALL_R     = 7;
    static final int SERVE_VX   = 46;    // 发球速度,单位 **1/10 px 每拍**

    // serve speed, in **1/10 px per tick**

    static final int SERVE_VY   = 22;
    static final int MAX_VX     = 120;   // 速度上限(防穿透)

    // speed cap (stops the ball tunnelling through a paddle)

    static final int WIN_SCORE  = 7;
    static final int SERVE_WAIT = 36;    // 得分后停多少拍再发球

    // how many ticks to wait after a point before serving again


    /* 状态数组 S 的下标
    The S slot map
       0 球心 x(×10)   1 球心 y(×10)   2 vx(×10/拍)  3 vy(×10/拍)
       0 ball centre x (x10); 1 ball centre y (x10); 2 vx (x10/tick); 3 vy (x10/tick)
       4 左板 y(px)    5 右板 y(px)    6 左分   7 右分
       4 left paddle y (px); 5 right paddle y (px); 6 left score; 7 right score
       8 发球倒计时    9 发球方向(±1)  10 帧号  11 回合数(加速档)
       8 serve countdown; 9 serve direction (+-1); 10 frame counter; 11 rally length (speed gear)
       12 胜者(0 无)   13 触摸按下中    14 触摸 x 15 触摸 y 16 触摸在哪半屏(1 上 2 下)
       12 winner (0 = none); 13 touch is down; 14 touch x; 15 touch y; 16 which half was touched (1 top, 2 bottom)
       17 屏宽         18 屏高         19 界面语言
       17 screen width; 18 screen height; 19 UI language
       20 球场顶       21 球场底       22 左板左沿 x 23 右板左沿 x
       20 court top; 21 court bottom; 22 left paddle's left edge x; 23 right paddle's left edge x
       24 上一拍击球方(0 无) 25 自检用的数组长度 26 最长连续对拉
       24 who hit last (0 = nobody); 25 array length used by the self-check; 26 longest rally */
    static int[] S = new int[32];

    // ── 小工具 ──

    // Small helpers

    static int absI(int v) {
        if (v < 0) { return 0 - v; }
        return v;
    }

    static int clampI(int v, int lo, int hi) {
        if (v < lo) { return lo; }
        if (v > hi) { return hi; }
        return v;
    }

    // ── 排版:几何全部由屏宽屏高推出来(一处写死的画布尺寸都没有)──

    // Layout: all geometry is derived from the screen size (not one hard-coded canvas value)

    static void layout() {
        S[17] = S[17];
        S[20] = 54;
        S[21] = S[18] - 10;
        S[22] = 12;
        S[23] = S[17] - 12 - PADDLE_W;
    }

    static int courtH() {
        return S[21] - S[20];
    }

    // 手指位置 → 板顶 y:半屏内按比例映射到整块球场(半个屏推满 = 打满全场)

    // Finger position → paddle top y: mapped proportionally from the half onto the whole court

    static void aimPaddle(int side, int fingerY) {
        int half = S[18] / 2;
        int rel = 0;
        int top = 0;
        if (side == 1) { rel = fingerY; } else { rel = fingerY - half; }
        rel = clampI(rel, 0, half);
        top = S[20] + rel * courtH() / half - PADDLE_H / 2;
        top = clampI(top, S[20], S[21] - PADDLE_H);
        if (side == 1) { S[4] = top; } else { S[5] = top; }
    }

    // 发球一律**水平**(vy = 0):球从球场正中出发、板也在正中 ⇒ 第一下一定接得住,

    // the serve is always **flat** (vy = 0): the ball leaves the court centre with both paddles centred,

    // 之后的角度全靠"打在板的哪一段"决定(经典乒乓的 english,见 hitPaddle)。

    // so the first return is guaranteed; after that the angle comes from **where on the paddle it lands**.

    static void serve(int toward) {
        S[0] = S[17] * 10 / 2;
        S[1] = (S[20] + courtH() / 2) * 10;
        S[2] = SERVE_VX;
        S[3] = 0;
        if (toward < 0) { S[2] = 0 - SERVE_VX; }
        S[8] = SERVE_WAIT;
        S[11] = 0;
    }

    // 打在板心之上 → 往上飞;之下 → 往下飞。偏移越大角度越大,但**纵向不许超过横向**

    // Hit above the paddle's centre -> up; below it -> down. The further off-centre the steeper,

    // (否则球会越来越竖、最后在两个板之间上下弹而不前进)。

    // but the vertical component is never allowed to exceed the horizontal one.

    static void hitPaddle(int paddleTop) {
        int center10 = (paddleTop + PADDLE_H / 2) * 10;
        S[3] = S[3] + (S[1] - center10) / 3;
        if (absI(S[3]) > absI(S[2])) {
            if (S[3] < 0) { S[3] = 0 - absI(S[2]); } else { S[3] = absI(S[2]); }
        }
    }

    static void newMatch() {
        S[6] = 0;
        S[7] = 0;
        S[12] = 0;
        S[4] = S[20] + courtH() / 2 - PADDLE_H / 2;
        S[5] = S[4];
        S[13] = 0;
        S[24] = 0;
        serve(1);
    }

    // 球被挡一次就快一档:|v| 每次 +10%,横向封顶

    // Each paddle hit speeds the ball up by 10%; the horizontal component is capped

    static void speedUp() {
        int vx = absI(S[2]);
        int vy = S[3];
        vx = vx + vx / 10 + 1;
        if (vx > MAX_VX) { vx = MAX_VX; }
        if (S[2] < 0) { S[2] = 0 - vx; } else { S[2] = vx; }
        // ⚠ **`vy == 0` 时不许给它加那 1**:加了就"每一拍都歪一点",两下之后球必然飞离板口

        // never add that 1 when `vy == 0`: it would skew the ball a little every tick and it would leave the paddle mouth after two returns

        //   (实测 `maxrally=2`、"不动的板"必丢分)。正中的球就该水平地一直打下去 ——

        // (measured: `maxrally=2` and a motionless paddle always loses). A ball leaving dead centre must stay flat --

        //   那既是经典乒乓的行为,也是"板真的挡得住"最干净的判据。

        // that is both classic pong behaviour and the cleanest evidence that the paddle really blocks.

        if (vy < 0) { S[3] = vy - (0 - vy) / 12 - 1; }
        if (vy > 0) { S[3] = vy + vy / 12 + 1; }
        S[11] = S[11] + 1;
        if (S[11] > S[26]) { S[26] = S[11]; }      // 26 = 本局最长连续对拉(发球时不清零)

        // 26 = the longest rally of this match (not cleared by a serve)

    }

    static void point(int who) {
        if (who == 1) { S[6] = S[6] + 1; } else { S[7] = S[7] + 1; }
        if (S[6] >= WIN_SCORE) { S[12] = 1; }
        if (S[7] >= WIN_SCORE) { S[12] = 2; }
        if (S[12] != 0) {
            ui_audio_beep(1047, 300);
            return;
        }
        ui_audio_beep(220, 200);
        // 发球方向朝着**失分**那一方

        // the serve goes toward the side that **lost** the point

        if (who == 1) { serve(0 - 1); } else { serve(1); }
    }

    // 一拍物理。返回 1 表示这一拍有"事件"(用来决定要不要发声)

    // One physics tick. Returns 1 if the tick had an event (used to decide on a sound)

    static int step() {
        int r10 = BALL_R * 10;
        int bx = 0;
        int by = 0;
        int leftRight = 0;
        int rightLeft = 0;
        int ev = 0;

        if (S[12] != 0) { return 0; }

        if (S[8] > 0) {
            S[8] = S[8] - 1;
            return 0;
        }

        S[0] = S[0] + S[2];
        S[1] = S[1] + S[3];

        // 上下墙

        // the top and bottom walls

        if (S[1] - r10 < S[20] * 10) { S[1] = S[20] * 10 + r10; S[3] = 0 - S[3]; ev = 1; }
        if (S[1] + r10 > S[21] * 10) { S[1] = S[21] * 10 - r10; S[3] = 0 - S[3]; ev = 1; }

        // 左右板:判据 = 球心 x 越过板的**内沿** 且 球心 y 落在板的纵向范围内

        // Paddles: hit when the ball's x crosses the paddle's inner edge and its y is within the paddle's span

        leftRight = (S[22] + PADDLE_W) * 10;
        rightLeft = S[23] * 10;
        if (S[2] < 0 && S[0] - r10 <= leftRight && S[0] >= leftRight - 40) {
            if (S[1] >= S[4] * 10 - r10 && S[1] <= (S[4] + PADDLE_H) * 10 + r10) {
                S[0] = leftRight + r10;
                S[2] = 0 - S[2];
                hitPaddle(S[4]);
                speedUp();
                S[24] = 1;
                ev = 2;
            }
        }
        if (S[2] > 0 && S[0] + r10 >= rightLeft && S[0] <= rightLeft + 40) {
            if (S[1] >= S[5] * 10 - r10 && S[1] <= (S[5] + PADDLE_H) * 10 + r10) {
                S[0] = rightLeft - r10;
                S[2] = 0 - S[2];
                hitPaddle(S[5]);
                speedUp();
                S[24] = 2;
                ev = 2;
            }
        }

        // 出界 = 对方得分

        // out of bounds = a point for the other side

        if (S[0] + r10 < 0) { point(2); return 1; }
        if (S[0] - r10 > S[17] * 10) { point(1); return 1; }

        return ev;
    }

    // ── 画数字:`int` → `String` 的 `+` 在本前端拼不出数(台账里那条),一位一位画 ──

    // ── Digits: `int` → `String` via `+` cannot produce a number here, so draw one digit at a time ──

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

    // 每帧图元数刻意压着(≤400 是任务书的上限;实测约 40,量法见文件末尾注记)

    // The primitive count per frame is deliberately low (the brief caps it at 400; measured ≈40 — see the note at the end)

    static void draw() {
        int lang = S[19];
        int sw = S[17];
        int sh = S[18];
        int half = sh / 2;
        int cx = sw / 2;
        int i = 0;
        int y = 0;
        String d = "0";

        ui_2d_clear(C_BG);

        // 两个触摸半区(很淡的底色 + 一条分界线,让玩家看得出"一人一半")

        // The two touch halves (a faint tint plus a divider so the "one half each" split is visible)

        ui_2d_rect(0, 0, sw, half, C_ZONE1, 1, 0, 0);
        ui_2d_rect(0, half, sw, sh - half, C_ZONE2, 1, 0, 0);
        ui_2d_line(0, half, sw, half, C_LINE, 1);

        // 球场

        // the court

        ui_2d_rect(0, S[20], sw, courtH(), C_COURT, 1, 0, 0);
        ui_2d_line(0, S[20], sw, S[20], C_LINE, 1);
        ui_2d_line(0, S[21], sw, S[21], C_LINE, 1);

        // 中线(虚线)

        // the centre line (dashed)

        y = S[20] + 6;
        while (y < S[21] - 6) {
            ui_2d_rect(cx - 1, y, 3, 12, C_LINE, 1, 0, 0);
            y = y + 26;
        }

        // 比分

        // the score

        d = dg(S[6]);
        ui_2d_text(cx - 34, 14, d, C_P1, 26, 1);
        d = dg(S[7]);
        ui_2d_text(cx + 34, 14, d, C_P2, 26, 1);
        ui_2d_text(cx, 24, "-", C_MUTED, 20, 1);

        // 挡板

        // the paddles

        ui_2d_rect(S[22], S[4], PADDLE_W, PADDLE_H, C_P1, 1, 0, 5);
        ui_2d_rect(S[22] + 3, S[4] + 6, PADDLE_W - 6, PADDLE_H - 12, -1, 1, 0, 3);
        ui_2d_rect(S[23], S[5], PADDLE_W, PADDLE_H, C_P2, 1, 0, 5);
        ui_2d_rect(S[23] + 3, S[5] + 6, PADDLE_W - 6, PADDLE_H - 12, -1, 1, 0, 3);

        // 球(发球倒计时里闪一下,提示"要发了")

        // the ball (it blinks during the serve countdown: "here it comes")

        if (S[12] == 0 && (S[8] == 0 || S[10] % 8 < 4)) {
            ui_2d_circle(S[0] / 10, S[1] / 10, BALL_R, C_BALL, 1, 0);
        }

        // 半区标签(写明谁管哪半屏)

        // half labels (which half drives which paddle)

        ui_2d_text(8, half - 22, lang == 0 ? "上半屏 → 左边 P1" : "Top half -> left P1", C_P1, 12, 0);
        ui_2d_text(8, half + 18, lang == 0 ? "下半屏 → 右边 P2" : "Bottom half -> right P2", C_P2, 12, 0);
        ui_2d_text(sw - 8, 8, lang == 0 ? "W/S 也行" : "W/S too", C_MUTED, 11, 2);
        ui_2d_text(sw - 8, sh - 8, lang == 0 ? "↑/↓ 也行" : "↑/↓ too", C_MUTED, 11, 2);

        // 速度档位条(条越长球越快 —— 屏幕上不写数字)

        // A speed bar (the longer the bar, the faster the ball — no numbers on screen)

        ui_2d_rect(8, sh - 26, (sw - 16) * S[11] / 24, 8, C_GOLD, 1, 0, 3);

        // 胜负横幅

        // the win banner

        if (S[12] != 0) {
            ui_2d_rect(0, S[20] + courtH() / 2 - 46, sw, 92, C_COURT, 1, 0, 0);
            if (S[12] == 1) {
                ui_2d_text(cx, S[20] + courtH() / 2 - 14, lang == 0 ? "左边 P1 获胜!" : "Left P1 wins!", C_P1, 24, 1);
            } else {
                ui_2d_text(cx, S[20] + courtH() / 2 - 14, lang == 0 ? "右边 P2 获胜!" : "Right P2 wins!", C_P2, 24, 1);
            }
            ui_2d_text(cx, S[20] + courtH() / 2 + 20, lang == 0 ? "点屏幕或按回车再来一局" : "Tap or press Enter for a new match", C_TEXT, 13, 1);
        }

        ui_2d_present();
    }

    public static void main(String[] args) {
        int sw = 0;
        int sh = 0;
        int t = 0;
        int k = 0;
        int ev = 0;
        int tid = 0;
        int running = 1;
        int i = 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)


        // 数组头里的个数(`.length` 修完之后读的是它)

        // The element count in the array header (what `.length` reads after the fix)

        S[25] = S.length;
        // 自检:`new int[N]` 的元素必须是 0(Java 语义)

        // self-check: the elements of `new int[N]` must be 0 (Java semantics)

        i = 0;
        while (i < S.length) { S[i] = S[i]; i = i + 1; }

        S[19] = ui_scr_get_language();

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

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

        ui_win_open_ex(S[19] == 0 ? "双人乒乓" : "Pong", 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[17] = sw;
        S[18] = sh;
        layout();
        newMatch();
        draw();

        tid = ui_timer_set(28, 0);
        acc = 0;
        lastMs = ui_time_get_ms();   // 墙钟起点:进主循环时取(不在模块级调 syscall)

        // Wall-clock origin: taken when entering the main loop (no syscall at module level)


        while (running == 1 && 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 > 28 * 8) acc = 0;      // 卡太久别一次补一大串(防瞬移)

            // Do not catch up in one huge burst after a long stall (anti-teleport)

            if (acc >= 28) {
                while (acc >= 28) {
                    acc = acc - 28;
                    S[10] = S[10] + 1;

                    // 键盘是**轮询**按住状态(比逐次按键事件更适合"挡板要跟手")

                    // The keyboard is **polled** for held state (better than per-press events for a paddle that must track)

                    if (ui_key_is_down(KEY_W) != 0) { S[4] = S[4] - PADDLE_STEP; }
                    if (ui_key_is_down(KEY_S) != 0) { S[4] = S[4] + PADDLE_STEP; }
                    if (ui_key_is_down(KEY_UP) != 0) { S[5] = S[5] - PADDLE_STEP; }
                    if (ui_key_is_down(KEY_DOWN) != 0) { S[5] = S[5] + PADDLE_STEP; }
                    S[4] = clampI(S[4], S[20], S[21] - PADDLE_H);
                    S[5] = clampI(S[5], S[20], S[21] - PADDLE_H);

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

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

                    if (ev == 1) { ui_audio_beep(440, 40); }
                    if (ev == 2) { ui_audio_beep(660, 35); }
                }
                draw();
            }

            if (t == MSG_CLOSE) { running = 0; }

            if (t == MSG_RESIZE) {
                S[17] = ui_msg_get_a();
                S[18] = ui_msg_get_b();
                layout();
                draw();
            }
            if (t == MSG_ORIENT) { S[18] = ui_scr_get_h(); S[17] = ui_scr_get_w(); layout(); draw(); }

            if (t == MSG_KEYDOWN) {
                k = ui_msg_get_a();
                if (k == KEY_ESC) { running = 0; }
                if (k == KEY_ENTER) {
                    newMatch();
                    ui_audio_beep(523, 90);
                    draw();
                }
            }

            if (t == MSG_TOUCHDOWN || t == MSG_TOUCHMOVE) {
                S[14] = ui_msg_get_a();
                S[15] = ui_msg_get_b();
                S[13] = 1;
                if (S[12] != 0) {
                    if (t == MSG_TOUCHDOWN) { newMatch(); ui_audio_beep(523, 90); draw(); }
                } else {
                    if (S[15] < S[18] / 2) { S[16] = 1; } else { S[16] = 2; }
                    aimPaddle(S[16], S[15]);
                    if (t == MSG_TOUCHDOWN) { ui_audio_beep(440, 25); }
                    draw();
                }
            }

            if (t == MSG_TOUCHUP) { S[13] = 0; }

            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();

        // 给无头验证留确定性判据

        // deterministic output for headless verification

        System.out.print("pong: length=");   System.out.println(S[25]);
        System.out.print("pong: score=");
        System.out.print(S[6]);
        System.out.print("-");
        System.out.println(S[7]);
        System.out.print("pong: winner=");   System.out.println(S[12]);
        System.out.print("pong: rally=");    System.out.println(S[11]);
        System.out.print("pong: ball=");
        System.out.print(S[0] / 10);
        System.out.print(",");
        System.out.println(S[1] / 10);
        System.out.print("pong: vx=");       System.out.println(S[2]);
        System.out.print("pong: p1y=");      System.out.println(S[4]);
        System.out.print("pong: p2y=");      System.out.println(S[5]);
        System.out.print("pong: maxrally="); System.out.println(S[26]);
        System.out.print("pong: frames=");   System.out.println(S[10]);
        System.out.println("pong: done");
    }
}