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

lua/app/snake.lua · 26861 字节 · 下载原文

-- 贪吃蛇 —— 用 **Lua** 写的手机游戏

-- Snake — a phone game written in **Lua**

--

-- 规则:方向键(或触摸)控制蛇头,吃到食物变长、加 1 分并提一档速度。

-- Rules: steer the head with the arrow keys (or by touch); eating food makes the snake longer,

-- adding 1 point and one notch of speed.

-- 撞墙或咬到自己就结束;蛇长到 40 节算通关。

-- Hitting a wall or biting yourself ends the run; reaching 40 cells wins.

-- 那个 40 是「场地大小」与「表的容量」两条约束的交点(见下)。

-- That 40 is where the field size and the **table capacity** constraint meet (see below).

-- 操作:方向键 / WASD 转向 · SELECT 暂停 · 回车重开 · 触摸 = 朝那边拐 · ESC 退出。

-- Controls: arrows / WASD to steer · SELECT pauses · Enter restarts · touch = turn that way · ESC exits.

--

-- ◆ 这份例程在练什么

-- ◆ What this example exercises

--

-- 生命游戏(`life.lua`)把状态摊成两个 480 格的扁平数组、一帧只做「遍历 + 条件求和」。

-- The Life example flattens its state into two 480-cell arrays and does nothing but "walk + conditional sum" per frame.

-- 这一份走另一条路:**蛇身是一张会增删的表**,每走一步 `table.insert` / `table.remove` 加一次整条挪位。

-- This one takes another path: the **snake is a table that grows and shrinks**; every step does

-- `table.insert` / `table.remove` plus one full shift of the body.

-- HUD 的分数与长度走一个**自己画的 3×5 点阵数字渲染器**,不靠字符串。

-- The HUD's score and length go through a **hand-rolled 3×5 dot-matrix number renderer**, not through strings.

--

-- ◆ 为什么 HUD 要自己画点阵数字

-- ◆ Why the HUD renders its own dot-matrix digits

--

-- `tostring(1234)` + `..` 拼串在本前端**不可靠**:实测 `#tostring(1234)` 得 **0**、

-- `tostring(1234)` plus `..` string building is **unreliable** here: measured, `#tostring(1234)` gives **0**,

-- `print(tostring(1234))` 打进 stdout 的是 **5**、`#("score=" .. tostring(9))` 得 **7**(应为 10)。

-- `print(tostring(1234))` prints **5**, and `#("score=" .. tostring(9))` gives **7** (it should be 10).

-- 所以数字改走**查表 + 位掩码**:`FONT[d+1]` 是 15 位掩码(3 宽 × 5 高,位序 = 行*3+列)。

-- So numbers go through a **table + bitmask** instead: `FONT[d+1]` is a 15-bit mask (3 wide × 5 tall, bit index = row*3+col).

-- 逐格测位、亮格画一个 `ui_2d_rect` —— 零字符串、零浮点,画出来的是确定的矩形(`--trace-draw` 能逐条核)。

-- Each bit is tested and one `ui_2d_rect` is drawn per lit cell — no strings, no floats, and every pixel is

-- verifiable line by line with `--trace-draw`.

--

-- ◆ 本语言的五条**实测约束**(这份例程按它们写)

-- ◆ Five **measured constraints** of this language (this example is written around them)

--

--   · **顶层 `local` 在函数里读不到**(读到野地址 ⇒ 内存错误);**函数也看不见外层函数的局部**。

--   · **A top-level `local` is not visible inside a function** (wild address ⇒ memory fault), and **a function

--     cannot see the enclosing function's locals either** (no closure capture).

--     ⇒ 跨函数共用的状态**一律写成顶层全局**,逻辑也写在顶层函数里。

--     ⇒ Everything shared across functions is a **top-level global**, and the logic lives in top-level functions.

--   · **空表 `{}` 只有 8 个键位**:`capacity = 字段数 + max(8, 字段数/2 + 4)`,**写满之后新键被静默丢弃**。

--   · **An empty `{}` table holds only 8 keys**: `capacity = fields + max(8, fields/2 + 4)`, and once it is full

--     **new keys are silently dropped**.

--     实测:`A = {}` 连写 90 个下标只有 8 个留下;`B = {0,0}` 留 10 个;`C = {16 个 0}` 留 28 个。

--     Measured: `A = {}` — 8 of 90 indices stick; `B = {0,0}` — 10; `C = {16 zeros}` — 28.

--     ✅ 2026-10-04 复验仍成立(重测:`{}` 连写 90 个 ⇒ `#A` = 8、`{0,0}` ⇒ 10、`{16 个 0}` ⇒ 28)。

--     ✅ Still true as of 2026-10-04 (re-measured: `{}` + 90 writes => `#A` = 8; `{0,0}` => 10; `{16 zeros}` => 28).

--     ⇒ 蛇身表先按 `{40 个 0}` 建出来、**再逐个弹空**(弹空只动 count、容量还在),之后最多放 64 节。

--     ⇒ The body table is built as `{40 zeros}` and then **popped empty** (popping only moves `count`; the

--     capacity stays), which supports up to 64 cells.

--   · **别写 `t[#t + 1] = v`**(内联的 `#` 当下标)—— 下标算错(实测写 3 次只剩 1 个元素)。

--   · **Do not write `t[#t + 1] = v`** (inline `#` as a subscript) — the index is miscomputed

--     (measured: three writes leave one element).

--     要么先 `local n = #t` 再 `t[n + 1] = v`,要么直接 `table.insert`。

--     Either take `local n = #t` first and then `t[n + 1] = v`, or just use `table.insert`.

--   · **`and` / `or` 求的是布尔 0/1**,不是操作数的值(见 `life.lua`)⇒ 选择一律 `if/else`。

--   · **`and` / `or` evaluate to boolean 0/1**, not the operand's value (see `life.lua`) ⇒ always use `if/else`.

--   · `pairs` / `tostring` / `math.random` / `math.sin` 这些路没通,别用;随机数走 `ui_math_rand(n)`。

--   · `pairs` / `tostring` / `math.random` / `math.sin` are not wired up — use `ui_math_rand(n)` for randomness.


-- ── 显式引用 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")

-- ── 顶层全局:常量、点阵字体、一局的全部状态 ────────────────────────────────

-- ── Top-level globals: constants, the dot-matrix font, and all the state of one round ──

-- 开局每步 220ms;吃到食物减 6ms

-- one step every 220 ms at the start; each food shaves 6 ms off that

SPEED0 = 220
-- 速度下限(再快就只是"看不清"了)

-- the speed floor (anything faster is just a blur)

SPEED_MIN = 96
-- 通关长度

-- the winning length: reach this many cells and the round is won

WIN_LEN = 40
COLS = 18
ROWS = 22
FONT = {31599, 29850, 29671, 31207, 18925, 31183, 31695, 18727, 31727, 31215}
-- 数字 0..9 的 15 位点阵掩码(位序 = 行*3 + 列):

-- 15-bit dot-matrix masks for 0..9 (bit index = row*3 + col):

--   "0" = 111/101/101/101/111 ⇒ 低 15 位里的 0、1、2、4、6、8、10、12、13、14 位置 1

--   "0" = 111/101/101/101/111 ⇒ bits 0,1,2,4,6,8,10,12,13,14 set

BIT = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384}
-- 蛇身:**先按 40 个元素建表拿到容量、再逐个弹空**(弹的是 count,容量留在 [p-8])。

-- The body: **built as a 40-element table to grab the capacity, then popped empty** (popping moves `count`;

-- the capacity lives in `[p-8]`).

-- 只写 `{}` 的话容量只有 8,蛇长到第 9 节起新下标会被 lua_table_set 静默丢掉(2026-10-04 复验仍成立)。

-- With a bare `{}` the capacity is 8 and every index past the eighth is silently dropped by `lua_table_set` (still true as of 2026-10-04).

BODY = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
-- 0 上 / 1 右 / 2 下 / 3 左

-- current heading: 0 up / 1 right / 2 down / 3 left

DIR = 1
FOOD = 0
SCORE = 0
-- 0 进行 / 1 结束 / 2 通关

-- round state: 0 playing / 1 over / 2 won

STATE = 0
PAUSED = 0
SPEED = SPEED0
TIMER_SPEED = SPEED0
TID = 0
CELL = 8
OX = 0
OY = 48
W = 360

-- 界面文字:开局按 `ui_scr_get_language()` 选一次(见 main)

-- UI strings: chosen once from `ui_scr_get_language()` at startup (see main)

T_TITLE = "Snake"
T_SCORE = "SCORE"
T_LEN = "LEN"
T_OVER = "GAME OVER"
T_WIN = "YOU WIN!"
T_AGAIN = "Enter = restart"
T_PAUSED = "PAUSED (SELECT)"

-- 画一个 3×5 点阵数字:`d` 是数字、`px` 是一格边长、`color` 是真彩色

-- Draw one 3×5 dot-matrix digit: `d` the digit, `px` the cell side, `color` a truecolor value

-- (多参函数 + 函数内调库:坐标在这里算,`ui_2d_rect` 也在这里调)

-- (multi-argument function calling into the library: coordinates computed here, `ui_2d_rect` called here too)

function draw_digit(x, y, d, px, color)
    if d < 0 then
        d = 0
    end
    if d > 9 then
        d = 9
    end
    local mask = FONT[d + 1]
    local r = 0
    while r < 5 do
        local c = 0
        while c < 3 do
            -- 位序与生成掩码时一致:idx = 行*3 + 列;掩码表 1 基 ⇒ +1。

            -- Same bit order as when the masks were generated: idx = row*3 + col; the table is 1-based ⇒ +1.

            -- 整数除 + 取余就能测位,不必用 `math.pow`(它对 Lua 走 8 字节 double 那条路)

            -- Integer division plus modulo tests the bit; no `math.pow` (that goes down the 8-byte double path).

            if mask / BIT[r * 3 + c + 1] % 2 == 1 then
                ui_2d_rect(x + c * px, y + r * px, px, px, color, 1, 0, 0)
            end
            c = c + 1
        end
        r = r + 1
    end
end

-- 画一个非负整数:先数位数、再从高位往低位画(每位占 4 格宽 = 3 格字形 + 1 格间距)

-- Draw a non-negative integer: count the digits, then draw from the most significant down

-- (each digit takes four cells = three glyph + one gap)

function draw_number(x, y, v, px, color)
    if v < 0 then
        v = 0
    end
    if v > 999999 then
        v = 999999
    end
    local digits = 1
    local n = v
    while n >= 10 do
        n = n / 10
        digits = digits + 1
    end
    local i = digits
    local rest = v
    while i >= 1 do
        local d = rest % 10
        rest = rest / 10
        draw_digit(x + (i - 1) * px * 4, y, d, px, color)
        i = i - 1
    end
end

-- 字符串宽度(方法调用形态:`s:len()` 被归一成 `string.len(s)`,实测可用)

-- String width (the method-call form: `s:len()` normalises to `string.len(s)` — measured working)

-- ⚠ 它数的是**字节**:中文一个字 3 字节(`"贪吃蛇":len()` = 9)—— 拿它排版要按字节算。

-- ⚠ It counts **bytes**: one CJK character is three bytes (a three-character title measures 9) — lay out by bytes.

function text_w(s)
    return s:len()
end

-- 复位一局:蛇身三格、方向向右、分数归零、速度回开局值

-- Reset a round: a three-cell snake heading right, score zero, speed back to its opening value

function reset_round()
    local i = 0
    while i < 64 do
        table.remove(BODY)
        i = i + 1
    end
    table.insert(BODY, 11 * COLS + 6)
    table.insert(BODY, 11 * COLS + 5)
    table.insert(BODY, 11 * COLS + 4)
    DIR = 1
    SCORE = 0
    STATE = 0
    PAUSED = 0
    SPEED = SPEED0
    FOOD = 11 * COLS + 12
end

-- 找一个空格放食物:先随机试 400 次,试不出来(场地快满了)就线性扫。返回 -1 = 一个空格都没有 = 通关

-- Pick an empty cell for food: 400 random tries first, then a linear scan when the field is nearly full

-- Returns -1 meaning "no empty cell left" = the player won

function free_cell()
    local total = COLS * ROWS
    local tries = 0
    while tries < 400 do
        local idx = ui_math_rand(total)
        local taken = 0
        local k = 1
        while k <= #BODY do
            if BODY[k] == idx then
                taken = 1
            end
            k = k + 1
        end
        if taken == 0 then
            return idx
        end
        tries = tries + 1
    end
    local j = 0
    while j < total do
        local taken = 0
        local k = 1
        while k <= #BODY do
            if BODY[k] == j then
                taken = 1
            end
            k = k + 1
        end
        if taken == 0 then
            return j
        end
        j = j + 1
    end
    return -1
end

-- 记下「下一步往哪走」,不允许 180° 掉头

-- Record the next heading; a 180-degree reversal is refused

function turn(nd)
    if nd ~= (DIR + 2) % 4 then
        DIR = nd
    end
end

-- 走一步:算新头 → 判撞墙 / 判自咬 → 吃或不吃

-- Take one step: compute the new head → test walls / self → eat or not

-- 返回 1 表示这一步改了画面(喂给主循环的「要不要重画」)

-- Returns 1 when the step changed the picture (fed to the main loop's "should we redraw")

function step()
    local r = BODY[1] / COLS
    local c = BODY[1] % COLS
    if DIR == 0 then r = r - 1 end
    if DIR == 1 then c = c + 1 end
    if DIR == 2 then r = r + 1 end
    if DIR == 3 then c = c - 1 end

    if r < 0 or r >= ROWS or c < 0 or c >= COLS then
        STATE = 1
        -- 撞墙:最低音 + 最长(§4.5「输取最低」,不看屏幕也分得出)

        -- wall hit: lowest and longest tone (4.5: take the lowest for a loss, audible without looking)

        ui_audio_beep(131, 320)
        return 1
    end

    local nh = r * COLS + c
    local eating = 0
    if nh == FOOD then
        eating = 1
    end

    -- 自咬判定:不吃食物时尾巴会挪开,所以尾巴那一格不算撞上

    -- Self-bite test: when not eating the tail moves away, so the tail cell does not count as a hit

    local last = #BODY
    if eating == 0 then
        last = last - 1
    end
    local k = 1
    while k <= last do
        if BODY[k] == nh then
            STATE = 1
            ui_audio_beep(131, 320)
            return 1
        end
        k = k + 1
    end

    if eating == 0 then
        -- 表增删之「删」:不吃就把尾巴弹掉(`table.remove(t)` 弹的是最后一个 = 尾巴)

        -- Table insert/remove, the "remove" half: `table.remove(t)` pops the last element, which is the tail

        table.remove(BODY)
    end
    -- 整条蛇往后挪一格,给新头腾出 body[1](整条挪位走的是**覆盖已有下标**,不动容量)。

    -- Shift the whole snake one slot back to free body[1] (the shift only **overwrites existing indices**,

    -- so it does not touch the capacity).

    -- ⚠ 本前端没有「插到表头」:`table.insert(t, 1, v)` 那条 3 实参形态没实现,弹的永远是最后一个。

    -- ⚠ No "insert at front" here: the 3-argument `table.insert(t, 1, v)` is not implemented, and popping

    -- always takes the last element.

    -- 2026-10-04 复验仍成立(`local T = {1,2,3}; table.insert(T, 1, 99)` ⇒ `#T` = 4、99 落在 T[4])。

    -- Still true as of 2026-10-04 (`local T = {1,2,3}; table.insert(T, 1, 99)` => `#T` = 4, with 99 landing at T[4]).

    local i = #BODY
    while i >= 1 do
        BODY[i + 1] = BODY[i]
        i = i - 1
    end
    BODY[1] = nh

    if eating ~= 0 then
        SCORE = SCORE + 1
        -- 吃到:清亮的一声(一个事件配一个音,§4.5)

        -- ate food: one bright tone (one sound per event, 4.5)

        ui_audio_beep(880, 60)
        if SPEED > SPEED_MIN then
            SPEED = SPEED - 6
        end
        if #BODY >= WIN_LEN then
            STATE = 2
            -- 通关:最高音 + 最长

            -- won: highest and longest tone

            ui_audio_beep(1047, 320)
        else
            local nx = free_cell()
            if nx < 0 then
                STATE = 2
                ui_audio_beep(1047, 320)
            else
                FOOD = nx
            end
        end
    end
    return 1
end

-- 画一整帧

-- Draw one whole frame

function draw()
    local cell = CELL
    local ox = OX
    local oy = OY

    ui_2d_clear(-15724520)        -- 0xFF101018


    -- 标题 + 与标题等宽的下划线(宽度由 `s:len()` 数出来 —— 方法调用那条路)

    -- Title plus an underline as wide as the title (the width comes from `s:len()` — the method-call path)

    ui_2d_text(ox, 4, T_TITLE, -1513232, 12, VML_ANCHOR_LEFT)
    ui_2d_rect(ox, 18, text_w(T_TITLE) * 2, 2, -11409298, 1, 0, 0)

    -- 右侧:得分 / 长度(数字走点阵渲染器)

    -- Right side: score / length (numbers through the dot-matrix renderer)

    ui_2d_text(W - ox - 132, 6, T_SCORE, -6643536, 11, VML_ANCHOR_LEFT)
    draw_number(W - ox - 132, 20, SCORE, 3, -2509750)
    ui_2d_text(W - ox - 54, 6, T_LEN, -6643536, 11, VML_ANCHOR_LEFT)
    draw_number(W - ox - 54, 20, #BODY, 3, -11409298)

    -- 场地底 + 边框

    -- Field floor plus border

    -- 0xFF29292A 边框

    -- 0xFF29292A border

    ui_2d_rect(ox - 2, oy - 2, cell * COLS + 4, cell * ROWS + 4, -14079702, 1, 0, 4)
    ui_2d_rect(ox, oy, cell * COLS, cell * ROWS, -15066588, 1, 0, 2)

    -- 食物

    -- Food

    local fr = FOOD / COLS
    local fc = FOOD % COLS
    ui_2d_circle(ox + fc * cell + cell / 2, oy + fr * cell + cell / 2, cell / 2 - 2, -2069424, 1, 0)

    -- 蛇:头一格亮、其余暗一点(长蛇一眼看得出头在哪)

    -- Snake: the head cell is bright, the rest dimmer (so the head is obvious on a long snake)

    local i = #BODY
    while i >= 1 do
        local r = BODY[i] / COLS
        local c = BODY[i] % COLS
        local col = -11483016
        if i == 1 then
            col = -11409298
        end
        ui_2d_rect(ox + c * cell + 1, oy + r * cell + 1, cell - 2, cell - 2, col, 1, 0, 3)
        i = i - 1
    end

    -- 暂停 / 结束 / 通关横幅(结束与通关一定伴随音效,见 step)

    -- Paused / over / won banner (the over and won cases always come with a sound — see step)

    if PAUSED ~= 0 then
        ui_2d_text(W / 2, oy + cell * ROWS / 2, T_PAUSED, -1513232, 14, VML_ANCHOR_CENTER)
    end
    if STATE ~= 0 then
        local cy = oy + cell * ROWS / 2
        -- 0xFF1F2422 半透明面板

        -- 0xFF1F2422 panel

        ui_2d_rect(ox, cy - 44, cell * COLS, 88, -14736350, 1, 0, 6)
        if STATE == 2 then
            ui_2d_text(W / 2, cy - 30, T_WIN, -11409298, 18, VML_ANCHOR_CENTER)
        else
            ui_2d_text(W / 2, cy - 30, T_OVER, -2069424, 18, VML_ANCHOR_CENTER)
        end
        ui_2d_text(W / 2, cy + 4, T_AGAIN, -1513232, 13, VML_ANCHOR_CENTER)
        draw_number(W / 2 - 30, cy + 24, SCORE, 3, -2509750)
    end

    ui_2d_present()
end

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

    -- UI language: ask the host once (0=Chinese 1=English), then branch on it for the whole session.

    -- ⚠ 别在每帧里调(一次 syscall),也**不能**用 `and/or` 选串(见文件头)。

    -- ⚠ Not per frame (it is a syscall), and do not pick strings with `and/or` (see the header).

    local lang = ui_scr_get_language()
    if lang == 0 then T_TITLE = "贪吃蛇" end
    if lang == 0 then T_SCORE = "得分" end
    if lang == 0 then T_LEN = "长度" end
    if lang == 0 then T_OVER = "游戏结束" end
    if lang == 0 then T_WIN = "通关了!" end
    if lang == 0 then T_AGAIN = "回车 = 重开" end
    if lang == 0 then T_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)
    ui_scr_set_keep_on(1)

    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
    W = w

    -- 场地:格子边长取「按宽」「按高」两个上界的小者(手机可用绘图区实测约 395×578)

    -- Field: the cell side is the smaller of the two bounds (the measured phone drawing area is about 395×578)

    local cell = (w - 12) / COLS
    local avail = h - 84
    if avail / ROWS < cell then
        cell = avail / ROWS
    end
    if cell < 4 then
        cell = 4
    end
    CELL = cell
    OX = (w - cell * COLS) / 2
    OY = 48

    reset_round()
    -- 定时器:每 `SPEED` 毫秒投一条 VML_MSG_TIMER(A=定时器 id、B=tag);tag 用 7 认出它

    -- Timer: one VML_MSG_TIMER every `SPEED` ms (A = timer id, B = tag); tag 7 identifies it

    TID = ui_timer_set(SPEED, 7)
    TIMER_SPEED = SPEED

    local quit = 0
    local need_draw = 1
    -- 墙钟补步累加器(按墙钟补步,用户 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 and quit == 0 do
        -- 先画后等:只在画面真会变时才画(一帧要把整幅场景光栅化一遍,手机上不便宜)

        -- Draw before waiting, and only when the picture really changes (rasterising a whole frame is not cheap)

        if need_draw ~= 0 then
            draw()
            need_draw = 0
        end

        -- 按墙钟补步,用户 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 step 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 > SPEED * 8 then
            acc = 0        -- 卡太久别一次补一大串

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

        end
        local t = 0
        if acc < SPEED then
            t = ui_msg_fetch_wait(0)
            if t == 10 then
                -- 用户把窗口关了

                -- the user closed the window

                quit = 1
            end
        end

        if acc >= SPEED then
            acc = acc - SPEED
            -- 定时器只做唤醒;真正跑几步由上面的墙钟欠账决定

            -- The timer only wakes us; how many steps run is decided by the wall-clock debt above

            if PAUSED == 0 and STATE == 0 then
                need_draw = step()
            end
        end

        if t == 1 then
            local k = ui_msg_get_a()
            if k == 27 then
                quit = 1
            end
            if k == 13 then
                -- 回车:任何状态下都重开(结束画面里这是唯一要按的键)

                -- Enter: restart from any state (on the end screen this is the only key to press)

                reset_round()
                ui_audio_beep(660, 70)
                need_draw = 1
            end
            if k == 16 then
                if STATE == 0 then
                    if PAUSED == 0 then
                        PAUSED = 1
                    else
                        PAUSED = 0
                    end
                    need_draw = 1
                end
            end
            -- 转向键:方向键 37-40、WASD 65/68/83/87

            -- Steering keys: arrows 37-40 and WASD 65/68/83/87

            if STATE == 0 and PAUSED == 0 then
                if k == 38 or k == 87 then turn(0) end
                if k == 39 or k == 68 then turn(1) end
                if k == 40 or k == 83 then turn(2) end
                if k == 37 or k == 65 then turn(3) end
            end
        end

        if t == 6 then
            -- 触摸:朝触点相对蛇头的**主轴**方向拐(手机上没键盘也能玩)

            -- Touch: turn toward the **dominant axis** from the head to the touch point (playable with no keyboard)

            if STATE == 0 and PAUSED == 0 then
                local tx = ui_msg_get_a()
                local ty = ui_msg_get_b()
                local hr = BODY[1] / COLS
                local hc = BODY[1] % COLS
                local hx = OX + hc * CELL + CELL / 2
                local hy = OY + hr * CELL + CELL / 2
                local dx = tx - hx
                local dy = ty - hy
                if dx < 0 then dx = 0 - dx end
                if dy < 0 then dy = 0 - dy end
                if dx > dy then
                    if tx > hx then turn(1) else turn(3) end
                else
                    if ty > hy then turn(2) else turn(0) end
                end
            end
        end

        -- 吃完食物会提速:定时器要换掉(tag 仍是 7:杀掉旧的重开一个)

        -- Eating speeds the game up, so the timer is replaced (same tag 7: kill the old one, arm a new one)

        if SPEED ~= TIMER_SPEED then
            TIMER_SPEED = SPEED
            ui_timer_kill(TID)
            TID = ui_timer_set(SPEED, 7)
        end
    end

    ui_timer_kill(TID)
    ui_scr_set_keep_on(0)
    ui_sfx_panic()
    ui_win_close()
end

main()