← 都来码 / Dolaima← 例程索引csharp/app/racer.cs原文

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