lua/app/g2048.lua · 29342 字节 · 下载原文
-- 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()