← 都来码 / Dolaima← 例程索引cpp/app/snake.cpp原文

cpp/app/snake.cpp · 15045 字节 · 下载原文

// 贪吃蛇 —— 用 **C++** 写的手机游戏

// Snake — a mobile game written in **C++**

//

// ◆ 手机那套 UI 怎么来的

// ◆ Where the mobile UI comes from

//

// 开窗 / 绘图 / 输入 / 定时器的实现是 C 写的(`Lib/shared/src/vmlui.c` → `vmlui.vml`),

// The implementations of window opening / drawing / input / timers are written in C (`Lib/shared/src/vmlui.c` → `vmlui.vml`),

// 由 `vmltool.config.xml` 的 `<Language Name="cpp" Libs="vmlui.vml">` 挂给每个前端。

// and are attached to each frontend by `<Language Name="cpp" Libs="vmlui.vml">` in `vmltool.config.xml`.

// 数值的唯一真源是 `Lib/c/vml_ui.h`。

// The single source of truth for the numbers is `Lib/c/vml_ui.h`.

//

// ◆ 一条**已失效**的旧约束(2026-09-17 实测更正)

// ◆ One **obsolete** old constraint (corrected by measurement on 2026-09-17)

//

// `corpus/cpp/skel.cpp` 的文件头写着「实参从左到右压栈 ⇒ 多参调用参数整体反序,

// The file header of `corpus/cpp/skel.cpp` says "arguments are pushed left to right ⇒ multi-argument calls receive their parameters in reverse order,

// `ipow(2,3)` 得 9,所以库调用只能用单参数」——**这条现在不成立了**:

// `ipow(2,3)` gives 9, so library calls can only take a single argument" — **that no longer holds**:

// 调用约定统一之后实测 `drift.cpp`(循环里调 `ipow(2, i)`)得 **126**,正确。

// after the calling convention was unified, `drift.cpp` (calling `ipow(2, i)` in a loop) measured **126**, which is correct.

// 本文件按自然写法写 8 参的 `ui_2d_rect`,不为那条旧规避而扭曲。

// This file writes the 8-argument `ui_2d_rect` in the natural way, without contorting itself for that old workaround.

//

// ◆ 写法说明

// ◆ Notes on the style

//

//   · ~~数组**没有花括号初始化**(`InitializerListExpr` 只编第一个元素、其余静默丢)~~

//   · ~~Arrays **have no brace initialization** (`InitializerListExpr` compiles only the first element and silently drops the rest)~~

//     ~~⇒ 逐元素赋值。~~

//     ~~⇒ assign element by element.~~

//     ✅ **已修好(2026-10-04 复验)** —— `int a[4] = {10,20,30,40};` 实测读回 `10,20,30,40`;`static int g[4] = {11,22,33,44};` 读回 `11,22,33,44`。本文件仍逐元素赋值 —— 能跑就别动。

//     ✅ **Fixed (re-measured 2026-10-04)** — `int a[4] = {10,20,30,40};` reads back `10,20,30,40`; `static int g[4] = {11,22,33,44};` reads back `11,22,33,44`. This file still assigns element by element — leave it if it runs.

//   · 循环更新写 `i = i + 1`。

//   · Loop updates are written `i = i + 1`.

//   · 颜色写负数十进制(`0xAARRGGBB` 的十进制负数形式)。

//   · Colors are written as negative decimals (the negative-decimal form of `0xAARRGGBB`).

//   · 状态放**一个**文件级数组、辅助函数一律无参。

//   · State lives in **one** file-level array, and helper functions never take arguments.

//   · 音效用 **`ui_audio_beep` 单音**(v0.96.509 从共享库的音序器换回来 —— 那一版在真机上

//   · Sound effects use the **single-tone `ui_audio_beep`** (switched back from the shared library's sequencer in v0.96.509 — on a real device that version

//     **破音**)。判据:频率取原音型的**首音**、**结局音取最低音**(输要一眼听出来),

//     **broke up**). Criteria: the frequency takes **the first note** of the original pattern, **the ending note takes the lowest pitch** (a loss has to be audible at a glance),

//     时长取整块时长、封顶 320ms,低音不低于 C3(131Hz)(手机外放 200Hz 以下衰减很快)。

//     the duration takes the whole block's duration capped at 320ms, and low notes stay no lower than C3 (131Hz) (phone speakers roll off fast below 200Hz).

//

// ◆ 操作

// ◆ Controls

//

// 方向键转向;SELECT 暂停;回车重开;ESC 或返回箭头退出。

// Arrow keys steer; SELECT pauses; Enter restarts; ESC or the back arrow exits.

// 吃到食物 +10 分并加速;撞墙或撞到自己结束(弹对话框问要不要再来一局)。

// Eating food gives +10 points and speeds things up; hitting a wall or hitting yourself ends it (a dialog asks whether to play another round).


// 状态表(一个数组放全部,避免多个全局数组互相踩)

//   0=head 1=len 2=dx 3=dy 4=fx 5=fy 6=score 7=best

//   8=alive 9=paused 10=stepMs 11=cell 12=ox 13=oy 14=sw 15=sh

//   16+2i=第 i 节 x,17+2i=第 i 节 y(i=0 是头;40 节上限 ⇒ 16..95)

//   16+2i = segment i's x, 17+2i = segment i's y (i=0 is the head; 40-segment cap ⇒ 16..95)

//   100=候选x 101=候选y 105/106=新方向 107=命中

//   100=candidate x 101=candidate y 105/106=new direction 107=hit

#include <vml_ui.h>
int A[112];

// 界面语言:开局查一次(ui_scr_get_language 是 syscall,别每帧调)。

// UI language: queried once at game start (ui_scr_get_language is a syscall, do not call it every frame).

// 状态表 A[] 刻意只放游戏状态,语言是"界面"的事,单独一个文件级量。

// The state table A[] deliberately holds only game state; language is a matter of the "UI", so it is a separate file-level value.

int Lang;

// 运动墙钟欠账(毫秒)—— 按墙钟补步,用户 2026-10-04 口径(定时器只唤醒,走几步看欠了多少)。

// Motion wall-clock debt in ms -- steps are caught up from the wall clock (user's 2026-10-04 rule: the timer only wakes, the debt decides how many steps run).

int acc;
int lastMs;   // 上次补步取的墙钟时刻(进主循环时初始化)

              // wall-clock time of the last catch-up step (initialized when entering the main loop)


void occupied() {
    int i;
    A[107] = 0;
    i = 0;
    while (i < A[1]) {
        if (A[16+i*2] == A[100] && A[17+i*2] == A[101]) {
            A[107] = 1;
        }
        i = i + 1;
    }
}

void placeFood() {
    int tries;
    A[100] = ui_math_rand(20);
    A[101] = ui_math_rand(18);
    occupied();
    tries = 0;
    while (tries < 300) {
        if (A[107] != 0) {
            A[100] = ui_math_rand(20);
            A[101] = ui_math_rand(18);
            occupied();
        }
        if (A[107] == 0) {
            A[4] = A[100];
            A[5] = A[101];
            tries = 300;
        }
        tries = tries + 1;
    }
}

void draw() {
    int cellN;
    int i;
    ui_2d_clear(-15724520);

    // 棋盘格:扁平序号 0..359;每格按 (r+c) 的奇偶决定铺不铺

    // Board squares: flat index 0..359; each square is filled or not depending on the parity of (r+c)

    cellN = 0;
    while (cellN < 360) {
        if ((cellN+cellN/20)-((cellN+cellN/20)/2)*2 == 0) {
            ui_2d_rect(A[12]+(cellN-(cellN/20)*20)*A[11], A[13]+(cellN/20)*A[11],
                    A[11]-1, A[11]-1, -15263713, 1, 0, 2);
        }
        cellN = cellN + 1;
    }

    ui_2d_rect(A[12]+A[4]*A[11], A[13]+A[5]*A[11], A[11]-1, A[11]-1, -131246, 1, 0, 2);

    i = 0;
    while (i < A[1]) {
        if (i == 0) {
            ui_2d_rect(A[12]+A[16+i*2]*A[11], A[13]+A[17+i*2]*A[11],
                    A[11]-1, A[11]-1, -63488, 1, 0, 2);
        }
        if (i != 0) {
            ui_2d_rect(A[12]+A[16+i*2]*A[11], A[13]+A[17+i*2]*A[11],
                    A[11]-1, A[11]-1, -11409298, 1, 0, 2);
        }
        i = i + 1;
    }

    ui_2d_text(8, 8, Lang == 0 ? "得分" : "Score", -6643536, 13, VML_ANCHOR_LEFT);
    ui_2d_rect(58, 11, A[6], 10, -11409298, 1, 0, 0);
    ui_2d_text(A[14]/2, 8, Lang == 0 ? "最高" : "Best", -6643536, 13, VML_ANCHOR_CENTER);
    ui_2d_rect(A[14]/2+46, 11, A[7], 10, -63488, 1, 0, 0);
    if (A[9] != 0) {
        ui_2d_text(A[14]/2, A[15]/2, Lang == 0 ? "暂停(SELECT 继续)" : "Paused (SELECT)",
                -131246, 16, VML_ANCHOR_CENTER);
    }
    ui_2d_present();
}

void resetGame() {
    A[1] = 3;
    A[0] = 2;
    A[16] = 8;
    A[17] = 8;
    A[18] = 7;
    A[19] = 8;
    A[20] = 6;
    A[21] = 8;
    A[2] = 1;
    A[3] = 0;
    A[6] = 0;
    A[8] = 1;
    A[9] = 0;
    A[10] = 170;
    placeFood();
}

void gameOver() {
    A[8] = 0;
    // 结束音:**最低最长的**那一档(`ui_audio_beep` 单音 —— 见文件末的音效说明)

    // End sound: the **lowest and longest** setting (single-tone `ui_audio_beep` — see the sound notes at the end of the file)

    ui_audio_beep(131, 320);
    draw();
    // 选「否/拒绝」→ 退出游戏(ui_dlg_msg 返回 0=是 / 1=否)。

    // Choosing "no/decline" → quit the game (ui_dlg_msg returns 0=yes / 1=no).

    // 此前不接返回值 ⇒ 两个按钮一个样、游戏还退不出去(用户实测报的)。

    // Previously the return value was not used ⇒ both buttons looked the same and the game could not be exited (reported by the user on a real device).

    // 用 A[109] 这个没人用的槽当退出标志 —— gameOver 有四五处调用点,逐个改返回值不划算。

    // Use the unused slot A[109] as the quit flag — gameOver has four or five call sites, so changing the return value at each one is not worth it.

    if (ui_dlg_msg(Lang == 0 ? "贪吃蛇" : "Snake",
                   Lang == 0 ? "撞到了,这一局结束。\n再来一局?(选「否」退出)"
                             : "Crashed - game over.\nPlay again? (choose \"No\" to quit)", VML_DLG_INFO) != 0) {
        A[109] = 1;
        return;
    }
    resetGame();
}

void step() {
    int nc;
    int nr;
    int i;
    if (A[8] == 0) { return; }
    if (A[9] != 0) { return; }
    nc = A[16] + A[2];
    nr = A[17] + A[3];
    if (nc < 0) { gameOver(); return; }
    if (nc >= 20) { gameOver(); return; }
    if (nr < 0) { gameOver(); return; }
    if (nr >= 18) { gameOver(); return; }

    // 自撞:只看前 len-1 节(尾巴这一步会挪走)

    // Self-collision: only look at the first len-1 segments (the tail moves away in this step)

    A[100] = nc;
    A[101] = nr;
    A[107] = 0;
    i = 0;
    while (i < A[1]-1) {
        if (A[16+i*2] == nc && A[17+i*2] == nr) {
            A[107] = 1;
        }
        i = i + 1;
    }
    if (A[107] != 0) { gameOver(); return; }

    // 整体后移一位(不做环形回绕)

    // Shift everything back by one (no ring wraparound)

    i = A[1];
    while (i > 0) {
        A[16+i*2] = A[16+(i-1)*2];
        A[17+i*2] = A[17+(i-1)*2];
        i = i - 1;
    }
    A[16] = nc;
    A[17] = nr;

    if (nc == A[4] && nr == A[5]) {
        if (A[1] < 40) {
            A[1] = A[1] + 1;
        }
        A[6] = A[6] + 10;
        if (A[6] > A[7]) {
            A[7] = A[6];
        }
        if (A[10] > 70) {
            A[10] = A[10] - 6;
        }
        // 吃到食物:一声高而短的「叮」(原上行两音的首音)

        // Food eaten: one high, short "ding" (the first note of the original two-note rise)

        ui_audio_beep(1047, 165);
        ui_vibrate_simple(30, 0);
        placeFood();
    }
}

void turn() {
    if (A[105]+A[2] == 0 && A[106]+A[3] == 0) {
        return;
    }
    A[2] = A[105];
    A[3] = A[106];
}

int main() {
    int w;
    int h;
    int byw;
    int byh;
    int tid;
    int curMs;
    int t;
    int k;

    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 ? "贪吃蛇" : "Snake", 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)

    w = ui_scr_get_w();
    h = ui_scr_get_h();
    if (w <= 0) { w = 360; }
    if (h <= 0) { h = 620; }
    A[14] = w;
    A[15] = h;

    // 版面算一次,画与判定共用

    // The layout is computed once and shared by the drawing and the collision checks

    byw = w - 8;
    byh = h - 46;
    A[11] = byw / 20;
    if (byh/18 < A[11]) { A[11] = byh / 18; }
    if (A[11] < 4) { A[11] = 4; }
    A[12] = (w - A[11]*20) / 2;
    A[13] = 40;

    ui_scr_set_keep_on(1);
    resetGame();

    tid = ui_timer_set(A[10], 0);
    curMs = A[10];
    acc = 0;
    lastMs = ui_time_get_ms();   // 墙钟起点:进主循环时取(不在模块级/全局初始化里调 syscall)

                                 // wall-clock origin: sampled when entering the main loop (never call syscalls at module/global-init level)


    while (ui_win_is_closed() == 0) {

        if (A[109] != 0) { break; }   // 对话框里选了「否」→ 退出

        // "No" was chosen in the dialog → exit

        draw();
        t = ui_msg_fetch_wait(0);

        // 这一帧该走几步:**按墙钟欠账**(各平台定时器分辨率不同 ⇒ 不能拿"收到几拍"当时间)。

        // how many steps this frame takes: **driven by the wall-clock deficit** (timer resolution differs per platform => never treat "ticks received" as time).

        // 用户 2026-10-04 口径:所有游戏时钟取系统毫秒数。

        // user's 2026-10-04 rule: all game clocks read system milliseconds.

        {
            int now2 = ui_time_get_ms();
            acc = acc + (now2 - lastMs);
            lastMs = now2;
            if (acc > A[10] * 8) acc = 0;        // 卡太久别一次补一大串(防瞬移)

                                                 // do not catch up in one huge burst after a long stall (prevents teleporting)

            while (acc >= A[10]) { acc = acc - A[10]; step(); }
        }

        if (t == 10) { break; }
        if (t == 9) {
            // 定时器**只负责唤醒**(推进在上面按墙钟欠账走)

            // the timer **only wakes us up** (advancement happens above, driven by the wall-clock deficit)

            if (A[10] != curMs) {
                ui_timer_kill(tid);
                curMs = A[10];
                tid = ui_timer_set(curMs, 0);
            }
        }
        if (t == 1) {
            k = ui_msg_get_a();
            if (k == 27) { break; }
            if (k == 37) { A[105] = -1; A[106] = 0;  turn(); }
            if (k == 39) { A[105] = 1;  A[106] = 0;  turn(); }
            if (k == 38) { A[105] = 0;  A[106] = -1; turn(); }
            if (k == 40) { A[105] = 0;  A[106] = 1;  turn(); }
            if (k == 13) { resetGame(); }
            if (k == 16) { A[9] = 1 - A[9]; }
        }
    }

    ui_timer_kill(tid);
    ui_scr_set_keep_on(0);
    ui_win_close();
    return 0;
}