-- 生命游戏 —— 用 **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` 的 `` 挂上来。 -- and are hooked in by `` 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()