← 都来码 / Dolaima← 例程索引kotlin/test/catch.kt原文

kotlin/test/catch.kt · 10675 字节 · 下载原文

// 接方块 —— 用 **Kotlin** 写的手机游戏

// Catch — a phone game written in **Kotlin**

//

// 玩法:左右方向键移动底部挡板,把落下来的球弹回去;没接住就结束。每接住一次 +10 分。

// Gameplay: left/right arrows move the paddle at the bottom and bounce the falling ball back; miss it and the game ends. Each catch is +10 points.

//

// ◆ 手机那套 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="kotlin" Libs="vmlui.vml">` 挂上来。

// Hooked up by `<Language Name="kotlin" Libs="vmlui.vml">` in `vmltool.config.xml`.

//

// ◆ ~~一条本份例程实测出来的前端缺陷:文件级 `arrayOf` 读回 0~~ —— ✅ **已修好**(2026-10-04 复验)

// ◆ ~~A frontend defect measured with this example: a file-level `arrayOf` reads back 0~~ -- ✅ **fixed** (re-verified 2026-10-04)

//

// 历史对照(2026-09-17):文件级 `var A = arrayOf(0,1,2,3,4)` 时 `A[0]+A[3]` 得 0;main 内局部得 10。

// Historical comparison (2026-09-17): with a file-level `var A = arrayOf(0,1,2,3,4)`, `A[0]+A[3]` gave 0; a local inside `main` gave 10.

// 根因是解析器不认顶层 `val`/`var`(兜底分支把声明逐 token 吃掉),2026-09-30 已修(`FRONTEND_DEFECTS.md` 的 Kotlin 一节)。

// The root cause was the parser not accepting top-level `val`/`var` (the fallback branch ate the declaration token by token); fixed 2026-09-30 (Kotlin section of `FRONTEND_DEFECTS.md`).

//

// 实测(2026-10-04):文件级 `val TOPVAL = 9` 读回 9、`var topA = 3` 读回 3、文件级 `arrayOf` 的 `A[0]+A[3]` 得 **10**。

// Measured (2026-10-04): file-level `val TOPVAL = 9` reads 9, `var topA = 3` reads 3, a file-level `arrayOf` gives `A[0]+A[3]` = **10**.

// ⇒ 状态数组**不必**再留在 `main` 内(本份保持原样 —— 能跑就别动;新写的代码不必再绕)。

// => the state array no longer **has to** stay inside `main` (this file keeps its original form -- leave working code alone; new code need not work around it).

//

// ◆ 另一条:`step` 是 Kotlin 前端的**保留字**(`Expected function name ... got KEYWORD 'step'`),

// ◆ Another one: `step` is a **reserved word** in the Kotlin frontend (`Expected function name ... got KEYWORD 'step'`),

//   所以计时器那一拍叫 `tick` 不叫 `step`。

//   so the timer tick is called `tick`, not `step`.

//

// ◆ 其余写法:调库函数不用声明(对不认识的函数名发裸标签 CALL、实参右到左);

// ◆ The rest of the style: library calls need no declaration (unknown function names become bare-label CALLs with arguments right-to-left);

//   颜色写负数十进制;打印用 print_str(不换行)+ println_int(含换行)。

//   colors are negative decimals; printing uses print_str (no newline) + println_int (with newline).


// ── 显式引用:下面用到的 `ui_*` 来自共享库 vmlui.vml ──────────────────

// ── Explicit reference: the `ui_*` used below come from the shared library vmlui.vml ──

// ⚠ `.*` 必须有:`import vmlui`(无 `.*`)会被 `ExtractImports` 的正则**静默忽略**。

// ⚠ The `.*` is required: `import vmlui` (without `.*`) is silently ignored by `ExtractImports`.

// ⚠ 但整行删掉也照样编、照样跑 —— 前端靠 `ui_` 前缀**自动**链 `vmlui.vml`。

// ⚠ But deleting the whole line still compiles and runs — the frontend auto-links `vmlui.vml` from the `ui_` prefix.

// ⇒ 所以这条是**文档级**的显式声明,不是强制开关(判据见 `FRONTEND_DEFECTS.md` 的 Kotlin 一节)。

// ⇒ So this is a **documentation-level** explicit reference, not a hard switch (see the Kotlin section of `FRONTEND_DEFECTS.md`).

// ✅ 2026-10-04 复验仍成立:三种写法(`import vmlui` / `import vmlui.*` / 整行删掉)产出的指令数完全相同(59377 条)。

// ✅ Re-verified 2026-10-04, still holds: all three forms (`import vmlui` / `import vmlui.*` / line deleted) produce the identical instruction count (59377).

#include "../../../Lib/kotlin/vml_ui.kt"
import vmlui.*

fun main() {
    // 状态:0=挡板x 1=球x 2=球y 3=球dx 4=球dy 5=分数 6=最高 7=存活 8=屏宽 9=屏高

    // State: 0=paddle x, 1=ball x, 2=ball y, 3=ball dx, 4=ball dy, 5=score, 6=best, 7=alive, 8=screen width, 9=screen height

    var A = arrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

    // ⚠ **先开窗、再问尺寸**:开窗前 `ui_scr_get_w/h` 只能按**默认 chrome**(假设要手柄)估算,

    //   与真正开出来的画布对不上(见 E141)。⇒ 开窗尺寸传 0 交给宿主,**开完再问**真实可用绘图区。

    // ⚠ **Open the window first, then ask for the size**: before the window exists `ui_scr_get_w/h`

    //   can only estimate from the **default chrome** (which assumes a gamepad) and will not match the

    //   real canvas (see E141). So pass 0 here and ask for the real usable area **after** opening.

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

    // UI language: queried once at start (`ui_scr_get_language` is a syscall, not once per frame)

    var lang = ui_scr_get_language()
    // ⚠ 本前端**没有三元运算符** —— **C 风格 `cond ? a : b` 仍不能用**(2026-10-04 复验:

    // ⚠ This frontend has **no ternary operator** -- the **C-style `cond ? a : b` still cannot be used** (re-verified 2026-10-04:

    //   实参位置静默错编 —— 两个字符串字面量都不进数据段、传下去的是条件本身 0/1,其余实参整体错位一格)。

    //   silently miscompiles in argument position -- neither string literal reaches the data section, the condition 0/1 is passed and the remaining args shift by one).

    //   ~~`?:` 也不支持~~ —— Kotlin 的 Elvis `?:` **已经能用**(实测 `ui_2d_text(0,0,u ?: "FALLBACK",…)` 正确渲染)。

    //   ~~`?:` is unsupported too~~ -- Kotlin's Elvis `?:` **works now** (measured: `ui_2d_text(0,0,u ?: "FALLBACK",…)` renders correctly).

    //   ⇒ 语言分支仍写成 `if (lang == 0) { … } else { … }`。

    //   => language branches are still written as `if (lang == 0) { ... } else { ... }`.

    if (lang == 0) { ui_win_open("接方块", 0, 0) } else { ui_win_open("Catch", 0, 0) }
    var w = ui_scr_get_w()
    var h = ui_scr_get_h()
    if (w <= 0) { w = 360 }
    if (h <= 0) { h = 620 }
    A[8] = w
    A[9] = h
    ui_scr_set_keep_on(1)

    A[0] = w / 2 - 40
    A[1] = w / 2
    A[2] = 70
    A[3] = 3
    A[4] = 5
    A[5] = 0
    A[6] = 0
    A[7] = 1

    var tid = ui_timer_set(40, 0)

    while (ui_win_is_closed() == 0) {

        // ── draw ──

        ui_2d_clear(-15724520)
        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[5], 10, -11409298, 1, 0, 0)
        if (lang == 0) { ui_2d_text(A[8] / 2, 8, "最高", -6643536, 13, VML_ANCHOR_CENTER) } else { ui_2d_text(A[8] / 2, 8, "Best", -6643536, 13, VML_ANCHOR_CENTER) }
        ui_2d_rect(A[8] / 2 + 46, 11, A[6], 10, -63488, 1, 0, 0)
        ui_2d_rect(A[0], A[9] - 40, 80, 12, -63488, 1, 0, 6)
        ui_2d_circle(A[1], A[2], 9, -131246, 1, 0)
        if (A[7] == 0) {
            if (lang == 0) { ui_2d_text(A[8] / 2, A[9] / 2, "按回车重开", -131246, 16, VML_ANCHOR_CENTER) } else { ui_2d_text(A[8] / 2, A[9] / 2, "Press Enter to restart", -131246, 16, VML_ANCHOR_CENTER) }
        }
        ui_2d_present()

        var t = ui_msg_fetch_wait(0)
        if (t == 10) { break }

        if (t == 9) {
            if (A[7] != 0) {
                A[1] = A[1] + A[3]
                A[2] = A[2] + A[4]
                if (A[1] < 10) { A[1] = 10; A[3] = 0 - A[3] }
                if (A[1] > A[8] - 10) { A[1] = A[8] - 10; A[3] = 0 - A[3] }
                if (A[2] < 30) { A[2] = 30; A[4] = 0 - A[4] }
                if (A[2] > A[9] - 52 && A[2] < A[9] - 30 && A[1] > A[0] - 9 && A[1] < A[0] + 89) {
                    A[4] = 0 - A[4]
                    A[2] = A[9] - 52
                    A[5] = A[5] + 10
                    if (A[5] > A[6]) { A[6] = A[5] }
                    // 音效:单音 ui_audio_beep(v0.96.509 从音序器换回来 ——

                    // Sound: single-tone ui_audio_beep (switched back from the sequencer in v0.96.509 —

                    //   那一版多声部叠加 / 长音拖尾在真机上破音)

                    //   that version's multi-voice stacking / long-note tails broke up on real devices)

                    ui_audio_beep(1047, 165)
                }
                if (A[2] > A[9]) {
                    A[7] = 0
                    // 音效:单音 ui_audio_beep;**结局音取最低音**(接住 1047 / 没接住 131,差得开)

                    // Sound: single-tone ui_audio_beep; **the ending tone takes the lowest note** (1047 on a catch / 131 on a miss — far enough apart)

                    ui_audio_beep(131, 320)
                    var ans = 0
                    if (lang == 0) { ans = ui_dlg_msg("接方块", "没接住,这一局结束。\n再来一局?(选「否」退出)", VML_DLG_INFO) } else { ans = ui_dlg_msg("Catch", "Missed. Round over.\nPlay again? (choose 'No' to quit)", VML_DLG_INFO) }
                    if (ans != 0) { ui_win_close(); break }
                    A[0] = A[8] / 2 - 40
                    A[1] = A[8] / 2
                    A[2] = 70
                    A[3] = 3
                    A[4] = 5
                    A[5] = 0
                    A[7] = 1
                }
            }
        }

        if (t == 1) {
            var k = ui_msg_get_a()
            if (k == 27) { break }
            if (k == 37) { A[0] = A[0] - 20; if (A[0] < 4) { A[0] = 4 } }
            if (k == 39) { A[0] = A[0] + 20; if (A[0] > A[8] - 84) { A[0] = A[8] - 84 } }
            if (k == 13) {
                A[0] = A[8] / 2 - 40
                A[1] = A[8] / 2
                A[2] = 70
                A[3] = 3
                A[4] = 5
                A[5] = 0
                A[7] = 1
            }
        }
    }

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