← 都来码 / Dolaima← 例程索引lua/app/life.lua原文

lua/app/life.lua · 14118 字节 · 下载原文

-- 生命游戏 —— 用 **Lua** 写的手机程序

-- Game of Life — a phone program written in **Lua**

--

-- 这是 Conway 的 Game of Life:双缓冲数组 + 邻居求和,**每格的状态由它自己与邻居算出**。

-- This is Conway's Game of Life: double-buffered arrays + neighbor summing, **each cell's state computed from itself and its neighbors**.

-- 与前面几份(弹球 / 段表)刻意不同:这里一整帧的开销几乎全在「遍历数组 + 条件求和」上,

-- Deliberately different from the earlier ones (bouncing ball / jump table): here a whole frame's cost is almost all in "walking arrays + conditional summing",

-- 照的是前端「数组当主力」的那条路径。

-- which exercises the frontend's "arrays as the workhorse" path.

--

-- 一个 20×24 的网格、初始随机撒点;每 300ms 推进一代。

-- A 20×24 grid, randomly seeded at startup; one generation advances every 300ms.

-- 操作:回车重新撒点;SELECT 暂停/继续;ESC 或返回箭头退出。

-- Controls: Enter re-seeds; SELECT pauses/resumes; ESC or the back arrow exits.

--

-- ◆ 手机那套 UI

-- ◆ The phone's UI set

--

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

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

-- 由 `vmltool.config.xml` 的 `<Language Name="lua" Libs="vmlui.vml">` 挂上来。

-- and are hooked in by `<Language Name="lua" Libs="vmlui.vml">` in `vmltool.config.xml`.

--

-- ◆ 为什么全部内联在 main 里

-- ◆ Why everything is inlined into main

--

-- Lua 的 `function` 看不见**别的函数里的局部**,而这份的状态表(A/B 两张 480 格的表)

-- Lua's `function` cannot see **locals of another function**, and this file's state tables (the two 480-cell tables A/B)

-- 正好是要跨帧保留的。写成全局表 + 顶层函数当然也可以,但本前端对全局表的行为没有把握

-- are exactly what must survive across frames. Writing it as global tables + top-level functions is of course possible, but this frontend's behavior around global tables is not something I trust

-- (Kotlin 的文件级 arrayOf 就是坏的),所以沿用 `corpus/lua/skel.lua` 的形状:

-- (Kotlin's file-level arrayOf is broken), so it follows the shape of `corpus/lua/skel.lua`:

-- ✅ 2026-10-04 复验:「全局表」本身其实是好的 —— 函数里改 `GT[1]`、加 `GN`,调用方都读得到。

-- ✅ Re-verified 2026-10-04: global tables themselves actually work — a function mutating `GT[1]` and `GN` is visible to the caller.

--    所以"没有把握"这条已不成立;本文件仍内联在 `main` 里(写法无害)。

--    So the "not something I trust" note no longer holds; this file still inlines everything in `main` (harmless).

-- 一切都在 `main` 内。

-- everything stays inside `main`.

--

-- ◆ 写法要求(前两条是实测出来的)

-- ◆ Writing requirements (the first two were measured in practice)

--

--   · ~~**循环变量必须先 `local` 声明**再进 `for` —— 否则循环体一次都不执行~~

--   · ~~**The loop variable must be declared with `local`** before entering `for` — otherwise the loop body never runs even once~~

--     ✅ **已修好(2026-10-04 复验)**:不预先 `local` 也能跑(`for i = 1, 5` 求和 ⇒ 15、顶层 `for i = 1, 4` ⇒ 1 2 3 4)。

--     ✅ **Fixed (re-verified 2026-10-04)**: it runs without a prior `local` (`for i = 1, 5` summing => 15; top-level `for i = 1, 4` => 1 2 3 4).

--   · 表是 **1 基**的:扁平下标 `行*20 + 列` 要 `+1` 才能对上。

--   · Tables are **1-based**: the flat index `row*20 + col` needs `+1` to line up.

--   · 颜色写负数十进制。

--   · Colors are written as negative decimal numbers.


-- ── 显式引用 UI 库的声明文件 ────────────────────────────────────────────────

-- Explicit reference to the UI library's declaration file.

--

-- `Lib/lua/vml_ui.lua` 由 `GenLib -n -c vmlui -l lua` 从 `Lib/shared/src/vmlui.c` 生成(155 个 `ui_*`)。

-- `Lib/lua/vml_ui.lua` is produced by `GenLib -n -c vmlui -l lua` from `Lib/shared/src/vmlui.c` (155 `ui_*`).

-- **不要手写、也不要手改生成物**(要改就改生成器 `tools/GenLib`)。

-- Never hand-write it and never edit the generated file (change the generator `tools/GenLib` instead).

--

-- ⚠ 这条 `require` 对 Lua 是**文档级**引用:本前端按名字解析库函数,删掉它照样编得过、跑得对。

-- ⚠ For Lua this `require` is a **documentation-level** reference: the front end resolves library

-- functions by name, so removing it still compiles and runs.

-- 实测结论与命令见 `docs/UI库导入约定.md` 的「lua 实测结论」一节。

-- Measured results and the exact commands are in the UI-library import convention doc (Lua section).

require("vml_ui")
function main()
    local W = 20
    local H = 24
    local N = W * H

    -- 两张 480 格的状态表:A 是当前代,B 是下一代(双缓冲)

    -- Two 480-cell state tables: A is the current generation, B is the next (double buffering)

    local A = {}
    local B = {}

    -- 界面语言:开局问一次宿主要中文还是英文(0=中文 1=英文),之后整局按它分支。

    -- UI language: ask the host once at startup whether it wants Chinese or English (0=Chinese 1=English), then branch on it for the whole game.

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

    -- ⚠ Do not call it in every frame — it is a syscall.

    -- ⚠ **不能用 `(lang == 0) and "中文" or "English"`** —— 本前端的 `and`/`or` 求的是

    -- ⚠ **Do not use `(lang == 0) and "Chinese" or "English"`** — this frontend's `and`/`or` evaluate

    --   **布尔 0/1**(`ExpressionManager.EmitAnd/EmitOr` 收尾就是 `MOVE R0, 0/1`),

    --   **boolean 0/1** (`ExpressionManager.EmitAnd/EmitOr` finish with exactly `MOVE R0, 0/1`),

    --   不是操作数的值 ⇒ 当字符串用会拿到 0/1,画出来是乱码(实测窗口标题变成 "VML")。

    --   not the value of the operand ⇒ using it as a string yields 0/1 and draws garbage (measured: the window title became "VML").

    --   只有 `if/else` 赋值这条路能用。

    --   Only the `if/else` assignment route works.

    local lang = ui_scr_get_language()
    local t_title = "Game of Life"
    local t_gen = "Generation"
    local t_paused = "Paused (SELECT)"
    if lang == 0 then t_title = "生命游戏"
    else t_title = "Game of Life" end
    if lang == 0 then t_gen = "世代"
    else t_gen = "Generation" end
    if lang == 0 then t_paused = "已暂停(SELECT 继续)"
    else t_paused = "Paused (SELECT)" end

    -- 开窗时传 0:尺寸交给宿主(用真实可用区)。**开窗之后**再问一次真实画布 —— 开窗前问到的只是估算值。

    -- Pass 0 when opening: the size is the host's to decide (the real usable area). Ask the **real** canvas only after the window is open -- before that the host can only estimate.

    ui_win_open(t_title, 0, 0)

    local w = ui_scr_get_w()
    local h = ui_scr_get_h()
    if w <= 0 then
        w = 360
    end
    if h <= 0 then
        h = 620
    end

    local cell = (w - 8) / W
    if (h - 90) / H < cell then
        cell = (h - 90) / H
    end
    if cell < 4 then
        cell = 4
    end
    local ox = (w - cell * W) / 2
    local oy = 50

    -- 初始撒点:ui_math_rand(2) 给 0 或 1

    -- Initial seeding: ui_math_rand(2) gives 0 or 1

    local i = 0
    i = 1
    while i <= N do
        A[i] = ui_math_rand(2)
        B[i] = 0
        i = i + 1
    end

    ui_scr_set_keep_on(1)
    local paused = 0
    local gen = 0
    local tickms = 300
    local tid = ui_timer_set(tickms, 0)
    -- 墙钟补步累加器(按墙钟补步,用户 2026-10-04 口径:游戏时钟取系统毫秒数)

    -- Wall-clock catch-up accumulator (steps are caught up from the wall clock; user's 2026-10-04 rule: all game clocks read system milliseconds)

    local acc = 0
    local lastms = ui_time_get_ms()   -- 墙钟起点(进主循环时取)

                                      -- wall-clock origin, sampled when entering the main loop


    while ui_win_is_closed() == 0 do

      ui_sfx_tick()
        -- ── draw ──

        ui_2d_clear(-15724520)
        ui_2d_text(8, 8, t_gen, -6643536, 13, VML_ANCHOR_LEFT)
        ui_2d_rect(58, 11, gen, 10, -11409298, 1, 0, 0)
        if paused ~= 0 then
            ui_2d_text(w / 2, 8, t_paused, -6643536, 13, VML_ANCHOR_CENTER)
        end

        local row = 0
        local col = 0
        while row < H do
            col = 0
            while col < W do
                local idx = row * W + col + 1
                if A[idx] ~= 0 then
                    ui_2d_rect(ox + col * cell, oy + row * cell, cell - 1, cell - 1, -131246, 1, 0, 0)
                end
                col = col + 1
            end
            row = row + 1
        end
        ui_2d_present()

        -- 按墙钟补步,用户 2026-10-04 口径:游戏时钟取系统毫秒数。

        -- steps are caught up from the wall clock; user's 2026-10-04 rule: all game clocks read system milliseconds.

        -- 定时器只做唤醒:欠账时不等消息、立刻补跑一代;欠账还清才回去等。

        -- the timer only wakes us up: while in deficit it does not wait for a message and runs one generation immediately; it goes back to waiting once the deficit is cleared.

        local now2 = ui_time_get_ms()
        acc = acc + (now2 - lastms)
        lastms = now2
        if acc > tickms * 8 then
            acc = 0        -- 卡太久别一次补一大串

                           -- do not catch up in one huge burst after a long stall

        end
        local t = 0
        if acc < tickms then
            t = ui_msg_fetch_wait(0)
            if t == 10 then
                break
            end
        end

        if acc >= tickms then
            acc = acc - tickms
            if paused == 0 then
                -- ── 推进一代:数邻居 → 套规则 → 写 B ──

                -- ── Advance one generation: count neighbors → apply the rules → write B ──

                row = 0
                while row < H do
                    col = 0
                    while col < W do
                        local n = 0
                        local dy = 0 - 1
                        while dy <= 1 do
                            local dx = 0 - 1
                            while dx <= 1 do
                                -- 只数八邻域、跳过自己

                                -- Count only the eight-neighborhood, skipping the cell itself

                                if dx ~= 0 or dy ~= 0 then
                                    local rr = row + dy
                                    local cc = col + dx
                                    -- 边界不环绕:出界当死

                                    -- Edges do not wrap: out of bounds counts as dead

                                    if rr >= 0 and rr < H then
                                        if cc >= 0 and cc < W then
                                            local j = rr * W + cc + 1
                                            if A[j] ~= 0 then
                                                n = n + 1
                                            end
                                        end
                                    end
                                end
                                dx = dx + 1
                            end
                            dy = dy + 1
                        end

                        local idx = row * W + col + 1
                        -- 规则:活细胞 2~3 邻居存活;死细胞恰好 3 邻居复活

                        -- Rules: a live cell with 2~3 neighbors survives; a dead cell with exactly 3 neighbors comes back to life

                        if A[idx] ~= 0 then
                            if n == 2 or n == 3 then
                                B[idx] = 1
                            else
                                B[idx] = 0
                            end
                        else
                            if n == 3 then
                                B[idx] = 1
                            else
                                B[idx] = 0
                            end
                        end
                        col = col + 1
                    end
                    row = row + 1
                end

                -- 双缓冲交换:B 拷回 A

                -- Double-buffer swap: copy B back into A

                i = 1
                while i <= N do
                    A[i] = B[i]
                    i = i + 1
                end
                gen = gen + 1
            end
        end

        if t == 1 then
            local k = ui_msg_get_a()
            if k == 27 then
                break
            end
            if k == 13 then
                i = 1
                while i <= N do
                    A[i] = ui_math_rand(2)
                    i = i + 1
                end
                gen = 0
                ui_sfx_add(0, 84, 0, 2, 70, 1)   -- 换代:短促

                -- generation change: short and crisp

            end
            if k == 16 then
                if paused == 0 then
                    paused = 1
                else
                    paused = 0
                end
            end
        end
    end

    ui_timer_kill(tid)
    ui_scr_set_keep_on(0)
    ui_win_close()
end

main()