csharp/app/racer.cs · 34022 字节 · 下载原文
// 赛车 —— 用 **C#** 写的手机游戏(不是 C)
// Racer — a phone game written in **C#** (not C)
//
// ✅ 能玩。判据是**逐像素量**出来的:本车(亮绿)恒定 1 辆、随左右键换道(实测 62px 一格)、
// ✅ Playable. The criteria were **measured pixel by pixel**: exactly 1 player car (bright green), lane changes on left/right keys (measured 62px per lane),
// 路面虚线每拍下移一档(y 在 40/58 之间循环)、敌车(红)自顶下行、
// the road dashes shift down one step per tick (y cycles between 40/58), the enemy cars (red) descend from the top,
// 躲过一辆 +10 分(分数条实测长到 20)、撞车后弹对话框并重开(一轮内两次归零)。
// dodging one is +10 points (the score bar measured 20), and after a crash a dialog pops up and the game restarts (two resets within one run).
//
// ⚠ 调参也调了两轮,第一版**根本得不了分**:敌车每 5 拍刷一辆、而穿过整条路要 98 拍,
// ⚠ Tuning took two rounds as well; the first version **could not score at all**: enemy cars spawned every 5 ticks while crossing the whole road takes 98 ticks,
// 于是玩家在"任何一辆车能跑到路底之前"就已经被撞死了(分数永远是 0)。
// so the player was already crashed before "any car could reach the bottom of the road" (the score stayed 0 forever).
// 现在改成 **26-速度 拍刷一辆 + 速度 8 起**,躲过的车才真的能跑到路底计分。
// It is now **26-speed ticks per spawn + speed starts at 8**, so dodged cars really do reach the bottom and score.
//
// ◆ 玩法
// ◆ Gameplay
//
// 三车道公路自上而下滚。**点屏幕哪条道就换到哪条道**(按住拖动跟手;方向键左右也行,
// A three-lane road scrolls top to bottom. **Tap a lane to move into it** (drag to follow your finger; left/right arrows also work,
// 上下键与左右等价),SELECT 暂停,回车重开,ESC 或返回箭头退出。
// up/down are equivalent to left/right), SELECT pauses, Enter restarts, ESC or the back arrow exits.
// 躲开迎面来的红车,每躲过一辆 +10 分并提一点速。
// Dodge the oncoming red cars; each one dodged is +10 points and a bit more speed.
// 撞上(或蹭到)就结束,弹对话框问要不要再来一局。
// Hitting one (or scraping it) ends the game, with a dialog asking whether to play another round.
//
// ⚠ 手机上**唯一的输入就是触摸**:这份文件在 2026-10-01 之前**一个触摸分支都没有**,
// ⚠ On a phone **touch is the only input**: before 2026-10-01 this file had **no touch branch at all**,
// 所以"左右换道"在手机上根本做不到(键盘只在桌面 `--input` 探针里有)。
// so "change lanes" was simply impossible on a phone (the keyboard only exists in the desktop `--input` probe).
//
// ⚠ 还有一条**玩法上的硬约束**:刷敌车时**必须保证每一横排至少留一条空道** ——
// ⚠ And one **hard gameplay constraint**: spawning must **always leave at least one lane free per row** —
// 三条道同时被占就是"玩家必死"的局面,而这不是难度、是 bug(见 `spawn()` 的 `scanRow`)。
// all three lanes blocked at once is an unwinnable position, and that is not difficulty, it is a bug (see `scanRow` in `spawn()`).
//
// ◆ 怎么验(探针说明 —— `spawn()` 里那句 `println_str("BLOCKED")` 就是为此留的)
// ◆ How to verify (the probe note — the `println_str("BLOCKED")` inside `spawn()` exists for this)
//
// 这条不变量**在桌面上用肉眼看不出来**(要看几百次刷车才可能撞上一次),所以把判据做进程序:
// This invariant **cannot be eyeballed on a desktop** (you would have to watch hundreds of spawns to maybe hit it once), so the judge lives inside the program:
//
// `spawn()` 放好一辆车后,回头再看一遍"这一横排是不是三条道全占了",是就打印 `BLOCKED`。
// After placing a car, `spawn()` re-scans "is this row blocked in all three lanes" and prints `BLOCKED` if so.
// 探针 = 跑一局(`--timeout 60`)grep 一下 stdout:
// The probe = run one round (`--timeout 60`) and grep stdout:
//
// dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll Examples/csharp/racer.cs \
// --vml-home third_party/vml --screen 480x640 --timeout 60 | grep -c BLOCKED # must be 0
// (这一行必须是 0 —— 非 0 就说明出现了"玩家过不去"的横排)
// (this line must be 0 — anything else means an unwinnable row appeared)
//
// **反证**(判据要能「响」):把本文件复制一份,把 `spawn()` 的选道换回旧的一行
// **Reverse test** (a judge must be able to ring): copy this file, put `spawn()`'s lane pick back to the old single line
// `int n = ui_math_rand(LANES);`(并把 `occ >= LANES - 1` 那条守卫去掉),同时把节奏调密
// `int n = ui_math_rand(LANES);` (and drop the `occ >= LANES - 1` guard), and make the cadence denser
// (`spawnIn = (26 - speed) / 2;`)——**实测 110 次刷车打出 12 次 BLOCKED、65 次同车道叠车**
// (`spawnIn = (26 - speed) / 2;`) — **measured: 12 BLOCKED and 65 same-lane stacks out of 110 spawns**
// (两次跑的抖动是 12~17 / 65~67);带着守卫、同样的密节奏则 **0 / 0**。
// (run-to-run jitter is 12~17 / 65~67); with the guards, at the same dense cadence: **0 / 0**.
// ⚠ 如实记一条:**按现在这个节奏**(`spawnIn = 26 - speed`,间距 ≥144px > 车高)旧逻辑实测
// ⚠ One honest note: **at the current cadence** (`spawnIn = 26 - speed`, spacing ≥144px > car height) the old logic measured
// 120 次刷车也是 0/0 —— 这条不变量当时是**碰巧**成立的,守卫把它变成**显式**的,
// 0/0 over 120 spawns too — the invariant held **by accident**; the guards make it **explicit**,
// 所以改节奏(上面那个自变量)时它才**真的挡住了** 15% 的死局。
// which is why it is what actually blocks the 15% dead rows when the cadence changes (the independent variable above).
//
// ◆ 为什么写得这么"平"
// ◆ Why it is written so "flat"
//
// C# 前端(patch 0010)修好过四处:带参方法参数串槽、读数组元素永远读到数组头、
// The C# frontend (patch 0010) fixed four things: parameters of methods with arguments crossing slots, array element reads always hitting the array head,
// `a` 指向的不是数组块、`static int[] x = new int[N]` 从来没分配过。
// `a` not pointing at the array block, and `static int[] x = new int[N]` never actually being allocated.
// 那四条都属"编译全绿、跑起来错",所以这里沿用**已被逐像素验证过的写法**:
// All four were "compiles clean, wrong at run time", so this uses **a style already verified pixel by pixel**:
// 数组只有 `ex/ey/eon` 三个,全部在 `Main` 里显式赋初值,不依赖字段初始化顺序。
// only three arrays, `ex/ey/eon`, all explicitly initialized inside `Main`, not relying on field initialization order.
//
// ◆ 一个设计上的取舍
// ◆ One design trade-off
//
// 不用"列表/容器",敌车直接用一个**定长数组 + 启用标志**(6 个槽):
// No "lists/containers"; enemy cars simply use a **fixed-length array + alive flags** (6 slots):
// 前端对定长数组的代码生成是验过的,容器类没有。
// the frontend's code generation for fixed-length arrays has been verified; container classes have not.
//
// 数值的唯一真源仍是 `Lib/c/vml_ui.h`。
// The single source of truth for these numbers is still `Lib/c/vml_ui.h`.
// 显式引用 UI 库:`ui_*` 的声明在 `Lib/csharp/VmlUi.cs`(tools/GenLib 生成,别手改)。
// Explicit reference to the UI library: `ui_*` is declared in `Lib/csharp/VmlUi.cs` (generated by tools/GenLib; don't hand-edit).
using Vml.Ui;
class Racer
{
// ── 常量(源:Lib/c/vml_ui.h)──
// ── Constants (source: Lib/c/vml_ui.h) ──
const int MSG_KEYDOWN = 1;
const int MSG_TOUCHDOWN = 6;
const int MSG_TOUCHMOVE = 7;
const int MSG_TOUCHUP = 8;
const int MSG_TIMER = 9;
const int MSG_WINDOWCLOSE = 10;
const int KEY_ENTER = 13;
const int KEY_SELECT = 16;
const int KEY_ESCAPE = 27;
const int KEY_LEFT = 37;
const int KEY_UP = 38;
const int KEY_RIGHT = 39;
const int KEY_DOWN = 40;
const int DLG_INFO = 0;
const int ANCHOR_LEFT = 0;
const int ANCHOR_CENTER = 1;
const int BG = -15724520; // 0xFF101018 路外底色
// 0xFF101018 off-road background
const int ROAD = -14999769; // 0xFF1B1F27 路面
// 0xFF1B1F27 road surface
const int DASH = -9537916; // 0xFF6E7684 车道虚线
// 0xFF6E7684 lane dashes
const int PLAYER = -11739014; // 0xFF4CE07A 本车
// 0xFF4CE07A player car
const int ENEMY = -50384; // 0xFFFF3B30 敌车
// 0xFFFF3B30 enemy car
const int HUD = -6643536; // 0xFF9AA0B0 次要文字
// 0xFF9AA0B0 secondary text
const int BAR = -11409298; // 0xFF51E86E 分数条
// 0xFF51E86E score bar
const int BAR2 = -63488; // 0xFFFF0800 最高分条
// 0xFFFF0800 best-score bar
const int WHITE = -1;
// ── 车道与车 ──
// ── Lanes and cars ──
const int LANES = 3;
const int SLOTS = 6; // 敌车槽位数(定长,前端只验过定长数组)
// number of enemy-car slots (fixed length; only fixed-length arrays have been verified in the frontend)
static int sw;
static int sh;
static int laneW; // 单车道宽
// width of one lane
static int roadX; // 路面左边界
// left edge of the road
static int roadW;
static int roadTop;
static int roadBot;
static int carW;
static int carH;
static int lane; // 本车所在车道 0..LANES-1
// the player car's lane, 0..LANES-1
static int score;
static int best;
static int alive;
static int paused;
static int speed; // 每拍敌车下移的像素
// pixels the enemy cars move down per tick
static int tickMs; // 每拍毫秒(越快越小)
// milliseconds per tick (smaller means faster)
static int acc; // 运动墙钟欠账(毫秒)—— 按墙钟补步,用户 2026-10-04 口径
// Motion wall-clock debt in ms — steps are caught up from the wall clock (user's 2026-10-04 rule)
static int lastMs; // 上次补步取的墙钟时刻(进主循环时初始化)
// wall-clock moment of the last catch-up (initialized on entering the main loop)
static int dash; // 虚线滚动偏移
// dash scroll offset
static int spawnIn; // 还有几拍刷一辆
// how many ticks until the next spawn
static int passed; // 已躲过几辆(提速用)
// how many cars have been dodged (used for speeding up)
static int lang; // 界面语言:0=中文 1=英文(开局问一次宿主,见 Main)
// UI language: 0=Chinese 1=English (asked of the host once at the start, see Main)
static int[] ex = new int[SLOTS]; // 敌车 x(像素,左边界)
// enemy car x (pixels, left edge)
static int[] ey = new int[SLOTS]; // 敌车 y(像素,上边界)
// enemy car y (pixels, top edge)
static int[] eon = new int[SLOTS];// 该槽是否启用
// whether this slot is active
static int[] busy = new int[LANES]; // `scanRow` 的产物:该道在窗口里有没有车
// what `scanRow` produces: whether that lane has a car inside the window
static void layout()
{
int pad = 10;
roadW = sw - pad * 2;
if (roadW > 260) roadW = 260; // 手机上别把公路拉得太宽
// don't stretch the road too wide on a phone
roadX = (sw - roadW) / 2;
roadTop = 40; // 顶部让出 40 给分数行
// leave 40 at the top for the score row
roadBot = sh - 10;
laneW = roadW / LANES;
// 车身宽度:从前是 `laneW - 24`(laneW=86 ⇒ 车宽 62,占车道 72%)——用户 2026-10-01 实测反馈
// Body width: it used to be `laneW - 24` (laneW=86 ⇒ carW=62, i.e. 72% of the lane) — the user's measured feedback on 2026-10-01 was
// 「车太大」,三辆车并排几乎贴在一起。改成 34 的余量(⇒ 52,占 60%),高度比也从 9/5 收到 8/5。
// "the cars are too big"; three of them side by side almost touch. Now 34 of margin (⇒ 52, i.e. 60%), and the height ratio drops from 9/5 to 8/5.
// ⚠ 别只改数字:改完要 `--frames` 出图看一眼比例对不对、内容有没有被裁掉。
// ⚠ Don't just change the numbers: after the change, render `--frames` and look at the proportions and that nothing is clipped.
carW = laneW - 34;
if (carW < 8) carW = 8;
carH = carW * 8 / 5;
if (carH > (roadBot - roadTop) / 5) carH = (roadBot - roadTop) / 5;
}
// 第 n 条车道的车身左边界
// Left edge of the car body in lane n
static int laneX(int n)
{
return roadX + n * laneW + (laneW - carW) / 2;
}
// 触摸 x → 车道号(钳进 0..LANES-1;点路面之外也算最近的那条道)
// Touch x -> lane index (clamped to 0..LANES-1; a tap outside the road counts as the nearest lane)
static int laneAt(int x)
{
int n = (x - roadX) / laneW;
if (n < 0) n = 0;
if (n > LANES - 1) n = LANES - 1;
return n;
}
// 把「生成线 y 往下 **2 个车身**以内,每条道有没有车」填进 `busy[]`,返回**被占的道数**。
// Fill `busy[]` with "does this lane have a car within **2 car-heights** below the spawn line y", and return **how many lanes are taken**.
//
// ⚠ 为什么是 **2 个车身**、不是 1 个(这条是被实测纠正的,别改回去):
// ⚠ Why **2 car-heights** and not 1 (this was corrected by measurement — do not change it back):
// 本车与敌车**各自占一个车身**,两车会撞上的条件是两个区间相交 ⇒ |ey − py| < carH。
// The player car and an enemy car **each take one car-height**, so they collide when the two intervals intersect ⇒ |ey − py| < carH.
// 换成"从生成线往下还有多远",就是 **2×carH** —— 取 1×carH 会漏掉
// In terms of "how far below the spawn line", that is **2×carH** — using 1×carH misses
// 「三辆车在两车身高以内各占一条道」这种**必死横排**。
// the **unwinnable row** where three cars take one lane each within two car-heights.
// **实测(2026-10-01,密节奏下刷车 110 次)**:不设防 12~17 次死局;守卫用 1×carH 时反而 **43 次**
// **Measured (2026-10-01, 110 spawns at a dense cadence)**: 12~17 dead rows with no guard; **43** with a 1×carH guard
// ("不叠车"把车摊到了三条道上);改成 2×carH ⇒ **0 次**。
// (avoiding same-lane stacking spread the cars across all three lanes); with 2×carH ⇒ **0**.
//
// 另一条让这件事成立的性质:所有敌车**每拍下移的像素相同**,相对间距恒定 ⇒
// Another property that makes this work: every enemy car moves down by the **same number of pixels per tick**, so relative spacing is fixed ⇒
// 三条道重叠过一次就永远重叠 ⇒ **只要在生成那一刻守住它,整局就都守住了**
// once three lanes overlap they overlap forever ⇒ **hold it at spawn time and it holds for the whole round**
// (这也是探针只需要盯 `spawn()` 的原因)。
// (which is why the probe only has to watch `spawn()`).
static int scanRow(int y)
{
int i = 0;
while (i < LANES)
{
busy[i] = 0;
i = i + 1;
}
int occ = 0;
i = 0;
while (i < SLOTS)
{
if (eon[i] != 0 && ey[i] < y + carH * 2 && ey[i] + carH * 2 > y)
{
int n = laneAt(ex[i] + carW / 2);
if (busy[n] == 0)
{
busy[n] = 1;
occ = occ + 1;
}
}
i = i + 1;
}
return occ;
}
static void reset()
{
lane = 1;
score = 0;
alive = 1;
paused = 0;
speed = 8;
tickMs = 45;
dash = 0;
spawnIn = 6;
passed = 0;
int i = 0;
while (i < SLOTS)
{
eon[i] = 0;
ey[i] = 0;
ex[i] = 0;
i = i + 1;
}
}
// 找空槽,从顶部放一辆敌车(**不堵满三条道**)
// Find a free slot and place an enemy car at the top (**never fill all three lanes**)
//
// 修之前这里只有一句 `int n = ui_math_rand(LANES);` —— 纯随机挑道,不看那条道上有没有车、
// Before the fix this was just `int n = ui_math_rand(LANES);` — a purely random lane, blind to whether
// 也不管这一横排会不会被占满。两个可见后果(用户 2026-10-01 报的「路被堵死」):
// that lane was taken or whether the row would end up fully blocked. Two visible consequences (the "the road gets blocked" report, 2026-10-01):
// ① 同一车道**纵向叠着两辆车**(视觉上糊成一片);
// ① two cars **stacked in the same lane** (visually smeared together);
// ② 三条道同时随机到 ⇒ **必死的横排**(三辆车各自纵向错开一点点,从缝里也钻不过去)。
// ② all three lanes randomly taken ⇒ **an unwinnable row** (the three cars offset a little vertically, with no gap to slip through).
// 判据(可复现):删掉下面 `occ >= LANES - 1` 那条守卫与"只在空道里挑",
// Judge (reproducible): remove the `occ >= LANES - 1` guard below and the "only pick a free lane" rule,
// 探针跑几十局就会打出 `BLOCKED`;留着则**一局都打不出来**。
// and a probe will print `BLOCKED` within a few dozen rounds; keep them and it **never** prints it.
static void spawn()
{
int slot = -1;
int i = 0;
while (i < SLOTS)
{
if (eon[i] == 0 && slot < 0) slot = i;
i = i + 1;
}
if (slot < 0) return;
int y0 = roadTop - 20;
// ① 生成带上还剩几条空道?**只剩 ≤1 条就不刷** —— 保证每一横排都有活路。
// ① How many lanes are still free on the spawn line? **Skip the spawn if only ≤1 is left** — every row keeps a way through.
int occ = scanRow(y0);
if (occ >= LANES - 1) return;
// ② 只在**空道**里均匀随机挑一条(谁都不与已有敌车重叠)。
// ② Pick uniformly among the **free** lanes only (so nothing overlaps an existing car).
int k = ui_math_rand(LANES - occ);
int n = -1;
i = 0;
while (i < LANES)
{
if (busy[i] == 0)
{
if (k == 0 && n < 0) n = i;
k = k - 1;
}
i = i + 1;
}
if (n < 0) return;
ex[slot] = laneX(n);
ey[slot] = y0;
eon[slot] = 1;
// ③ 自检(**这就是探针的判据**):这一横排三条道全被占 = 玩家必死。
// ③ Self-check (**this is what the probe asserts on**): all three lanes taken in this row == the player is doomed.
// 上面两条守卫在,它**永远打不出来**;删掉守卫,它立刻出现。
// With the two guards above it **can never print**; remove them and it shows up immediately.
// 走 `println_str` 而不是 stderr,是因为这条要在**跑起来之后**从 stdout 里 grep —— 见文件头的探针说明。
// It goes through `println_str` rather than stderr because the probe greps it from stdout **at run time** — see the probe note in the header.
if (scanRow(y0) >= LANES) println_str("BLOCKED");
}
// 一辆敌车的车身面积(不做旋转,画法是"车身 + 挡风玻璃 + 两个轮子")
// Body area of one enemy car (no rotation; drawn as "body + windshield + two wheels")
static void drawCar(int x, int y, int body)
{
ui_2d_rect(x, y, carW, carH, body, 1, 0, 4);
// 挡风玻璃:车身上部一条浅色
// Windshield: a light-colored strip on the upper part of the body
ui_2d_rect(x + 3, y + carH / 4, carW - 6, carH / 4, BG, 1, 0, 2);
// 两个轮子
// Two wheels
ui_2d_rect(x - 2, y + carH / 5, 3, carH / 4, WHITE, 1, 0, 0);
ui_2d_rect(x + carW - 1, y + carH / 5, 3, carH / 4, WHITE, 1, 0, 0);
}
static void draw()
{
ui_2d_clear(BG);
// 路面
// Road surface
ui_2d_rect(roadX, roadTop, roadW, roadBot - roadTop, ROAD, 1, 0, 0);
// 车道虚线:每条分道线一段一段画,整体按 dash 上滚
// Lane dashes: each divider is drawn segment by segment, scrolling upward by dash as a whole
int dashH = 18;
int gap = 18;
int period = dashH + gap;
int l = 1;
while (l < LANES)
{
int x = roadX + l * laneW - 1;
int y = roadTop - period + (dash % period);
while (y < roadBot)
{
if (y + dashH > roadTop)
{
int y0 = y;
int y1 = y + dashH;
if (y0 < roadTop) y0 = roadTop;
if (y1 > roadBot) y1 = roadBot;
if (y1 > y0) ui_2d_rect(x, y0, 2, y1 - y0, DASH, 1, 0, 0);
}
y = y + period;
}
l = l + 1;
}
// 敌车
// Enemy cars
int i = 0;
while (i < SLOTS)
{
if (eon[i] != 0)
{
if (ey[i] + carH > roadTop && ey[i] < roadBot)
{
drawCar(ex[i], ey[i], ENEMY);
}
}
i = i + 1;
}
// 本车
// Player car
drawCar(laneX(lane), roadBot - carH - 6, PLAYER);
// HUD:分数条 + 最高分条(不拼字符串 —— 见文件头对前端能力的说明)
// HUD: score bar + best-score bar (no string concatenation — see the file header on the frontend's capabilities)
ui_2d_text(8, 8, lang == 0 ? "得分" : "Score", HUD, 13, ANCHOR_LEFT);
ui_2d_rect(58, 11, score, 10, BAR, 1, 0, 0);
ui_2d_text(sw / 2, 8, lang == 0 ? "最高" : "Best", HUD, 13, ANCHOR_CENTER);
ui_2d_rect(sw / 2 + 46, 11, best, 10, BAR2, 1, 0, 0);
if (paused != 0)
{
ui_2d_text(sw / 2, sh / 2, lang == 0 ? "暂停(SELECT 继续)" : "Paused (SELECT)", ENEMY, 16, ANCHOR_CENTER);
}
ui_2d_present();
}
// 返回 1 表示本拍有变化、需要重画
// Returning 1 means something changed this tick and a redraw is needed
static int step()
{
if (alive == 0 || paused != 0) return 0;
dash = dash + speed;
if (dash >= 36) dash = dash - 36;
// 敌车下移
// Move enemy cars down
int i = 0;
while (i < SLOTS)
{
if (eon[i] != 0)
{
ey[i] = ey[i] + speed;
// 出路面:回收 + 计分 + 提速
// Off the road: recycle + score + speed up
if (ey[i] > roadBot)
{
eon[i] = 0;
score = score + 10;
if (score > best) best = score;
passed = passed + 1;
if (passed % 5 == 0)
{
if (speed < 18) speed = speed + 1;
if (tickMs > 24) tickMs = tickMs - 4;
// 躲过一辆:一声高而短的「叮」(单音 ui_audio_beep —— v0.96.509 从
// Dodged one: a short, high "ding" (single-tone ui_audio_beep — in v0.96.509 switched back
// 音序器换回来;那一版多声部叠加 / 长音在真机上破音)
// from the sequencer; that version's multi-voice stacking / long notes broke up on real devices)
ui_audio_beep(1047, 165);
}
}
}
i = i + 1;
}
// 刷新
// Refresh
spawnIn = spawnIn - 1;
if (spawnIn <= 0)
{
spawn();
spawnIn = 26 - speed;
if (spawnIn < 10) spawnIn = 10;
}
// 碰撞:本车占 [roadBot-carH-6, roadBot-6),纵向重叠且横向贴近
// Collision: the player car occupies [roadBot-carH-6, roadBot-6); overlapping vertically and near horizontally
int px = laneX(lane);
int py = roadBot - carH - 6;
i = 0;
while (i < SLOTS)
{
if (eon[i] != 0)
{
int vOver = 0;
if (ey[i] < py + carH && ey[i] + carH > py) vOver = 1;
int hNear = 0;
if (ex[i] < px + carW && ex[i] + carW > px) hNear = 1;
if (vOver != 0 && hNear != 0)
{
alive = 0;
// 撞车(结局):**最低音**、最长
// Crash (ending): **the lowest note**, the longest one
ui_audio_beep(131, 320);
draw();
if (ui_dlg_msg(lang == 0 ? "赛车" : "Racer", lang == 0 ? "撞车了,这一局结束。\n再来一局?(选「否」退出)" : "You crashed. Round over.\nPlay again? (choose \"No\" to quit)", DLG_INFO) != 0) { ui_win_close(); return 1; }
reset();
return 1;
}
}
i = i + 1;
}
return 1;
}
static void Main()
{
// 界面语言:开局问一次宿主要中文还是英文(0=中文 1=英文),之后整局按它分支。
// UI language: ask the host once at the start whether it wants Chinese or English (0=Chinese 1=English), then branch on it for the whole game.
// ⚠ 别在每帧里调 —— 那是一次 syscall。
// ⚠ Don't call it in every frame — that's a syscall.
lang = 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(lang == 0 ? "赛车" : "Racer", 0, 0);
// 尺寸:开窗**之后**再问一次 —— 这时 `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;
layout();
ui_scr_set_keep_on(1);
reset();
draw();
int tid = ui_timer_set(tickMs, 0);
int curMs = tickMs;
acc = 0;
lastMs = ui_time_get_ms(); // 墙钟起点:进主循环时取(不在模块级/全局初始化里调 syscall)
// Wall-clock origin: taken when entering the main loop (no syscall at module level or in a global initializer)
while (ui_win_is_closed() == 0)
{
int t = ui_msg_fetch_wait(0);
// 这一帧该走几步:**按墙钟欠账**(各平台定时器分辨率不同 ⇒ 不能拿"收到几拍"当时间)。
// 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.
int now2 = ui_time_get_ms();
acc = acc + (now2 - lastMs);
lastMs = now2;
if (acc > tickMs * 8) acc = 0; // 卡太久别一次补一大串(防瞬移)
// Do not catch up in one huge burst after a long stall (anti-teleport)
while (acc >= tickMs) { acc = acc - tickMs; if (step() != 0) draw(); }
if (t == 0) continue;
if (t == MSG_WINDOWCLOSE) break;
if (t == MSG_TIMER)
{
// 定时器**只负责唤醒**(推进在上面按墙钟欠账走)
// The timer **only wakes us up** — the advance happens above, by wall-clock debt
// 提速后要把定时器换掉(重复定时器的间隔是建立时定死的)
// After speeding up you must replace the timer (a repeating timer's interval is fixed when it is created)
if (tickMs != curMs)
{
ui_timer_kill(tid);
curMs = tickMs;
tid = ui_timer_set(curMs, 0);
}
continue;
}
// ── 触摸:手机上唯一的输入 ──
// ── Touch: the only input on a phone ──
// 点/拖到哪条道就换到哪条道(拖动跟手)。**只在车道真的变了才重画** ——
// Tap/drag to a lane and the car moves there (drag follows the finger). **Redraw only when the lane actually changed** —
// 一帧要把整幅场景光栅化一遍,手指划过同一车道时别白画。
// a frame rasterizes the whole scene, so don't redraw while the finger stays in the same lane.
if (t == MSG_TOUCHDOWN)
{
if (paused == 0)
{
int nl = laneAt(ui_msg_get_a());
if (nl != lane) { lane = nl; draw(); }
}
continue;
}
if (t == MSG_TOUCHMOVE)
{
// 拖动类消息只追最新位置 ⇒ 把积压的那一类丢掉(与 `g2048.cs` 同一条约定)
// Drag messages only care about the newest position ⇒ drop the backlog of that kind (same rule as `g2048.cs`)
ui_msg_drop(1);
if (paused == 0)
{
int nl = laneAt(ui_msg_get_a());
if (nl != lane) { lane = nl; draw(); }
}
continue;
}
if (t == MSG_TOUCHUP)
{
// ⚠ 这里**不** `ui_msg_drop` —— 它丢的是**整类触摸消息**,抬手的同一刻
// ⚠ **No** `ui_msg_drop` here — it discards **the whole touch kind**, and at the moment of release
// 紧接着的那次按下(连点)已经在队列里,一丢就丢掉了下一次点击。
// the tap right after it (a double tap) is already queued, so dropping it loses the next tap.
// `ui_msg_drop` 只用在**拖动**那条路上(位置类消息追最新,旧的没有价值)。
// `ui_msg_drop` is only used on the **drag** path (position messages chase the latest; old ones are worthless).
if (paused == 0)
{
int nl = laneAt(ui_msg_get_a());
if (nl != lane) { lane = nl; draw(); }
}
continue;
}
if (t == MSG_KEYDOWN)
{
int k = ui_msg_get_a();
if (k == KEY_ESCAPE) break;
if (k == KEY_LEFT)
{
if (lane > 0) lane = lane - 1;
draw();
}
else if (k == KEY_RIGHT)
{
if (lane < LANES - 1) lane = lane + 1;
draw();
}
else if (k == KEY_UP)
{
if (lane > 0) lane = lane - 1;
draw();
}
else if (k == KEY_DOWN)
{
if (lane < LANES - 1) lane = lane + 1;
draw();
}
else if (k == KEY_ENTER)
{
reset();
draw();
}
else if (k == KEY_SELECT)
{
if (paused != 0) paused = 0; else paused = 1;
draw();
}
}
}
ui_timer_kill(tid);
ui_scr_set_keep_on(0);
ui_win_close();
}
}