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()