-- 2048 —— 用 **Lua** 写的手机游戏 -- 2048 — a phone game written in **Lua** -- -- 规则:四方向滑动,同值方块相撞就合并成双倍并加分。 -- Rules: slide in one of four directions; equal tiles that collide merge into their double and score. -- 每滑一次补一块新方块(90% 是 2、10% 是 4)。 -- Every slide spawns one new tile (90% a 2, 10% a 4). -- 合出 2048 算赢;棋盘满且四个方向都动不了算输。 -- Reaching 2048 wins; a full board with no legal move in any direction loses. -- 操作:方向键 / WASD 滑动 · 触摸**滑动** · 回车或 R 重开 · ESC 退出。 -- Controls: arrows / WASD to slide · swipe by touch · Enter or R restarts · ESC exits. -- -- ◆ 这份例程在练什么 -- ◆ What this example exercises -- -- 这是本目录里第一个**回合制**的游戏(`snake.lua` 是定时器驱动的实时游戏、`life.lua` 是纯状态机)。 -- This is the first **turn-based** game in this directory (`snake.lua` is timer-driven and real time, -- `life.lua` is a pure state machine). -- 一次输入触发一次整体重排:「取一条线 → 压紧 → 合并 → 写回」四步对 4 行 / 4 列各做一遍。 -- One input triggers one full rearrangement: "extract a line → compact → merge → write back", run once per row -- and once per column. -- 逻辑全在**多参函数**里:`merge4(v1,v2,v3,v4)` 收 4 个格子,`line_cell(dir,i,k)` 算格子下标。 -- The logic lives in **multi-argument functions**: `merge4(v1,v2,v3,v4)` takes four cells and -- `line_cell(dir,i,k)` computes a cell index. -- -- ◆ 本语言的五条**实测约束**(与 `snake.lua` 同一套,都在这份里躲开了) -- ◆ Five **measured constraints** of this language (the same set as `snake.lua` — all avoided here) -- -- · **顶层 `local` 在函数里读不到**(野地址 ⇒ 内存错误),函数也看不见外层函数的局部。 -- · **A top-level `local` is invisible inside a function** (wild address ⇒ memory fault), and a function -- cannot see the enclosing function's locals either. -- 所以状态与逻辑**一律顶层全局**。 -- So all state and logic are **top-level globals**. -- · **空表 `{}` 只有 8 个键位**:`capacity = 字段数 + max(8, 字段数/2 + 4)`,**写满后新键被静默丢弃**。 -- · **An empty `{}` holds only 8 keys**: `capacity = fields + max(8, fields/2 + 4)`, and **new keys are -- silently dropped once it is full**. -- 实测:`A = {}` 连写 90 个下标只留下 8 个;`C = {16 个 0}` 留下 28 个。 -- Measured: `A = {}` — 8 of 90 indices survive; `C = {16 zeros}` — 28. -- ✅ 2026-10-04 复验仍成立(重测:`{}` 连写 90 个 ⇒ `#A` = 8、`{16 个 0}` ⇒ 28)。 -- ✅ Still true as of 2026-10-04 (re-measured: `{}` + 90 writes => `#A` = 8; `{16 zeros}` => 28). -- ⇒ 棋盘是 `{16 个 0}`(容量 28),临时线是 `{0,0,0,0}`(容量 12),**都按容量建表**。 -- ⇒ The board is `{16 zeros}` (capacity 28) and the scratch lines are `{0,0,0,0}` (capacity 12). -- · 别写 `t[#t + 1] = v`(内联 `#` 当下标):实测写 3 次只剩 1 个元素,要先 `local n = #t`。 -- · Do not write `t[#t + 1] = v` (inline `#` as a subscript): measured, three writes leave one element — -- take `local n = #t` first. -- · `and` / `or` 求的是布尔 0/1(见 `life.lua`)⇒ 一律 `if/else`。 -- · `and` / `or` evaluate to boolean 0/1 (see `life.lua`) ⇒ always use `if/else`. -- `tostring` / `pairs` 的路没通,数字要画出来就自己画点阵(`draw_number`,见下)。 -- `tostring` / `pairs` are not wired up, so numbers are drawn with our own dot-matrix renderer. -- · **表字面量没有 GC**:每帧 `{}` 会一直攒着 ⇒ 临时数组**建一次、反复覆盖**(`LINE` / `LINE2`)。 -- · **Table literals are never collected**: a `{}` per frame keeps piling up ⇒ scratch arrays are created -- once and overwritten in place (`LINE` / `LINE2`). -- ── 显式引用 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, the board and the state of one round ── -- 棋盘边长(4×4) -- board side (4x4) N = 4 -- 合出它就通关 -- reaching this value wins the round TARGET = 2048 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) BIT = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384} -- 棋盘:**按 16 个字段建表**拿到 28 个键位(空表 `{}` 只有 8 个,写到第 9 格起会被静默丢掉;2026-10-04 复验仍成立) -- The board: **built with 16 fields** so it gets 28 key slots (a bare `{}` has 8, and writes past the eighth are -- silently dropped; still true as of 2026-10-04) BOARD = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} -- 两条复用的临时线(容量 12):**建一次、反复覆盖**,因为本平台没有 GC,逐次 `{}` 会一直攒着 -- Two reused scratch lines (capacity 12): created once and overwritten, because there is no GC here and a fresh -- `{}` per move would pile up LINE = {0, 0, 0, 0} LINE2 = {0, 0, 0, 0} SCORE = 0 -- 0 进行 / 1 结束 / 2 通关 -- round state: 0 playing / 1 over / 2 won STATE = 0 -- 盘面上最大的方块(HUD 上画出来,不用 tostring) -- the largest tile on the board (drawn in the HUD; no tostring here) MAXTILE = 0 -- merge4 的四个输出格(本前端不做多返回值,结果落在全局) -- the four outputs of merge4 (no multiple return values here, so they land in globals) M1 = 0 M2 = 0 M3 = 0 M4 = 0 -- 这一步有没有真的动过(动了才补新块、才重画) -- did this slide really move anything (only then spawn a tile and redraw) MOVED = 0 -- 这一步合并拿到的分(一次滑动里累加,滑完一次性记进 SCORE) -- score gained by merges this slide (accumulated per line, added to SCORE once at the end) GAIN = 0 W = 360 CELL = 80 OX = 8 OY = 96 -- 界面文字:开局按 `ui_scr_get_language()` 选一次(见 main) -- UI strings: chosen once from `ui_scr_get_language()` at startup (see main) T_TITLE = "2048" T_SCORE = "SCORE" T_MAX = "MAX" T_OVER = "NO MOVES LEFT" T_WIN = "YOU WIN!" T_AGAIN = "Enter = restart" T_HELP = "Swipe or use the arrow keys" -- 画一个 3×5 点阵数字:`d` 是数字、`px` 是一格边长、`color` 是真彩色 -- Draw one 3×5 dot-matrix digit: `d` the digit, `px` the cell side, `color` a truecolor value 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 the masks were generated with: idx = row*3 + col; the table is 1-based ⇒ +1 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 格间距),最后一位不算间距 -- Width of a number in cells: four cells per digit (three glyph + one gap), the last digit has no gap function num_w(v) local digits = 1 local n = v while n >= 10 do n = n / 10 digits = digits + 1 end return digits * 4 - 1 end -- 画一个非负整数并**居中**在 (cx, cy):先用 num_w 量宽度,再算左上角 -- Draw a non-negative integer **centred** at (cx, cy): measure with num_w first, then compute the top-left corner function draw_number_centered(cx, cy, v, px, color) if v < 0 then v = 0 end if v > 999999 then v = 999999 end local wcells = num_w(v) local x = cx - wcells * px / 2 local y = cy - 5 * px / 2 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 -- 左对齐画一个非负整数(HUD 用) -- Draw a non-negative integer left-aligned (used by the HUD) 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()` ⇒ `string.len(s)`, measured working; it counts **bytes**) function text_w(s) return s:len() end -- 方块配色:按数值回一个真彩色(0 表示空格,用底色) -- Tile color: one truecolor per value (0 = empty, drawn with the board color) function tile_color(v) if v == 0 then -- 0xFF1F2422 空格 -- 0xFF1F2422 empty cell return -14736350 end if v == 2 then return -13684674 -- 0xFF2F303E end if v == 4 then return -12631730 -- 0xFF3F414E end if v == 8 then return -2069424 -- 0xFFE06C50 end if v == 16 then return -2056112 -- 0xFFE0A050 end if v == 32 then return -2509750 -- 0xFFD9B44A end if v == 64 then return -11483016 -- 0xFF50C878 end if v == 128 then return -11890471 -- 0xFF4A90D9 end if v == 256 then return -8823591 -- 0xFF795CD9 end if v == 512 then return -2532194 -- 0xFFD95C9E end if v == 1024 then return -11409298 -- 0xFF51E86E end -- 0xFFFFE060 2048 及以上 -- 0xFFFFE060 2048 and above return -8096 end -- 数字在方块上的颜色:亮底用深字、暗底用亮字(对比度是能看清的前提) -- Digit color on a tile: dark text on light tiles, light text on dark tiles (contrast is what makes it readable) function tile_ink(v) if v >= 8 then -- 0xFF101018 深字(亮底) -- 0xFF101018 dark ink on bright tiles return -15724520 end -- 0xFFE8E8F0 亮字(暗底) -- 0xFFE8E8F0 light ink on dark tiles return -1513232 end -- 格子下标:`dir` 方向、第 `i` 条线、线上第 `k` 格(`k` 从「靠前」那一侧数) -- Cell index: direction `dir`, line `i`, position `k` along that line (k counted from the leading side) -- 下标 = 行*4 + 列 + 1(本前端的表是 1 基的) -- Index = row*4 + col + 1 (tables here are 1-based) -- 0 上 / 1 左 / 2 下 / 3 右(与滑动方向同名,见 do_move) -- 0 up / 1 left / 2 down / 3 right (named after the slide direction, see do_move) function line_cell(dir, i, k) if dir == 1 then -- 左:第 i 行,从左数第 k 个 -- left: row i, the k-th cell counting from the left return i * N + k + 1 end if dir == 3 then -- 右:同一行,从右数第 k 个 -- right: the same row, the k-th cell counting from the right return i * N + (N - 1 - k) + 1 end if dir == 0 then -- 上:第 i 列,从上往下第 k 个 -- up: column i, the k-th cell counting from the top return k * N + i + 1 end -- 下:同一列,从下往上第 k 个 -- down: the same column, the k-th cell counting from the bottom return (N - 1 - k) * N + i + 1 end -- 一条线(4 个格子的值)压紧 + 合并,结果写进全局 M1..M4、得分增量写进 GAIN、动过没有写进 MOVED。 -- Compact and merge one line (four cell values); the result goes into M1..M4, the score gain into GAIN, -- and whether anything moved into MOVED. -- 多参函数形态:4 个格子直接当形参传进来,不建临时表。 -- The multi-argument form: the four cells come in as parameters, so no temporary table is built. function merge4(v1, v2, v3, v4) -- ① 压紧:把非 0 的往前提(`LINE[n] = v` 覆盖的是**已有下标**,不碰容量) -- ① Compact: move non-zero values forward (`LINE[n] = v` overwrites an existing index, capacity untouched) local n = 0 if v1 ~= 0 then n = n + 1 LINE[n] = v1 end if v2 ~= 0 then n = n + 1 LINE[n] = v2 end if v3 ~= 0 then n = n + 1 LINE[n] = v3 end if v4 ~= 0 then n = n + 1 LINE[n] = v4 end -- ② 合并:相邻同值合成双倍,加一次分(一次滑动里同一块只能合一次 —— 靠跳过被吃掉的那格实现) -- ② Merge: adjacent equal values double and score once (a tile merges only once per slide — done by skipping -- the swallowed cell) local m = 0 local i = 1 while i <= n do local v = LINE[i] if i < n and LINE[i + 1] == v then v = v * 2 GAIN = GAIN + v i = i + 1 end m = m + 1 LINE2[m] = v i = i + 1 end -- ③ 补 0 到 4 格 -- ③ Pad back to four cells while m < 4 do m = m + 1 LINE2[m] = 0 end M1 = LINE2[1] M2 = LINE2[2] M3 = LINE2[3] M4 = LINE2[4] -- ④ 有没有动过(写回前比一次;`MOVED` 决定要不要补新块) -- ④ Did anything move? (compared before writing back; `MOVED` decides whether a new tile spawns) if M1 ~= v1 or M2 ~= v2 or M3 ~= v3 or M4 ~= v4 then MOVED = 1 end end -- 找个空格放新方块:随机试 200 次,试不出来(棋盘快满了)就线性扫 -- Pick an empty cell for the new tile: 200 random tries, then a linear scan when the board is nearly full function spawn_tile() local total = N * N local slot = -1 local tries = 0 while tries < 200 do local idx = ui_math_rand(total) + 1 if BOARD[idx] == 0 then slot = idx -- `break` 在本前端是好的(见 demo_ui.lua 的说明) -- `break` works on this front end (see the note in demo_ui.lua) break end tries = tries + 1 end if slot < 0 then local j = 1 while j <= total do if BOARD[j] == 0 then slot = j break end j = j + 1 end end if slot < 0 then return 0 end -- 90% 出 2、10% 出 4 -- 90% of the time a 2, 10% of the time a 4 local r = ui_math_rand(10) if r == 0 then BOARD[slot] = 4 else BOARD[slot] = 2 end return 1 end -- 重开一局:清盘、放两块、分数与状态复位 -- Restart: clear the board, place two tiles, reset score and state function reset_game() local i = 1 while i <= N * N do BOARD[i] = 0 i = i + 1 end SCORE = 0 STATE = 0 MAXTILE = 0 GAIN = 0 spawn_tile() spawn_tile() -- ⚠ MAXTILE 要在这里就填一次:它原本只在 `do_move` 末尾算,于是开局 HUD 上会一直挂着 0。 -- ⚠ MAXTILE is also filled here: it used to be computed only at the end of `do_move`, so the HUD showed 0 -- at the start even though two 2s were already on the board. MAXTILE = max_tile() end -- 还能不能动:有空格就能动;没有空格就看有没有相邻同值 -- Is any move still possible: an empty cell means yes; otherwise look for an adjacent equal pair function any_move_possible() local i = 1 while i <= N * N do if BOARD[i] == 0 then return 1 end i = i + 1 end local r = 0 while r < N do local c = 0 while c < N do local v = BOARD[r * N + c + 1] if c + 1 < N and BOARD[r * N + c + 2] == v then return 1 end if r + 1 < N and BOARD[(r + 1) * N + c + 1] == v then return 1 end c = c + 1 end r = r + 1 end return 0 end -- 盘面上最大的方块(HUD 画它 —— 离 2048 还有多远一眼看得出) -- The largest tile on the board (drawn in the HUD — how far 2048 is, at a glance) function max_tile() local best = 0 local i = 1 while i <= N * N do if BOARD[i] > best then best = BOARD[i] end i = i + 1 end return best end -- 往 `dir` 滑一次:4 条线各跑一遍 merge4,然后写回、补块、判胜负 -- Slide once toward `dir`: run merge4 over the four lines, write back, spawn, and test win/lose -- 返回 1 表示这一步改了盘面(喂给主循环的「要不要重画」) -- Returns 1 when the move changed the board (fed to the main loop's "should we redraw") function do_move(dir) MOVED = 0 GAIN = 0 local i = 0 while i < N do local a = BOARD[line_cell(dir, i, 0)] local b = BOARD[line_cell(dir, i, 1)] local c = BOARD[line_cell(dir, i, 2)] local d = BOARD[line_cell(dir, i, 3)] merge4(a, b, c, d) -- 写回时**必须按同一个方向顺序**放(左/上从 `k=0` 起,右/下也按「靠前」那一侧起) -- The write-back **must use the same directional order** (left/up start at `k=0`; right/down also start -- from their leading side) BOARD[line_cell(dir, i, 0)] = M1 BOARD[line_cell(dir, i, 1)] = M2 BOARD[line_cell(dir, i, 2)] = M3 BOARD[line_cell(dir, i, 3)] = M4 i = i + 1 end if MOVED == 0 then -- ⚠ 滑不动也要判一次胜负:正常情况下「无路可走」是在**上一次成功移动补完新块之后** -- ⚠ A blocked slide still has to test win/lose: normally "no moves left" is caught right below by -- 立刻被下面的 `any_move_possible()` 抓到的,但那种"先卡住、再按键"的顺序也要兜住 —— -- `any_move_possible()` after the spawn that follows the previous successful move, but the -- 否则玩家会看到"按键只响一声、游戏永远不结束"(实测:塞一个满盘四向皆堵的盘面, -- "already stuck, then press a key" order must be covered too — otherwise the player sees a beep and a -- 按左键只会一直响 196,横幅永远不出来) -- game that never ends (measured: with a full board stuck in all four directions, pressing left only -- beeps 196 forever and the banner never appears) if any_move_possible() == 0 then STATE = 1 ui_audio_beep(131, 320) return 1 end -- 滑不动:一声很低的"闷"提示(不是错误,不打断节奏) -- blocked slide: one low thud (not an error, it does not break the rhythm) ui_audio_beep(196, 40) return 0 end SCORE = SCORE + GAIN if GAIN > 0 then -- 合并成功:清亮的一声(得分多少由画面说话) -- a merge happened: one bright tone (the picture tells you how much it scored) ui_audio_beep(523, 60) end spawn_tile() MAXTILE = max_tile() if MAXTILE >= TARGET then STATE = 2 -- 通关:最高音 + 最长 -- won: highest and longest tone ui_audio_beep(1047, 320) return 1 end if any_move_possible() == 0 then STATE = 1 -- 走投无路:最低音 + 最长(§4.5「输取最低」) -- no moves left: lowest and longest tone (4.5: take the lowest for a loss) ui_audio_beep(131, 320) end return 1 end -- 画一整帧 -- Draw one whole frame function draw() local cell = CELL local ox = OX local oy = OY local gap = 6 ui_2d_clear(-15724520) -- 0xFF101018 -- 标题 + 与标题等宽的下划线(宽度由 `s:len()` 数出来 —— 方法调用那条路) -- Title plus an underline as wide as the title (width from `s:len()` — the method-call path) ui_2d_text(ox, 8, T_TITLE, -1513232, 20, VML_ANCHOR_LEFT) ui_2d_rect(ox, 34, text_w(T_TITLE) * 4, 3, -2069424, 1, 0, 0) -- HUD:得分 / 最大方块(都走点阵渲染器) -- HUD: score / largest tile (both through the dot-matrix renderer) ui_2d_text(W - ox - 150, 8, T_SCORE, -6643536, 11, VML_ANCHOR_LEFT) draw_number(W - ox - 150, 22, SCORE, 3, -2509750) ui_2d_text(W - ox - 60, 8, T_MAX, -6643536, 11, VML_ANCHOR_LEFT) draw_number(W - ox - 60, 22, MAXTILE, 3, -11409298) -- 棋盘底 -- Board floor ui_2d_rect(ox - gap, oy - gap, cell * N + gap * 2, cell * N + gap * 2, -14079702, 1, 0, 8) -- 16 个格子 -- The sixteen cells local r = 0 while r < N do local c = 0 while c < N do local v = BOARD[r * N + c + 1] local x = ox + c * cell local y = oy + r * cell ui_2d_rect(x + gap / 2, y + gap / 2, cell - gap, cell - gap, tile_color(v), 1, 0, 6) if v ~= 0 then -- 位数多(≥ 4 位)时把字缩小一档,免得顶出格子 -- Values with four or more digits use a smaller step so they do not overflow the tile local px = 5 if v >= 1024 then px = 4 end if v >= 16384 then px = 3 end draw_number_centered(x + cell / 2 + 2, y + cell / 2, v, px, tile_ink(v)) end c = c + 1 end r = r + 1 end -- 结束 / 通关的横幅(都伴随音效,见 do_move) -- The over / won banner (each comes with a sound — see do_move) if STATE ~= 0 then local cy = oy + cell * N / 2 ui_2d_rect(ox - gap, cy - 58, cell * N + gap * 2, 116, -14736350, 1, 0, 8) if STATE == 2 then ui_2d_text(W / 2, cy - 40, T_WIN, -11409298, 22, VML_ANCHOR_CENTER) else ui_2d_text(W / 2, cy - 40, T_OVER, -2069424, 20, VML_ANCHOR_CENTER) end ui_2d_text(W / 2, cy - 6, T_AGAIN, -1513232, 13, VML_ANCHOR_CENTER) draw_number_centered(W / 2, cy + 34, SCORE, 4, -2509750) end ui_2d_text(W / 2, oy + cell * N + 20, T_HELP, -6643536, 12, VML_ANCHOR_CENTER) 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_SCORE = "得分" end if lang == 0 then T_MAX = "最大" end if lang == 0 then T_OVER = "走不动了" end if lang == 0 then T_WIN = "合出 2048!" end if lang == 0 then T_AGAIN = "回车 = 重开" end if lang == 0 then T_HELP = "滑动屏幕 / 方向键移动" 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("2048", 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) -- The board side is the smaller of the width and height bounds (the measured phone drawing area is ~395×578) local cell = (w - 24) / N local avail = h - 200 if avail / N < cell then cell = avail / N end if cell < 20 then cell = 20 end CELL = cell OX = (w - cell * N) / 2 OY = 96 reset_game() local quit = 0 local need_draw = 1 -- 按下的位置(触摸滑动要用「按下 → 抬起」的位移判方向) -- where the press started (a swipe needs the press-to-release displacement to pick a direction) local td_x = 0 local td_y = 0 while ui_win_is_closed() == 0 and quit == 0 do -- 先画后等:只在盘面真会变时才画 -- Draw before waiting, and only when the board really changes if need_draw ~= 0 then draw() need_draw = 0 end local t = ui_msg_fetch_wait(0) if t == 10 then quit = 1 end if t == 1 then local k = ui_msg_get_a() if k == 27 then quit = 1 end -- 回车 / R -- Enter or R if k == 13 or k == 82 then reset_game() ui_audio_beep(660, 70) need_draw = 1 end if STATE == 0 then if k == 38 or k == 87 then need_draw = do_move(0) end if k == 37 or k == 65 then need_draw = do_move(1) end if k == 40 or k == 83 then need_draw = do_move(2) end if k == 39 or k == 68 then need_draw = do_move(3) end end end -- 触摸:按下时记位置、抬起时按位移的主轴判方向(真正的"滑动",不是点击) -- Touch: record the press, then on release use the dominant axis of the displacement (a real swipe, not a tap) if t == 6 then td_x = ui_msg_get_a() td_y = ui_msg_get_b() end if t == 8 then local ux = ui_msg_get_a() local uy = ui_msg_get_b() local dx = ux - td_x local dy = uy - td_y local adx = dx local ady = dy if adx < 0 then adx = 0 - adx end if ady < 0 then ady = 0 - ady end -- 位移太小当点击处理(免得误触就把盘面搅了) -- Too small a displacement is treated as a tap (so an accidental touch does not scramble the board) if adx > 24 or ady > 24 then if STATE == 0 then if adx > ady then if dx > 0 then need_draw = do_move(3) else need_draw = do_move(1) end else if dy > 0 then need_draw = do_move(2) else need_draw = do_move(0) end end end end end end ui_scr_set_keep_on(0) ui_sfx_panic() ui_win_close() end main()