← 都来码 / Dolaima← 例程索引csharp/app/g2048.cs原文

csharp/app/g2048.cs · 35903 字节 · 下载原文

// 2048 —— 用 **C#** 写的手机游戏(不是 C)

// 2048 — a phone game written in **C#** (not C)

//

// 玩法:4×4 棋盘,**手指滑动**(上/下/左/右)让所有方块朝那个方向压紧并合并,每次有效

// Gameplay: a 4×4 board. **Swipe** (up/down/left/right) to pack and merge every tile that way;

// 移动后随机空位冒出 2(90%)或 4(10%)。两块同值相撞合成一块、分值相加并计入得分。

// after each effective move a random empty cell spawns a 2 (90%) or a 4 (10%). Two equal tiles

// 出现 2048 即获胜(本局结束),棋盘塞满且相邻无可合并者即失败。

// that collide merge into one, and their value is added to the score. Reaching 2048 wins the

// 键盘兜底:方向键同义,R / 回车重开,ESC 退出。底部「重开」按钮点一下也重开。

// round; a full board with no adjacent equal pair loses. Keyboard fallback: the arrow keys do

// the same thing, R / Enter restart, ESC quits. The on-screen Restart button does the same.

//

// ## 为什么写得这么"平"

// ## Why it is written so "flat"

//

// C# 前端只支持**一个子集**(实测,见下),所以这里只用了被验证过的写法:

// The C# front end only supports **a subset** (measured; see below), so this only uses verified constructs:

//   · 一个类 + `static` 字段 / `static` 方法 / `const int`(**不需要实例**)

//   · one class with `static` fields / `static` methods / `const int` (**no instances needed**)

//   · `static int[]` 定长数组 + 局部定长数组(读、写、当实参传都对)

//   · `static int[]` fixed arrays plus local fixed arrays (read, write and pass-as-argument all work)

//   · `while` / `do-while` / `if-else` / `switch-case` / `?:` / `&&` / `||`

//   · 不带参的 `for(;;)`、~~`foreach`~~、`List<T>`、属性、继承、struct、`$"…"`、~~字符串 `+`~~、

//   · `for(;;)` with no condition, ~~`foreach`~~, `List<T>`, properties, inheritance, structs, `$"…"`,

//     `?.` / `??`、LINQ、委托、`out`/`ref`、`try/finally`、运算符重载 —— **这条前端都不支持**

//     ~~string `+`~~, `?.` / `??`, LINQ, delegates, `out`/`ref`, `try/finally` and operator overloading —

//     (有的是编译报错,有的是**编得过、跑起来是错的**),本文件一个都不碰。

//     **none of which this front end supports** (some fail to compile, some compile and then

//     misbehave). This file touches none of them.

//     ⚠ 2026-10-04 复验更正:`foreach` 与字符串 `+` **现在能用了**(实测 `foreach` 求和 = 6;`string c = a + b;` ⇒ `"ABCD"`),

//     ⚠ Corrected 2026-10-04: `foreach` and string `+` **now work** (measured `foreach` sum = 6; `string c = a + b;` ⇒ `"ABCD"`),

//       其余各项仍不支持(`for(;;)`/`List<T>`/属性/继承/struct/`$"…"`/`?.`/LINQ/委托/`out`·`ref`/`try/finally`/运算符重载 —— 逐条实测仍编译不过或跑错)。

//       the rest are still unsupported (`for(;;)` / `List<T>` / properties / inheritance / struct / `$"…"` / `?.` / LINQ / delegates / `out`·`ref` / `try/finally` / operator overloading — each re-measured, still failing to compile or misbehaving).

//

// ## 两条绕开子集缺口的写法(不是风格,是必需)

// ## Two workarounds forced by the subset (not style — necessity)

//

// ① ~~**字符串拼不起来**(`a + b` 编得过,但结果是空的/垃圾内存)⇒ 数字**不能拼成字符串**,~~

// ① ~~**Strings cannot be concatenated** (`a + b` compiles but yields an empty string or garbage) ⇒~~

//    ~~只能:数字**逐位画**(`drawNum`),或者查一张**字面量表**(`numText`)。~~

//    ~~numbers cannot be turned into strings; they are either **drawn digit by digit** (`drawNum`)~~

//    ~~⚠ 这条也是全文件没有 `numToStr` 这种函数的原因 —— 那种函数在这里必然是坏的。~~

//    ~~or looked up in a **table of literals** (`numText`). ⚠ That is also why this file has no~~

//    ~~`numToStr`-style helper: such a function is necessarily broken here.~~

//    ✅ **已修好(2026-10-04 复验)** —— 字符串拼接正常:`string c = a + b;`(`"AB"`,`"CD"`)实测得 `"ABCD"`;

//    ✅ **Fixed (re-measured 2026-10-04)** — string concatenation works: `string c = a + b;` (`"AB"`,`"CD"`) measured `"ABCD"`;

//       `string s = "" + n;`(n=12345)得 `"12345"`,所以 `numToStr` 这种函数**现在是能写的**(`static string NumToStr(int n){ string s = "" + n; return s; }` 实测得 `"12345"`)。

//       `string s = "" + n;` (n=12345) gives `"12345"`, so a `numToStr`-style helper **can be written now** (`static string NumToStr(int n){ string s = "" + n; return s; }` measured `"12345"`).

//       ⚠ 唯一残留:**同名重载方法**收到字符串实参时仍会被解成 `int` 重载(`Show(string)`+`Show(int)` 下 `Show(a+b)`/`Show(c)` 打印地址)—— 与拼接本身无关。

//       ⚠ One residual: an **overloaded** method still resolves a string argument to the `int` overload (`Show(a+b)`/`Show(c)` print addresses under `Show(string)`+`Show(int)`) — unrelated to concatenation itself.

// ② **数字的字宽得自己算**(宿主没有"量文字"的接口)⇒ `dw = 字号 × 14/25` 是**实测**出来的

// ② **Digit advance must be computed by hand** (the host has no text-measuring call) ⇒

//    (用 `ui_2d_get_pixel` 扫 size=24 的 "8888":墨迹 20..72,3 个推进 = 53−13 ⇒ 13.33/24 = 0.556)。

//    `dw = size × 14/25` was **measured** by scanning "8888" at size 24 with `ui_2d_get_pixel`

//    (ink spans 20..72; 3 advances = 53−13 ⇒ 13.33/24 = 0.556).

//

// ## 操作

// ## Controls

//

//   滑动棋盘(或方向键)移动;「重开」按钮 / 回车 / R 重开;ESC 或返回箭头退出。

//   Swipe the board (or use the arrow keys) to move; the Restart button / Enter / R restart;

//   ESC or the back arrow quits.

//

// 数值的唯一真源仍是 `Lib/c/vml_ui.h`。

// The single source of truth for these numbers is still `Lib/c/vml_ui.h`.


// 显式引用 UI 库:`ui_*` 的声明在 `Lib/csharp/VmlUi.cs`(tools/GenLib 生成,别手改)。

// Explicit reference to the UI library: `ui_*` is declared in `Lib/csharp/VmlUi.cs` (generated by tools/GenLib; don't hand-edit).

using Vml.Ui;

class G2048
{
    // ── 消息类型(源:Lib/c/vml_ui.h)──

    // ── Message types (source: Lib/c/vml_ui.h) ──

    const int MSG_NONE = 0;
    const int MSG_KEYDOWN = 1;
    const int MSG_TOUCHDOWN = 6;
    const int MSG_TOUCHMOVE = 7;
    const int MSG_TOUCHUP = 8;
    const int MSG_TIMER = 9;
    const int MSG_WINDOWCLOSE = 10;
    const int MSG_WINDOWRESIZE = 11;

    // ── 键码 ──

    // ── Key codes ──

    const int KEY_ENTER = 13;
    const int KEY_ESCAPE = 27;
    const int KEY_SPACE = 32;
    const int KEY_LEFT = 37;
    const int KEY_UP = 38;
    const int KEY_RIGHT = 39;
    const int KEY_DOWN = 40;
    const int KEY_PAD_A = 65;
    const int KEY_PAD_B = 66;
    const int KEY_R = 82;

    // ── 文字样式 / 锚点 / 竖对齐 ──

    // ── Text style / anchors / vertical alignment ──

    const int ANCHOR_LEFT = 0;
    const int ANCHOR_CENTER = 1;
    const int ANCHOR_RIGHT = 2;
    const int FONT_BOLD = 1;
    const int VANCHOR_MIDDLE = 1;

    // ── 界面语言 / 对话框 ──

    // ── UI language / dialog ──

    const int LANG_ZH = 0;
    const int DLG_INFO = 0;

    // ── 窗口声明(开窗之前生效)──

    // ── Window declarations (take effect before the window opens) ──

    const int WIN_PORTRAIT = 0;
    const int WIN_NO_GAMEPAD = 0;

    // ── 规则常量 ──

    // ── Rule constants ──

    const int N = 4;                 // 4×4

    const int CELLS = 16;
    const int DIR_L = 0;
    const int DIR_R = 1;
    const int DIR_U = 2;
    const int DIR_D = 3;
    const int TARGET = 2048;
    const int SWIPE_MIN = 24;        // 滑动多少像素才算一次滑动

    // How many pixels count as one swipe

    const int GAP_MIN = 2;

    // ── 颜色(0xAARRGGBB 的十进制负数形式,与仓库里其它语言例程一致)──

    // ── Colors (the decimal-negative form of 0xAARRGGBB, consistent with the other examples) ──

    const int BG = -15724520;        // 0xFF101018 底色

    // 0xFF101018 background

    const int HUD_C = -6643536;      // 0xFF9AA0B0 次要文字

    // 0xFF9AA0B0 secondary text

    const int WHITE = -1;            // 0xFFFFFFFF

    const int PANEL = -15066588;     // 0xFF1A1A24 空格子

    // 0xFF1A1A24 empty cell

    const int ACCENT = -11409298;    // 0xFF51E86E

    const int DARKTXT = -15724520;   // 0xFF101018 亮块上的字

    // 0xFF101018 text drawn on a bright tile

    const int BOARD_C = -15263713;   // 0xFF17181F 棋盘底板

    // 0xFF17181F board plate


    // 各方块的颜色(2/4/8… 越往后越亮)

    // Per-tile colors (2/4/8… brighter as the value grows)

    const int C2 = -13618602;        // 0xFF30323C

    const int C4 = -12632212;        // 0xFF3F424E

    const int C8 = -11890471;        // 0xFF4A90D9

    const int C16 = -12623695;       // 0xFF3F7CB1

    const int C32 = -2069424;        // 0xFFE06C50

    const int C64 = -2523025;        // 0xFFD9534F

    const int C128 = -2509750;       // 0xFFD9B44A

    const int C256 = -3690693;       // 0xFFC79B3B

    const int C512 = -11483016;      // 0xFF50C878

    const int C1024 = -12933028;     // 0xFF3AA85C

    const int C2048 = -11409298;     // 0xFF51E86E

    const int CBIG = -7623434;       // 0xFF8B5CF6


    // ── 状态 ──

    // ── State ──

    static int[] bd = new int[CELLS];    // 棋盘(行优先:r*4+c)

    // the board (row-major: r*4+c)

    static int[] ln = new int[N];        // 单行/单列的暂存(压紧与合并都在这上面做)

    // scratch row/column (both compaction and merging happen on it)

    static int score;
    static int best;
    static int lang;
    static int over;                     // 1 = 这一局已经结束(等重开)

    // 1 = this round is over (waiting for a restart)

    static int won;                      // 1 = 这一局已经达成 2048

    // 1 = this round already reached 2048

    static int moves;
    static int flash;                    // 合并后的高亮还剩几拍

    // how many ticks the post-merge highlight still lasts

    static int flashIdx;
    static int lastGain;                 // 上一步 slide 的合并得分

    // merge score of the last slide


    // ── 版面(开窗时算一次;画与命中判定共用同一套数)──

    // ── Layout (computed once at open; drawing and hit-testing share the same numbers) ──

    static int sw;
    static int sh;
    static int pad;
    static int cell;
    static int board;
    static int bx;
    static int by;
    static int hudH;
    static int btnX;
    static int btnY;
    static int btnW;
    static int btnH;

    // ── 触摸 ──

    // ── Touch ──

    static int sx;
    static int sy;
    static int ex;
    static int ey;
    static int down;

    // ── 每帧图元数:宿主侧的 `ui_res_get_count(0)` 报"场景里有几个图元",

    // ── Primitives per frame: the host's `ui_res_get_count(0)` reports how many primitives the

    //    每帧画完查一次、记最大值,**只在新高时打印**(被超时杀掉的运行也能留下这个数)。

    //    scene holds. It is queried once per frame and the max is printed **only when it grows**,

    //    so even a run killed by the timeout leaves the number in the log.

    static int primMax;

    // ── 界面语言(开局问一次宿主,之后整局按它分支)──

    // ── UI language (asked once at startup, then branched on for the whole round) ──

    static string T(string zh, string en)
    {
        if (lang == LANG_ZH) return zh;
        return en;
    }

    // ════════════════════════════════════════════════════════════════════

    // 版面

    // Layout

    // ════════════════════════════════════════════════════════════════════

    static void layout()
    {
        pad = sw / 24;
        if (pad < 8) pad = 8;
        hudH = 104;
        btnH = 44;
        // 棋盘是正方形:宽高两个方向都要够,取小的那个(只缩一个维度会把棋盘切掉)

        // The board is a square, so both axes must fit — take the smaller (scaling one axis

        // only would clip the board).

        int maxw = sw - pad * 2;
        btnH = 44;
        btnY = sh - btnH - 18;
        int maxh = btnY - 34 - hudH;
        board = maxw;
        if (maxh < board) board = maxh;
        cell = board / N;
        board = cell * N;
        bx = (sw - board) / 2;
        // 竖直方向在「HUD 下沿」到「按钮上沿」之间居中(只按宽度摆会留下一大块空白)

        // Centre vertically between the HUD's bottom edge and the button's top edge (laying out by

        // width alone leaves a large empty band)

        by = hudH + (maxh - board) / 2;
        btnW = sw / 3;
        if (btnW < 120) btnW = 120;
        if (btnW > sw - pad * 2) btnW = sw - pad * 2;
        btnX = (sw - btnW) / 2;
    }

    // ════════════════════════════════════════════════════════════════════

    // 数字文本

    // Number text

    //

    // ⚠ 字符串拼不起来(见文件头)⇒ 方块上的数字走**字面量表**(正好都是 2 的幂),

    // ⚠ Strings cannot be concatenated (see the header) ⇒ tile numbers use a **table of literals**

    //   得分那种任意数走**逐位画**。

    // (they are exactly the powers of two); arbitrary numbers such as the score are **drawn digit

    // by digit**.

    // ════════════════════════════════════════════════════════════════════

    static string numText(int v)
    {
        if (v == 2) return "2";
        if (v == 4) return "4";
        if (v == 8) return "8";
        if (v == 16) return "16";
        if (v == 32) return "32";
        if (v == 64) return "64";
        if (v == 128) return "128";
        if (v == 256) return "256";
        if (v == 512) return "512";
        if (v == 1024) return "1024";
        if (v == 2048) return "2048";
        if (v == 4096) return "4096";
        if (v == 8192) return "8192";
        if (v == 16384) return "16384";
        if (v == 32768) return "32768";
        if (v == 65536) return "65536";
        return "?";
    }

    static string digitText(int d)
    {
        if (d == 0) return "0";
        if (d == 1) return "1";
        if (d == 2) return "2";
        if (d == 3) return "3";
        if (d == 4) return "4";
        if (d == 5) return "5";
        if (d == 6) return "6";
        if (d == 7) return "7";
        if (d == 8) return "8";
        return "9";
    }

    static int numDigits(int v)
    {
        if (v < 10) return 1;
        if (v < 100) return 2;
        if (v < 1000) return 3;
        if (v < 10000) return 4;
        if (v < 100000) return 5;
        if (v < 1000000) return 6;
        if (v < 10000000) return 7;
        return 8;
    }

    static int pow10(int n)
    {
        if (n <= 0) return 1;
        if (n == 1) return 10;
        if (n == 2) return 100;
        if (n == 3) return 1000;
        if (n == 4) return 10000;
        if (n == 5) return 100000;
        if (n == 6) return 1000000;
        if (n == 7) return 10000000;
        return 100000000;
    }

    // 逐位画一个数。anchor:0=左 1=中 2=右(按**整串**的宽度算,不是按单字)

    // Draw a number digit by digit. anchor: 0=left 1=centre 2=right (based on the **whole string's**

    // width, not a single digit's).

    static void drawNum(int x, int y, int v, int color, int size, int anchor)
    {
        int n = numDigits(v);
        int dw = size * 14 / 25;             // 实测字宽(见文件头)

        // measured glyph width (see the file header)

        int w = n * dw;
        int startX = x;
        if (anchor == ANCHOR_CENTER) startX = x - w / 2;
        if (anchor == ANCHOR_RIGHT) startX = x - w;
        int i = 0;
        while (i < n)
        {
            int d = (v / pow10(n - 1 - i)) % 10;
            ui_2d_text(startX + i * dw, y, digitText(d), color, size, ANCHOR_LEFT);
            i = i + 1;
        }
    }

    static int tileColor(int v)
    {
        if (v == 2) return C2;
        if (v == 4) return C4;
        if (v == 8) return C8;
        if (v == 16) return C16;
        if (v == 32) return C32;
        if (v == 64) return C64;
        if (v == 128) return C128;
        if (v == 256) return C256;
        if (v == 512) return C512;
        if (v == 1024) return C1024;
        if (v == 2048) return C2048;
        return CBIG;
    }

    static int tileSize(int v)
    {
        int n = numDigits(v);
        if (n <= 2) return cell * 32 / 100;
        if (n == 3) return cell * 25 / 100;
        if (n == 4) return cell * 19 / 100;
        return cell * 15 / 100;
    }

    // ════════════════════════════════════════════════════════════════════

    // 绘制

    // Drawing

    // ════════════════════════════════════════════════════════════════════

    static void draw()
    {
        int i = 0;
        ui_2d_clear(BG);

        // ── 顶部:标题 / 分数 / 最高分 ──

        // ── Top: title / score / best ──

        ui_2d_text(sw / 2, 26, T("2048 · 都来码", "2048 · Dolaima"), WHITE, 18, ANCHOR_CENTER);
        ui_2d_text(pad, 62, T("得分", "Score"), HUD_C, 12, ANCHOR_LEFT);
        drawNum(pad, 84, score, ACCENT, 20, ANCHOR_LEFT);
        ui_2d_text(sw / 2, 62, T("最高", "Best"), HUD_C, 12, ANCHOR_CENTER);
        drawNum(sw / 2, 84, best, WHITE, 20, ANCHOR_LEFT);
        ui_2d_text(sw - pad, 62, T("步数", "Moves"), HUD_C, 12, ANCHOR_RIGHT);
        drawNum(sw - pad, 84, moves, WHITE, 20, ANCHOR_RIGHT);

        // ── 棋盘底板 ──

        // ── Board backing plate ──

        ui_2d_rect(bx - 4, by - 4, board + 8, board + 8, BOARD_C, 1, 0, 10);

        int r = 0;
        while (r < N)
        {
            int c = 0;
            while (c < N)
            {
                int idx = r * N + c;
                int v = bd[idx];
                int gap = GAP_MIN;
                int tx = bx + c * cell + gap;
                int ty = by + r * cell + gap;
                int tw = cell - gap * 2;
                int color = PANEL;
                if (v != 0) color = tileColor(v);
                // 刚合并出来的那一块闪一下(白色描边)

                // The tile that just merged flashes (a white outline)

                ui_2d_rect(tx, ty, tw, tw, color, 1, 0, 6);
                if (flash > 0 && idx == flashIdx) ui_2d_rect(tx, ty, tw, tw, WHITE, 0, 3, 6);
                if (v != 0)
                {
                    int txtc = WHITE;
                    if (v >= 8 && v <= 16) txtc = WHITE;
                    ui_2d_text_v(tx + tw / 2, ty + tw / 2, numText(v), txtc, tileSize(v),
                              ANCHOR_CENTER, VANCHOR_MIDDLE, FONT_BOLD);
                }
                c = c + 1;
            }
            r = r + 1;
        }

        // ── 底部:提示 / 重开按钮 / 终局横幅 ──

        // ── Bottom: hint / Restart button / end-of-round banner ──

        ui_2d_text(sw / 2, btnY - 14, T("滑动屏幕(或方向键)移动", "Swipe the board (or use the arrow keys)"),
                HUD_C, 12, ANCHOR_CENTER);
        ui_2d_rect(btnX, btnY, btnW, btnH, ACCENT, 1, 0, 8);
        ui_2d_text_v(btnX + btnW / 2, btnY + btnH / 2, T("重开", "Restart"), DARKTXT, 15,
                  ANCHOR_CENTER, VANCHOR_MIDDLE, FONT_BOLD);

        if (over != 0)
        {
            int bwd = sw - pad * 2 - 20;
            ui_2d_rect(pad + 10, by + board / 2 - 40, bwd, 80, -16777216, 1, 0, 12);
            ui_2d_text_v(sw / 2, by + board / 2 - 12, T("无处可动,这一局结束", "No moves left — round over"),
                      WHITE, 15, ANCHOR_CENTER, VANCHOR_MIDDLE, FONT_BOLD);
            ui_2d_text_v(sw / 2, by + board / 2 + 16, T("点屏幕或按回车重开", "Tap or press Enter to restart"),
                      ACCENT, 13, ANCHOR_CENTER, VANCHOR_MIDDLE, 0);
        }
        if (won != 0 && over == 0)
        {
            ui_2d_text(sw / 2, by - 18, T("达成 2048!", "You reached 2048!"), ACCENT, 14, ANCHOR_CENTER);
        }

        // 每帧图元数(宿主侧真值)——**只在新高时打印**,这样被超时杀掉的运行也留得下数

        // Primitives this frame (the host's own count) — printed **only when it grows**, so a run

        // killed by the timeout still leaves the number behind.

        int np = ui_res_get_count(0);
        if (np > primMax)
        {
            primMax = np;
            print_str("PRIMAX ");
            println_int(primMax);
        }

        ui_2d_present();
    }

    // ════════════════════════════════════════════════════════════════════

    // 棋盘逻辑

    // Board logic

    // ════════════════════════════════════════════════════════════════════


    // 某方向第 line 条第 k 格在棋盘数组里的下标

    // Index into the board array of the k-th cell of line #line along the given direction

    static int cellIdx(int dir, int line, int k)
    {
        if (dir == DIR_L) return line * N + k;
        if (dir == DIR_R) return line * N + (N - 1 - k);
        if (dir == DIR_U) return k * N + line;
        return (N - 1 - k) * N + line;
    }

    static int emptyCount()
    {
        int n = 0;
        int i = 0;
        while (i < CELLS)
        {
            if (bd[i] == 0) n = n + 1;
            i = i + 1;
        }
        return n;
    }

    static int randEmpty(int nth)
    {
        int seen = 0;
        int i = 0;
        while (i < CELLS)
        {
            if (bd[i] == 0)
            {
                if (seen == nth) return i;
                seen = seen + 1;
            }
            i = i + 1;
        }
        return -1;
    }

    static void spawn()
    {
        int e = emptyCount();
        if (e == 0) return;
        int idx = randEmpty(ui_math_rand(e));
        if (idx < 0) return;
        int v = 2;
        if (ui_math_rand(10) == 0) v = 4;             // 10% 出 4

        // 10% of the time a 4 appears

        bd[idx] = v;
        print_str("SPAWN ");
        print_int(idx);
        print_str(" ");
        println_int(v);
    }

    static void dumpState()
    {
        int i = 0;
        print_str("STATE");
        while (i < CELLS)
        {
            print_str(" ");
            print_int(bd[i]);
            i = i + 1;
        }
        print_str(" SCORE ");
        println_int(score);
    }

    // 朝 dir 压紧并合并。返回 1 = 棋盘变了(所以要出新的块)

    // Pack and merge towards `dir`. Returns 1 if the board changed (so a new tile must spawn).

    static int slide(int dir)
    {
        int moved = 0;
        int gain = 0;
        int line = 0;
        while (line < N)
        {
            int k = 0;
            while (k < N)
            {
                ln[k] = bd[cellIdx(dir, line, k)];
                k = k + 1;
            }

            // ① 压紧:非零的往前面挤(读 ln[k] 写 ln[w],w ≤ k,不会读脏)

            // ① Pack: push the non-zeros forward (read ln[k], write ln[w]; w ≤ k, so no stale reads)

            int w = 0;
            k = 0;
            while (k < N)
            {
                if (ln[k] != 0) { ln[w] = ln[k]; w = w + 1; }
                k = k + 1;
            }
            while (w < N) { ln[w] = 0; w = w + 1; }

            // ② 合并:同值相邻合成一块;**合并过的那一格不再参与**(k = k + 2)

            // ② Merge: equal neighbours combine; a merged cell does **not** take part again (k += 2)

            k = 0;
            while (k < N - 1)
            {
                if (ln[k] != 0 && ln[k] == ln[k + 1])
                {
                    ln[k] = ln[k] * 2;
                    ln[k + 1] = 0;
                    gain = gain + ln[k];
                    if (ln[k] == TARGET) won = 1;
                    k = k + 2;
                }
                else
                {
                    k = k + 1;
                }
            }

            // ③ 再压紧一次(合并产生的空格)

            // ③ Pack once more (the merges left holes)

            w = 0;
            k = 0;
            while (k < N)
            {
                if (ln[k] != 0) { ln[w] = ln[k]; w = w + 1; }
                k = k + 1;
            }
            while (w < N) { ln[w] = 0; w = w + 1; }

            // ④ 写回,并和原来的比一比(有没有变)

            // ④ Write back and compare with what was there (did anything change?)

            k = 0;
            while (k < N)
            {
                int idx = cellIdx(dir, line, k);
                if (bd[idx] != ln[k]) moved = 1;
                bd[idx] = ln[k];
                k = k + 1;
            }
            line = line + 1;
        }

        lastGain = gain;
        if (moved == 0) return 0;
        moves = moves + 1;
        if (gain > 0)
        {
            score = score + gain;
            if (score > best) best = score;
            flash = 3;
            // 闪哪一块:合并后棋盘上**最大的那一块**(合并出来的必然是局部最大值)

            // Which tile flashes: the **largest one** on the board after merging (a merge always

            // produces a local maximum).

            int mt = 0;
            int mi = 0;
            int j = 0;
            while (j < CELLS)
            {
                if (bd[j] > mt) { mt = bd[j]; mi = j; }
                j = j + 1;
            }
            flashIdx = mi;
        }
        print_str("MOVE ");
        print_int(dir);
        print_str(" GAIN ");
        println_int(gain);
        return 1;
    }

    static int canMove()
    {
        if (emptyCount() > 0) return 1;
        int r = 0;
        while (r < N)
        {
            int c = 0;
            while (c < N)
            {
                int v = bd[r * N + c];
                if (c + 1 < N && bd[r * N + c + 1] == v) return 1;
                if (r + 1 < N && bd[(r + 1) * N + c] == v) return 1;
                c = c + 1;
            }
            r = r + 1;
        }
        return 0;
    }

    static void reset()
    {
        int i = 0;
        while (i < CELLS) { bd[i] = 0; i = i + 1; }
        score = 0;
        moves = 0;
        over = 0;
        won = 0;
        flash = 0;
        flashIdx = -1;
        spawn();
        spawn();
        println_str("INIT");
        dumpState();
        draw();
    }

    // 走一步(dir),返回 1 = 要来一次重画

    // One move in `dir`; returns 1 if a redraw is needed

    static int doMove(int dir)
    {
        if (over != 0) return 0;
        int m = slide(dir);
        if (m == 0)
        {
            ui_audio_beep(196, 30);                    // 这一步没动:低音提示

            // the move changed nothing: low-tone cue

            print_str("NOMOVE ");
            println_int(dir);
            return 1;
        }
        int gain = lastGain;
        if (gain == 0) { flash = 0; flashIdx = -1; }   // 没合并 ⇒ 不闪(合并时 slide 已经设好了)

        // no merge => no flash (slide already set it up when merging)

        spawn();
        dumpState();
        if (won != 0)
        {
            ui_audio_beep(1318, 220);
            showWin();
            return 1;
        }
        if (gain > 0) ui_audio_beep(1047, 90);          // 合并了:高音

        // merged: high tone

        else ui_audio_beep(660, 60);                    // 只是挪了:中音

        // just moved: mid tone

        if (canMove() == 0)
        {
            over = 1;
            ui_audio_beep(131, 320);
            gameOver();
        }
        return 1;
    }

    static void showWin()
    {
        draw();
        int r = ui_dlg_msg(T("2048", "2048"),
                           T("达成 2048,你赢了!\n再来一局?", "You reached 2048 — you win!\nPlay again?"),
                           DLG_INFO);
        if (r != 0) { ui_win_close(); }
        reset();
    }

    static void gameOver()
    {
        draw();
        int r = ui_dlg_msg(T("2048", "2048"),
                           T("无处可动了,这一局结束。\n再来一局?", "No moves left — round over.\nPlay again?"),
                           DLG_INFO);
        if (r != 0) { ui_win_close(); }
        reset();
    }

    // ════════════════════════════════════════════════════════════════════

    // 输入

    // Input

    // ════════════════════════════════════════════════════════════════════

    static int inBtn(int x, int y)
    {
        if (x < btnX || x > btnX + btnW) return 0;
        if (y < btnY || y > btnY + btnH) return 0;
        return 1;
    }

    // 一次完整滑动(起终点都在手里)→ 方向;认不出返回 -1

    // A complete swipe (both ends known) → a direction; -1 if it is not a swipe

    static int swipeDir(int x0, int y0, int x1, int y1)
    {
        int dx = x1 - x0;
        int dy = y1 - y0;
        // `Math.Abs` = 前端新增的“标准方法 → 共享库 abs”映射(v0.96.592 修)

        // `Math.Abs` = the frontend's new "standard method -> shared-library abs" mapping (fixed in v0.96.592)

        int ax = Math.Abs(dx);
        int ay = Math.Abs(dy);
        if (ax < SWIPE_MIN && ay < SWIPE_MIN) return -1;
        if (ax >= ay)
        {
            if (dx > 0) return DIR_R;
            return DIR_L;
        }
        if (dy > 0) return DIR_D;
        return DIR_U;
    }

    static void Main()
    {
        // 界面语言:开局问一次宿主(0=中文 1=英文),之后整局按它分支

        // UI language: asked once of the host (0=Chinese 1=English), then branched on all round

        lang = ui_scr_get_language();
        // 开窗:尺寸传 0 ⇒ 交给宿主给真实可用区(开窗**之前**问到的只是估算值)

        // Opening: pass size 0 ⇒ the host supplies the real available area (a **pre-open** read is only an estimate)

        ui_win_open_ex("2048", 0, 0, WIN_PORTRAIT, WIN_NO_GAMEPAD);
        ui_scr_set_keep_on(1);

        // 尺寸:开窗**之后**再问一次 —— 这时 `ui_scr_get_w/h` 给的是实测画布(真值)

        // Size: re-read **after** the open — `ui_scr_get_w/h` now report the measured canvas (the real value)

        sw = ui_scr_get_w();
        sh = ui_scr_get_h();
        if (sw <= 0) sw = 411;
        if (sh <= 0) sh = 726;
        layout();
        reset();

        int tid = ui_timer_set(60, 0);
        int running = 1;
        while (running != 0 && ui_win_is_closed() == 0)
        {
            int t = ui_msg_fetch_wait(80);
            if (t == MSG_NONE) continue;
            if (t == MSG_WINDOWCLOSE) break;
            if (t == MSG_WINDOWRESIZE)
            {
                // 视口变了:用**消息自带的** A/B 重排(别回头问 ui_scr_get_w/h)

                // Viewport changed: re-lay out with the **message's own** A/B (don't re-ask ui_scr_get_w/h)

                sw = ui_msg_get_a();
                sh = ui_msg_get_b();
                if (sw <= 0) sw = 411;
                if (sh <= 0) sh = 726;
                layout();
                draw();
                continue;
            }
            if (t == MSG_TIMER)
            {
                if (flash > 0) { flash = flash - 1; draw(); }
                continue;
            }
            if (t == MSG_TOUCHDOWN)
            {
                sx = ui_msg_get_a();
                sy = ui_msg_get_b();
                ex = sx;
                ey = sy;
                down = 1;
                if (over != 0) { reset(); continue; }
                if (inBtn(sx, sy) != 0) { ui_audio_beep(880, 60); reset(); continue; }
                continue;
            }
            if (t == MSG_TOUCHMOVE)
            {
                // 拖动类消息只追最新位置 ⇒ 把积压的那一类丢掉(不然队列只涨不落)

                // Drag messages only care about the newest position ⇒ drop the backlog of that

                // kind (otherwise the queue only grows)

                ui_msg_drop(1);
                ex = ui_msg_get_a();
                ey = ui_msg_get_b();
                continue;
            }
            if (t == MSG_TOUCHUP)
            {
                ex = ui_msg_get_a();
                ey = ui_msg_get_b();
                if (down != 0 && over == 0)
                {
                    int d = swipeDir(sx, sy, ex, ey);
                    if (d >= 0) doMove(d);
                }
                down = 0;
                continue;
            }
            if (t == MSG_KEYDOWN)
            {
                int k = ui_msg_get_a();
                if (k == KEY_ESCAPE) break;
                if (k == KEY_ENTER || k == KEY_R || k == KEY_PAD_A) { reset(); continue; }
                if (k == KEY_LEFT) doMove(DIR_L);
                else if (k == KEY_RIGHT) doMove(DIR_R);
                else if (k == KEY_UP) doMove(DIR_U);
                else if (k == KEY_DOWN) doMove(DIR_D);
                continue;
            }
        }

        ui_timer_kill(tid);
        ui_scr_set_keep_on(0);
        ui_win_close();
        print_str("PRIMAX-FINAL ");
        println_int(primMax);
        println_str("g2048: done");
    }
}