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

objc/app/snake.m · 18203 字节 · 下载原文

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

// Snake — a phone game written in **ObjC**

//

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

// ◆ Where the phone UI comes from

//

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

// Window / drawing / input / timers are implemented in C (`Lib/shared/src/vmlui.c` → `vmlui.vml`),

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

// hooked to each frontend by `<Language Name="objc" Libs="vmlui.vml">` in `vmltool.config.xml`.

// 数值的唯一真源是 `Lib/c/vml_ui.h`,本文件**显式引用**它(`#import`)。

// The single source of truth for these numbers is `Lib/c/vml_ui.h`, and this file **references it explicitly** (`#import`).

//

// ◆ ObjC 这一路原本跑不了游戏,两条缺陷修完才通(2026-09-17)

// ◆ This ObjC path could not run games at first; it only worked after two defects were fixed (2026-09-17)

//

//   · **帧比局部量偏移少 4 字节**:文件级声明(全局量、函数原型)走基类 `EmitStoreVar`

//   · **The frame was 4 bytes smaller than the locals' offset**: file-level declarations (globals, function prototypes) went through the base class's `EmitStoreVar`

//     的兜底分配 `AllocAndRegisterVar` —— 它抬高 `nextStackOffset` 却**不发 `SUB`**,

//     fallback allocation `AllocAndRegisterVar` — it raised `nextStackOffset` without **emitting `SUB`**,

//     每有一条这样的声明,帧就比偏移少 4 字节。实测多一条函数原型即让局部量落到

//     so each such declaration left the frame 4 bytes short of the offset. Measured: one extra function prototype put a local at

//     `R12-12` 而帧只有 8 ⇒ 循环变量正好落在 push 区、被循环体那三个 push 写花。

//     `R12-12` while the frame was only 8 ⇒ the loop variable landed exactly in the push area and was clobbered by the loop body's three pushes.

//     判据 `drift.m` 由 2059 转 126。修法是 Dart 那套:序言占位 `SUB R13,#0`、

//     The `drift.m` criterion went from 2059 to 126. The fix is Dart's approach: a placeholder `SUB R13,#0` in the prologue,

//     函数体生成完回填 `nextStackOffset + 8`。

//     backfilled with `nextStackOffset + 8` once the function body is generated.

//   ✅ 2026-10-04 复验:`scripts/vml-abi-probe/langs/drift.m` 实测 `DRIFT=126`(= 2^1+…+2^6,正确),帧偏移缺陷不再出现。

//   ✅ Re-measured 2026-10-04: `scripts/vml-abi-probe/langs/drift.m` measured `DRIFT=126` (= 2^1+…+2^6, correct); the frame-offset defect no longer appears.

//

// ◆ 写法约束(每条都实测过,不是偏好)

// ◆ Style constraints (each one measured, not a preference)

//

//   ① **循环更新写 `i = i + 1`,不能写 `i++`** —— `++` 只 `ADD R0` 不写回

//   ① **Write the loop update as `i = i + 1`, never `i++`** — `++` only does `ADD R0` without writing back

//      (`CodeGenerator.Expressions.cs:235`)⇒ `for (…; …; i++)` 死循环。

//      (`CodeGenerator.Expressions.cs:235`) ⇒ `for (…; …; i++)` loops forever.

//   ② **数组没有初始化列表**:`int a[4] = {1,2,3,4};` 的初值会被丢掉

//   ② **Arrays have no initializer lists**: the values in `int a[4] = {1,2,3,4};` are dropped

//      ⇒ 只能逐元素赋值。

//      ⇒ they can only be assigned element by element.

//   ③ **颜色写负数十进制**:词法器十进制专用(`Lexer.cs:123`),不认 `0x`。

//   ③ **Write colors as negative decimals**: the lexer is decimal-only (`Lexer.cs:123`) and does not recognize `0x`.

//   ④ 状态放**一个**文件级数组、辅助函数一律无参 —— 与 Go/C# 那些示例同一处置。

//   ④ State goes in **one** file-level array and helper functions take no arguments — the same treatment as the Go/C# examples.

//

// ◆ 操作

// ◆ Controls

//

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

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

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

// Eating food is +10 points and a speed-up; hitting a wall or yourself ends the game (a dialog asks whether to play another round).

//

// ⚠ 与 Go/C#/Swift 那几份一样,**「开出绘图窗口」这条判据只能在设备上验**

// ⚠ As with the Go/C#/Swift copies, **the "opens a drawing window" criterion can only be verified on a device**

//   (桌面 vmlcli 的 ui_* 是宿主桩,主循环不会自然退出)。

//   (on the desktop, vmlcli's ui_* are host stubs and the main loop never exits on its own).

//   设备验收条目见 `scripts/maui-vml-verify/corpus.tsv` 的 `game-objc-snake`。

//   For the device acceptance entry see `game-objc-snake` in `scripts/maui-vml-verify/corpus.tsv`.


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

//   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=x of segment i, 17+2i=y of segment i (i=0 is the head; 40 segments max ⇒ 16..95)

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

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

//   110=界面语言(0=中文 1=英文,开局问一次宿主;见 draw 里那段说明)

//   110=UI language (0=Chinese 1=English, asked of the host once at the start; see the note in draw)

#include <vml_ui.h>
#import <vml_ui.h>

int A[112];

// 扫一遍身上的每一节,看 (A[100],A[101]) 这格有没有被占;结果写 A[107]

// Scan every segment of the body to see whether the cell (A[100],A[101]) is occupied; write the result into A[107]

void occupied() {
    int i;
    A[107] = 0;
    i = 0;
    while (i < A[1]) {
        if (A[16+i*2] == A[100]) {
            if (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 cells: flat index 0..359, row/column computed inline; whether each cell is filled depends 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;
    }

    // 食物

    // Food

    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 是头

    // Snake: i=0 is the head

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

    // 界面语言(0=中文 1=英文):从状态表里取,**每个函数各取一次当局部量** ——

    // UI language (0=Chinese 1=English): take it from the state table, **once per function into a local** —

    //   ~~本前端没有三元运算符(`? :` 只到词法,解析器未实现,见 OBJC_LANGUAGE_SPEC 的缺口表),~~

    //   ~~this frontend has no ternary operator (`? :` stops at the lexer, the parser never implements it — see the gap table in OBJC_LANGUAGE_SPEC),~~

    //   ~~所以界面文字只能靠 `if/else` 二选一、把调用写两遍。~~

    //   ~~so UI text can only be an `if/else` choice that writes the call twice.~~

    //   ✅ **已修好(2026-10-04 复验)** —— 三元运算符正常:`int b = a > 3 ? 100 : 200;`(a=5)实测得 `b=100`。本文件仍用 `if/else` 二选一 —— 能跑就别动;新写的代码不必再绕。

    //   ✅ **Fixed (re-measured 2026-10-04)** — the ternary operator works: `int b = a > 3 ? 100 : 200;` (a=5) measured `b=100`. This file still uses an `if/else` choice — leave it if it runs; new code need not avoid it.

    int lang;
    lang = A[110];
    if (lang == 0) ui_2d_text(8, 8, "得分", -6643536, 13, VML_ANCHOR_LEFT);
    else ui_2d_text(8, 8, "Score", -6643536, 13, VML_ANCHOR_LEFT);
    ui_2d_rect(58, 11, A[6], 10, -11409298, 1, 0, 0);
    if (lang == 0) ui_2d_text(A[14]/2, 8, "最高", -6643536, 13, VML_ANCHOR_CENTER);
    else ui_2d_text(A[14]/2, 8, "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) {
        if (lang == 0) ui_2d_text(A[14]/2, A[15]/2, "暂停(SELECT 继续)", -131246, 16, VML_ANCHOR_CENTER);
        else ui_2d_text(A[14]/2, A[15]/2, "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 在 Lib/ 里是「声明了但未实现」(见文件头)~~

    // ~~Key sound omitted: ui_audio_beep is "declared but not implemented" in Lib/ (see the file header)~~

    // ✅ **已修好(2026-10-04 复验)** —— `ui_audio_beep` 已实现并能发声:实测 `ui_audio_beep(440,100)` 打出 `[vml-audio] tone hz=440 ms=100 wave=1 vol=100 master=80`(桌面走 CoreAudio 真发声)。本文件仍未发音 —— 能跑就别动;新写的代码可以发声。

    // ✅ **Fixed (re-measured 2026-10-04)** — `ui_audio_beep` is implemented and sounds: `ui_audio_beep(440,100)` measured `[vml-audio] tone hz=440 ms=100 wave=1 vol=100 master=80` (desktop uses CoreAudio, real sound). This file still omits it — leave it if it runs; new code can play sound.

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

    // Choosing "No / refuse" → exit the game (ui_dlg_msg returns 0=yes / 1=no).

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

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

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

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

    int lang;
    int ans;
    lang = A[110];
    if (lang == 0) ans = ui_dlg_msg("贪吃蛇", "撞到了,这一局结束。\n再来一局?(选「否」退出)", VML_DLG_INFO);
    else ans = ui_dlg_msg("Snake", "You crashed. Round over.\nPlay again? (choose \"No\" to quit)", VML_DLG_INFO);
    if (ans != 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 on this step)

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

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

    // Shift the whole body back by one (no wrap-around)

    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]) {
        if (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;
            }
            placeFood();
        }
    }
}

void turn() {
    if (A[105]+A[2] == 0) {
        if (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;
    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)

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

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

    int nowMs;      // 墙钟采样

                    // wall-clock sample


    // 界面语言:开局问一次宿主要中文还是英文(0=中文 1=英文),存进 A[110](见 draw 的说明)。

    // UI language: ask the host once at the start whether it wants Chinese or English (0=Chinese 1=English) and store it in A[110] (see the note in draw).

    // ⚠ 别在每帧里调 —— 那是一次 syscall。

    // ⚠ Don't call it in every frame — that's a syscall.

    int lang;
    A[110] = ui_scr_get_language();
    lang = A[110];
    // 开窗:尺寸传 0 ⇒ 交给宿主给真实可用区(开窗**之前**问到的只是估算值)

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

    if (lang == 0) ui_win_open("贪吃蛇", 0, 0);
    else ui_win_open("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 drawing and hit-testing

    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;

    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 (no syscall at module 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.

        nowMs = ui_time_get_ms();
        acc = acc + (nowMs - lastMs);
        lastMs = nowMs;
        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_win_close();
    return 0;
}