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