/* chess.c —— 中国象棋（人机对战），跑在手机端 VML 上
 * chess.c -- Chinese chess (human vs computer), running on VML on mobile.
 *
 * 用法（手机 App 的 vml 工具）：  vml run examples/c/chess.c
 * Usage (with the mobile app's vml tool):  vml run examples/c/chess.c
 * 桌面（无 UI 号段，只能验编译与规则）：
 * Desktop (no UI syscall range -- only compilation and rules can be checked):
 *     vmlcli chess.c -I . --vml /tmp/chess.vml        # 只编译
 *     vmlcli chess.c -I . --vml /tmp/chess.vml        # compile only
 *     vmlcli .scratch/chessprobe/probe.c -I Examples/c   # 跑规则自测（见 .scratch/chessprobe）
 *     vmlcli .scratch/chessprobe/probe.c -I Examples/c   # run the rules self-test (see .scratch/chessprobe)
 *
 * 界面全部用绘图指令画出来（棋盘线、九宫斜线、楚河汉界、棋子圆 + 居中汉字），
 * The whole UI is drawn with drawing commands (board lines, palace diagonals, the river, piece circles + centred characters),
 * 不依赖任何图片与字体资源；尺寸按实际可用绘图区自适应。
 * with no image or font assets at all; sizes adapt to the actually available drawing area.
 *
 * ## 规则部分与界面部分是**分开的**
 * ## The rules half and the UI half are **separate**
 *
 * 上面那一半（`can_move` / `in_check` / `legal_move` / `gen_moves` / `has_legal` /
 * The upper half (`can_move` / `in_check` / `legal_move` / `gen_moves` / `has_legal` /
 * `ai_pick`）**没有一处开窗、画图或读输入**，因此可以在桌面被一个探针 `#include`
 * `ai_pick`) **never opens a window, draws, or reads input**, so a desktop probe can `#include`
 * 进来单测（`#ifndef CHESS_LIB_ONLY` 包住 main 就是为这个）—— 象棋的规则漏洞（蹩马腿、
 * it for unit tests (that is what wrapping main in `#ifndef CHESS_LIB_ONLY` is for) -- xiangqi rule holes (blocked horse leg,
 * 塞象眼、炮翻山、将帅照面）在手机上靠点屏幕是**测不完**的，必须有可跑的判据。
 * blocked elephant eye, cannon screen, facing kings) cannot be tested exhaustively by tapping a phone screen, so a runnable check is a must.
 * 判据里最硬的一条：**开局红方合法着法恰好 44 步**（象棋 perft(1) 的标准值），
 * The hardest check of all: **red has exactly 44 legal moves in the opening position** (the standard perft(1) value for xiangqi) --
 * 数不对就是走法生成器错了。
 * get that count wrong and the move generator is broken.
 *
 * ⚠ 唯一的例外是 `ai_pick` 末尾那句 `ui_math_rand(9)`（同分抖动）—— 它**不是**开窗/绘图，
 * ⚠ The only exception is the `ui_math_rand(9)` at the end of `ai_pick` (tie-break jitter) -- that is **not** window/draw code,
 *   落到 VM 的 `#50` 号 syscall 上，所以探针里照样调得动（实测：
 *   it lands on the VM's syscall `#50`, so the probe can still call it (measured:
 *   `.scratch/chessprobe/aiprobe.c` 直接调 `ai_pick` 断言它该不该吃白送的子）。
 *   `.scratch/chessprobe/aiprobe.c` calls `ai_pick` directly and asserts whether it should take a free piece).
 *   真要在没有 `ui_*` 号段的宿主上复用这一半，把那句换成常量即可。
 *   To reuse this half on a host without the `ui_*` syscall range, just replace that line with a constant.
 *
 * ## 为什么不用全局变量
 * ## Why no global variables
 *
 * 与 gomoku.c 同一条约束：`GenerateAsmStatement` 的 `${名}` 只查**局部**变量表，
 * Same constraint as gomoku.c: the `${name}` in `GenerateAsmStatement` only consults the **local** variable table,
 * 全局变量在 dataSection 里、查不到，会退化成 `R12+0`（**2026-10-04 复验仍成立**）。所以棋盘是 main 的局部数组、
 * a global lives in dataSection, is not found, and degrades to `R12+0` (**still holds, re-checked 2026-10-04**). So the board is a local array in main,
 * 靠参数传下去；调宿主一律走 `vml_ui.h` 的包装函数（形参是普通 C 参数，安全）。
 * passed down as a parameter; every host call goes through the `vml_ui.h` wrappers (plain C parameters, safe).
 *
 * ## 棋盘坐标
 * ## Board coordinates
 *
 * 9 列 × 10 行，扁平数组 `b[y * 9 + x]`。**y = 0 是黑方底线（屏幕上边）**，
 * 9 columns x 10 rows, flat array `b[y * 9 + x]`. **y = 0 is black's back rank (top of the screen)**,
 * y = 9 是红方底线（屏幕下边）。人是红方（在下），电脑是黑方（在上）。
 * y = 9 is red's back rank (bottom of the screen). The human is red (bottom), the computer is black (top).
 * 棋子编码 = `方 * 10 + 种类`：红 1..7、黑 11..17，0 = 空 —— 于是
 * Piece encoding = `side * 10 + kind`: red 1..7, black 11..17, 0 = empty -- so
 * `v / 10` 是方、`v % 10` 是种类，两个判断都是一次整除，没有负号参与。
 * `v / 10` is the side and `v % 10` is the kind; both tests are one integer division and no negative sign is involved.
 */

#include <vml_ui.h>

#define BW 9
#define BH 10
/* 数组维度一律写这个字面量：`int t[BW * BH]` 要求前端对宏表达式做常量折叠，不值得赌。
 * Array dimensions always use this literal: `int t[BW * BH]` would need the frontend to
 * ⚠ 注释**不能跨行**跟在 `#define` 同一行后面 —— 前端按行处理 `#define`，跨行的块注释
 * ⚠ A comment must NOT span lines after a `#define` on the same line -- the frontend handles `#define` line by line,
 *   会在换行处被截断，第二行（以 `*` 开头）就当成代码了，里面的全角标点直接报
 *   a multi-line block comment is cut at the newline, the second line (starting with `*`) is taken as code, and its
 *   「未知字符：，」。实测过（本文件 v1 就是这么红的）。
 *   full-width punctuation reports "unknown character". Measured -- version 1 of this file went red exactly this way. */
#define CELLS 90

/* 种类（v % 10） */
/* Kinds (v % 10) */
#define T_K 1   /* 帅 / 将 */
/* King / General */
#define T_A 2   /* 仕 / 士 */
/* Advisor */
#define T_B 3   /* 相 / 象 */
/* Elephant */
#define T_N 4   /* 马 */
/* Horse */
#define T_R 5   /* 车 */
/* Chariot */
#define T_C 6   /* 炮 */
/* Cannon */
#define T_P 7   /* 兵 / 卒 */
/* Pawn / Soldier */

/* 方（v / 10） */
/* Side (v / 10) */
#define S_RED 0
#define S_BLK 1

#define MAXM 240   /* 着法上限：合法着法远不到这里，纯粹防越界 */
/* Move cap: legal moves never come near it; this only guards against overrun */

/* 配色 */
/* Colors */
#define COL_BG      0xFF14100C
#define COL_BOARD   0xFFE9C88F
#define COL_LINE    0xFF6B4A22
#define COL_RED     0xFFB3261E
#define COL_BLACK   0xFF14100C
#define COL_PIECE   0xFFF8EEDC
#define COL_TEXT    0xFFEDEDF2
#define COL_DIM     0xFF8A8A93
#define COL_SEL     0xFF1B7F3B
#define COL_BTN     0xFF2A2A33
#define COL_BTN_TX  0xFFEDEDF2
#define COL_ACCENT  0xFFE8B23A

/* 界面语言：开局查一次（ui_scr_get_language 是 syscall，别每帧调）。
 * UI language: queried once at startup (`ui_scr_get_language` is a syscall, do not call it every frame).
 * chess.c 的规则层刻意没有全局状态（可在桌面单测），这里是唯一的例外 —— 它只影响界面文字。
 * The rules layer of chess.c deliberately has no global state (so it is desktop-testable); this is the only exception -- it affects UI text only. */
int g_lang;

/* ═══════════════════════ 规则（不碰 ui_*，可在桌面单测）═══════════════════════ */
/* ═══════════════════════ Rules (no ui_* calls, desktop-testable) ═══════════════════════ */

/* 象/相 不过河：红方只能待在 y>=5，黑方只能 y<=4 */
/* Elephant never crosses the river: red may only stay at y>=5, black only at y<=4 */
int elephant_side_ok(int y, int s) {
    if (s == S_RED) { if (y >= 5) return 1; return 0; }
    if (y <= 4) return 1;
    return 0;
}

/* 九宫：列恒为 3..5；红方行 7..9，黑方行 0..2 */
/* The palace: columns are always 3..5; rows 7..9 for red, rows 0..2 for black */
int in_palace(int x, int y, int s) {
    if (x < 3 || x > 5) return 0;
    if (s == S_RED) { if (y >= 7 && y <= 9) return 1; return 0; }
    if (y >= 0 && y <= 2) return 1;
    return 0;
}

/* 兵/卒 过河了吗：红方向前是 y 变小，过河即 y<=4；黑方相反 */
/* Has the pawn crossed the river? Red moves forward as y shrinks, so crossing means y<=4; black is the other way round */
int pawn_crossed(int y, int s) {
    if (s == S_RED) { if (y <= 4) return 1; return 0; }
    if (y >= 5) return 1;
    return 0;
}

/* (fx,fy)→(tx,ty) 之间夹着几个子（不含两端）。不同行不同列返回 -1。
 * How many pieces sit between (fx,fy) and (tx,ty) (exclusive)? Returns -1 when not on one row or column.
 * 车/炮 共用它 —— 炮"翻山"的炮架计数与车的"路径必须干净"是同一件事的两种用法。
 * Chariot and cannon share it -- counting screens for the cannon's "hop over" and requiring a clear path for the chariot are two uses of the same thing. */
int between_count(int* b, int fx, int fy, int tx, int ty) {
    int dx;
    int dy;
    int x;
    int y;
    int n;

    if (fx != tx && fy != ty) return -1;
    dx = 0;
    dy = 0;
    if (tx > fx) dx = 1;
    if (tx < fx) dx = -1;
    if (ty > fy) dy = 1;
    if (ty < fy) dy = -1;
    x = fx + dx;
    y = fy + dy;
    n = 0;
    while (x != tx || y != ty) {
        if (b[y * BW + x] != 0) n = n + 1;
        x = x + dx;
        y = y + dy;
    }
    return n;
}

/* 这步棋**符合该子的走法**吗（只看走法，不看走完会不会自己被将）。
 * Does this move **match the piece's movement rule** (movement only, ignoring whether it exposes your own king)?
 * 这是走法规则的**唯一实现**：走法生成本身也只管"落点候选"，成不成由它判。
 * This is the **single implementation** of the movement rules: move generation only proposes landing squares and this decides whether each is allowed. */
int can_move(int* b, int fx, int fy, int tx, int ty) {
    int v;
    int s;
    int tp;
    int dx;
    int dy;
    int ax;
    int ay;
    int n;
    int mid;

    if (fx < 0 || fx >= BW || fy < 0 || fy >= BH) return 0;
    if (tx < 0 || tx >= BW || ty < 0 || ty >= BH) return 0;
    if (fx == tx && fy == ty) return 0;

    v = b[fy * BW + fx];
    if (v == 0) return 0;
    s = v / 10;
    tp = v % 10;

    /* 目标点不能是自己的子 */
    /* The target square must not hold one of your own pieces */
    if (b[ty * BW + tx] != 0 && b[ty * BW + tx] / 10 == s) return 0;

    dx = tx - fx;
    dy = ty - fy;
    ax = dx;
    if (ax < 0) ax = 0 - ax;
    ay = dy;
    if (ay < 0) ay = 0 - ay;

    if (tp == T_R) {                       /* 车：直线、路径干净 */
        /* Chariot: straight line, clear path */
        if (dx != 0 && dy != 0) return 0;
        if (between_count(b, fx, fy, tx, ty) != 0) return 0;
        return 1;
    }
    if (tp == T_C) {                       /* 炮：不吃子时同车；吃子必须正好翻一个炮架 */
        /* Cannon: like the chariot when not capturing; to capture it must hop exactly one screen */
        if (dx != 0 && dy != 0) return 0;
        n = between_count(b, fx, fy, tx, ty);
        if (b[ty * BW + tx] == 0) { if (n == 0) return 1; return 0; }
        if (n == 1) return 1;
        return 0;
    }
    if (tp == T_N) {                       /* 马：走日 + **蹩马腿** */
        /* Horse: the "day" move + the **blocked horse leg** */
        if (!((ax == 1 && ay == 2) || (ax == 2 && ay == 1))) return 0;
        if (ax == 2) {
            mid = (fx + tx) / 2;           /* 先横着走两格 → 横方向的相邻格是腿 */
            /* Two squares horizontally first -> the adjacent square across is the leg */
            if (b[fy * BW + mid] != 0) return 0;
        } else {
            mid = (fy + ty) / 2;           /* 先竖着走两格 → 竖方向的相邻格是腿 */
            /* Two squares vertically first -> the adjacent square along is the leg */
            if (b[mid * BW + fx] != 0) return 0;
        }
        return 1;
    }
    if (tp == T_B) {                       /* 相/象：走田 + **塞象眼** + 不过河 */
        /* Elephant: the 2x2 move + the **blocked elephant eye** + never crossing the river */
        if (ax != 2 || ay != 2) return 0;
        if (elephant_side_ok(ty, s) == 0) return 0;
        if (b[((fy + ty) / 2) * BW + (fx + tx) / 2] != 0) return 0;
        return 1;
    }
    if (tp == T_A) {                       /* 仕/士：走斜一格，不出九宫 */
        /* Advisor: one square diagonally, never leaving the palace */
        if (ax != 1 || ay != 1) return 0;
        if (in_palace(tx, ty, s) == 0) return 0;
        return 1;
    }
    if (tp == T_K) {                       /* 帅/将：走直一格，不出九宫（照面规则在 in_check 里） */
        /* King: one square orthogonally, never leaving the palace (the facing rule lives in in_check) */
        if (ax + ay != 1) return 0;
        if (in_palace(tx, ty, s) == 0) return 0;
        return 1;
    }
    if (tp == T_P) {                       /* 兵/卒：只进不退；过河后才能横走 */
        /* Pawn: forward only, never back; it may step sideways only after crossing the river */
        if (s == S_RED) {
            if (dx == 0 && dy == -1) return 1;
            if (pawn_crossed(fy, s) == 1 && dy == 0 && ax == 1) return 1;
            return 0;
        }
        if (dx == 0 && dy == 1) return 1;
        if (pawn_crossed(fy, s) == 1 && dy == 0 && ax == 1) return 1;
        return 0;
    }
    return 0;
}

/* 两个帅/将是否**照面**（同一列且中间无子）—— 这是象棋特有的禁着：
 * Do the two kings **face each other** (same column, nothing in between)? That is a xiangqi-specific illegal move:
 * 谁走成照面就等于把帅送给对方吃，所以它必须算作"被将"。
 * walking into a facing position is handing your king over to be captured, so it must count as "in check". */
int kings_face(int* b) {
    int rk;
    int bk;
    int i;
    int x;
    int y;
    int y1;
    int y2;
    int t;
    int cnt;

    rk = -1;
    bk = -1;
    for (i = 0; i < BW * BH; i = i + 1) {
        if (b[i] == S_RED * 10 + T_K) rk = i;
        if (b[i] == S_BLK * 10 + T_K) bk = i;
    }
    if (rk < 0 || bk < 0) return 0;
    if (rk % BW != bk % BW) return 0;
    x = rk % BW;
    /* ⚠ 两个帅**谁在谁的上面不定**：红在下（y 大）、黑在上（y 小），所以红帅的下标
     * ⚠ Which king is above the other is **not fixed**: red is at the bottom (larger y), black at the top
     *   **大于**黑将。这里必须比较后取小/大 —— 直接 `y = rk/BW + 1; while (y < bk/BW)`
     *   (smaller y), so red's king has the **larger** index. You must compare and take the smaller/larger --
     *   会在初始局面下**一次都不进循环**（10 < 0 为假），于是把"两个王之间塞满了子"
     *   writing `y = rk/BW + 1; while (y < bk/BW)` directly makes the loop **never run once** in the initial
     *   误判成"照面" ⇒ 任何着法走完都算自己被将 ⇒ 全部着法非法、开局 44 步变 0 步。
     *   position (10 < 0 is false), so "pieces fill the space between the kings" is misread as "they face each
     *   这个错是探针的 perft 判据抓出来的（规则自测存在的意义）。
     *   other" => every move leaves you in check => all moves illegal and the 44 opening moves become 0.
     *   The probe's perft check caught this error -- that is why the rules self-test exists. */
    y1 = rk / BW;
    y2 = bk / BW;
    if (y1 > y2) {
        t = y1;
        y1 = y2;
        y2 = t;
    }
    cnt = 0;
    y = y1 + 1;
    while (y < y2) {
        if (b[y * BW + x] != 0) cnt = cnt + 1;
        y = y + 1;
    }
    if (cnt == 0) return 1;
    return 0;
}

/* 试一个落点：在盘内且 can_move 通过就记进 out，返回新的个数。
 * Try one landing square: if it is on the board and can_move allows it, record it in out and return the new count.
 * 走法生成全靠它 —— 于是"生成"与"判定"共用 can_move 这一份规则。
 * Move generation relies entirely on it -- so generation and validation share the one copy of the rules in can_move. */
int try_to(int* b, int fx, int fy, int tx, int ty, int* out, int n) {
    if (tx < 0 || tx >= BW || ty < 0 || ty >= BH) return n;
    if (can_move(b, fx, fy, tx, ty) == 0) return n;
    out[n] = (fy * BW + fx) * 100 + (ty * BW + tx);
    return n + 1;
}

/* 车/炮 沿 (dx,dy) 一条射线上的着法。is_cannon=1 时按"翻一个炮架吃第一个子"办。 */
/* Chariot/cannon moves along the ray (dx,dy). With is_cannon=1 it works as "hop one screen and take the first piece". */
int ray_moves(int* b, int s, int fx, int fy, int dx, int dy, int is_cannon, int* out, int n) {
    int x;
    int y;
    int v;
    int screen;

    x = fx + dx;
    y = fy + dy;
    screen = 0;
    while (x >= 0 && x < BW && y >= 0 && y < BH) {
        v = b[y * BW + x];
        if (v == 0) {
            if (screen == 0) {
                out[n] = (fy * BW + fx) * 100 + (y * BW + x);
                n = n + 1;
            }
        } else {
            if (screen == 0) {
                if (v / 10 != s) {          /* 车吃到子；炮此时只是搭上炮架 */
                    /* The chariot captures; for the cannon this is merely resting on a screen */
                    if (is_cannon == 0) {
                        out[n] = (fy * BW + fx) * 100 + (y * BW + x);
                        n = n + 1;
                    }
                }
                if (is_cannon == 0) return n;
                screen = 1;
            } else {
                if (v / 10 != s) {          /* 炮翻过炮架，吃它后面第一个子 */
                    /* The cannon hops the screen and takes the first piece behind it */
                    out[n] = (fy * BW + fx) * 100 + (y * BW + x);
                    n = n + 1;
                }
                return n;
            }
        }
        x = x + dx;
        y = y + dy;
    }
    return n;
}

/* 生成 s 方**全部走法**（只看走法，不看走完是否自己被将）。返回条数。 */
/* Generate **all moves** for side s (movement only, ignoring whether they leave you in check). Returns the count. */
int gen_moves(int* b, int s, int* out) {
    int i;
    int n;
    int fx;
    int fy;
    int tp;

    n = 0;
    for (i = 0; i < BW * BH; i = i + 1) {
        if (b[i] == 0) continue;
        if (b[i] / 10 != s) continue;
        fx = i % BW;
        fy = i / BW;
        tp = b[i] % 10;

        if (tp == T_R) {
            n = ray_moves(b, s, fx, fy, 1, 0, 0, out, n);
            n = ray_moves(b, s, fx, fy, -1, 0, 0, out, n);
            n = ray_moves(b, s, fx, fy, 0, 1, 0, out, n);
            n = ray_moves(b, s, fx, fy, 0, -1, 0, out, n);
        } else if (tp == T_C) {
            n = ray_moves(b, s, fx, fy, 1, 0, 1, out, n);
            n = ray_moves(b, s, fx, fy, -1, 0, 1, out, n);
            n = ray_moves(b, s, fx, fy, 0, 1, 1, out, n);
            n = ray_moves(b, s, fx, fy, 0, -1, 1, out, n);
        } else if (tp == T_N) {
            n = try_to(b, fx, fy, fx + 1, fy - 2, out, n);
            n = try_to(b, fx, fy, fx - 1, fy - 2, out, n);
            n = try_to(b, fx, fy, fx + 2, fy - 1, out, n);
            n = try_to(b, fx, fy, fx - 2, fy - 1, out, n);
            n = try_to(b, fx, fy, fx + 1, fy + 2, out, n);
            n = try_to(b, fx, fy, fx - 1, fy + 2, out, n);
            n = try_to(b, fx, fy, fx + 2, fy + 1, out, n);
            n = try_to(b, fx, fy, fx - 2, fy + 1, out, n);
        } else if (tp == T_B) {
            n = try_to(b, fx, fy, fx + 2, fy - 2, out, n);
            n = try_to(b, fx, fy, fx - 2, fy - 2, out, n);
            n = try_to(b, fx, fy, fx + 2, fy + 2, out, n);
            n = try_to(b, fx, fy, fx - 2, fy + 2, out, n);
        } else if (tp == T_A) {
            n = try_to(b, fx, fy, fx + 1, fy - 1, out, n);
            n = try_to(b, fx, fy, fx - 1, fy - 1, out, n);
            n = try_to(b, fx, fy, fx + 1, fy + 1, out, n);
            n = try_to(b, fx, fy, fx - 1, fy + 1, out, n);
        } else if (tp == T_K) {
            n = try_to(b, fx, fy, fx + 1, fy, out, n);
            n = try_to(b, fx, fy, fx - 1, fy, out, n);
            n = try_to(b, fx, fy, fx, fy + 1, out, n);
            n = try_to(b, fx, fy, fx, fy - 1, out, n);
        } else if (tp == T_P) {
            if (s == S_RED) n = try_to(b, fx, fy, fx, fy - 1, out, n);
            else n = try_to(b, fx, fy, fx, fy + 1, out, n);
            if (pawn_crossed(fy, s) == 1) {
                n = try_to(b, fx, fy, fx + 1, fy, out, n);
                n = try_to(b, fx, fy, fx - 1, fy, out, n);
            }
        }
        if (n >= MAXM - 8) return n;       /* 上限保护：宁可少列也不要越界 */
        /* Cap guard: list fewer moves rather than overrun the array */
    }
    return n;
}

/* s 方被将军吗（含将帅照面）。
 * Is side s in check (facing kings included)?
 *
 * 判据是**"对方有没有哪一步能落到我的帅上"** —— 直接复用 gen_moves，
 * The test is **"does the opponent have any move that lands on my king"** -- reusing gen_moves directly,
 * 不另写一套"从帅的位置反推谁在攻击我"的反向代码。反向那份要点名车/炮/马的
 * rather than writing a second, reverse set of code that works back from the king's square to who attacks it. The reverse
 * 攻击几何（马的腿还得反着判），同一套规则写两遍必然漂移；而这里代价只是
 * version would have to spell out chariot/cannon/horse attack geometry (and judge the horse leg backwards), and writing
 * 一次走法生成（几十条），换来"走法规则只有一份"。
 * the same rules twice is bound to drift; here the cost is just one move generation (a few dozen moves) for a single copy of the rules. */
int in_check(int* b, int s) {
    int i;
    int k;
    int op;
    int m[MAXM];
    int n;
    int j;

    if (kings_face(b) == 1) return 1;
    k = -1;
    for (i = 0; i < BW * BH; i = i + 1) {
        if (b[i] == s * 10 + T_K) { k = i; break; }
    }
    if (k < 0) return 0;                    /* 帅已被吃（不该走到这儿） */
    /* The king is already captured (we should never get here) */
    op = 1 - s;
    n = gen_moves(b, op, m);
    for (j = 0; j < n; j = j + 1) {
        if (m[j] % 100 == k) return 1;
    }
    return 0;
}

/* 走一步并在副本上试算：符合走法 **且** 走完自己不被将 = 合法。 */
/* Play a move on a copy and try it: matches the movement rule **and** does not leave you in check = legal. */
int legal_move(int* b, int fx, int fy, int tx, int ty) {
    int s;
    int t[CELLS];
    int i;

    if (can_move(b, fx, fy, tx, ty) == 0) return 0;
    s = b[fy * BW + fx] / 10;
    for (i = 0; i < BW * BH; i = i + 1) t[i] = b[i];
    t[ty * BW + tx] = t[fy * BW + fx];
    t[fy * BW + fx] = 0;
    if (in_check(t, s) == 1) return 0;
    return 1;
}

/* s 方还有没有合法着法（= 判断将死 / 困毙）。象棋里**困毙也算输**，与国象不同。 */
/* Does side s still have a legal move (= checkmate / stalemate detection)? In xiangqi **stalemate is also a loss**, unlike international chess. */
int has_legal(int* b, int s) {
    int m[MAXM];
    int n;
    int i;
    int f;
    int t;

    n = gen_moves(b, s, m);
    for (i = 0; i < n; i = i + 1) {
        f = m[i] / 100;
        t = m[i] % 100;
        if (legal_move(b, f % BW, f / BW, t % BW, t / BW) == 1) return 1;
    }
    return 0;
}

/* 合法着法条数 —— 单测用（开局应为 44）。 */
/* Number of legal moves -- for unit tests (it should be 44 in the opening position). */
int count_legal(int* b, int s) {
    int m[MAXM];
    int n;
    int i;
    int f;
    int t;
    int c;

    c = 0;
    n = gen_moves(b, s, m);
    for (i = 0; i < n; i = i + 1) {
        f = m[i] / 100;
        t = m[i] % 100;
        if (legal_move(b, f % BW, f / BW, t % BW, t / BW) == 1) c = c + 1;
    }
    return c;
}

/* 子力价值（帅极大，保证"被吃帅"永远是最坏局面） */
/* Piece values (the king is huge, so "losing the king" is always the worst position) */
int piece_value(int tp) {
    if (tp == T_K) return 10000;
    if (tp == T_R) return 900;
    if (tp == T_C) return 450;
    if (tp == T_N) return 400;
    if (tp == T_B) return 200;
    if (tp == T_A) return 200;
    if (tp == T_P) return 100;
    return 0;
}

int material(int* b, int s) {
    int i;
    int sum;

    sum = 0;
    for (i = 0; i < BW * BH; i = i + 1) {
        if (b[i] == 0) continue;
        if (b[i] / 10 != s) continue;
        sum = sum + piece_value(b[i] % 10);
    }
    return sum;
}

/* 局面小项：兵过河往前走值钱，车/炮占中路值一点 —— 只用来打破同分僵局。
 * Small positional bonus: a pawn that has crossed and pushes forward is worth a little, a chariot/cannon on the
 * ⚠ 收的是**动子**的种类（不是落点上那个子）—— 落点上是敌方子，拿它判"我的兵过河了"
 * ⚠ central file a little too -- it only exists to break ties. The parameter is the kind of the **moving** piece
 *   是张冠李戴；这个参数是调用方把起点上的子取出来传进来的。
 *   (not the piece on the landing square): the landing square holds an enemy piece, so using it to test "my pawn
 *   crossed the river" would be mixing up two different things; the caller takes the piece from the origin and passes it in. */
int pos_bonus(int moverTp, int fromY, int tx, int ty, int s) {
    int sc;
    int d;

    sc = 0;
    if (fromY != ty) sc = sc + 2;           /* 动子比不动子略好 */
    /* Moving a piece is slightly better than not moving one */
    if (moverTp == T_P) {
        if (s == S_RED && ty <= 4) sc = sc + 12;    /* 红兵过河 */
        /* A red pawn that has crossed the river */
        if (s == S_BLK && ty >= 5) sc = sc + 12;
    }
    d = tx - 4;
    if (d < 0) d = 0 - d;
    if (moverTp == T_R || moverTp == T_C) sc = sc + (4 - d) * 2;
    return sc;
}

/* 电脑选一步。返回 from*100+to；无棋可走返回 -1。
 * The computer picks a move. Returns from*100+to, or -1 when there is nothing to play.
 *
 * 深度 = **1 层 + 回吃**：对每个候选着法，算走完之后的子力差，再减掉对方
 * Depth = **1 ply + recapture**: for each candidate move, score the material difference after it, then subtract
 * 立刻能吃回的最大价值 —— 这样它不会白送子，也会吃白送的子。
 * the largest value the opponent can immediately take back -- so it neither gives pieces away nor refuses free ones.
 * 只在**将军**时才额外做一次"对方还有没有棋"（将死判断），因为那一步最贵；
 * Only when the move gives **check** does it additionally ask "does the opponent still have a move" (mate detection),
 * 于是 CPU 代价大致是"候选数 × 一次走法生成"，手机上零点几秒量级。
 * because that step is the most expensive; so the CPU cost is roughly "candidates x one move generation", a fraction of a second on a phone.
 * 同分之间用 ui_math_rand 抖一下，否则每局走得一模一样。
 * Ties are jittered with ui_math_rand, otherwise every game would play out identically. */
int ai_pick(int* b, int me) {
    int m[MAXM];
    int om[MAXM];
    int n;
    int on;
    int i;
    int j;
    int f;
    int t;
    int fx;
    int fy;
    int tx;
    int ty;
    int trial[CELLS];
    int k;
    int sc;
    int best;
    int bm;
    int opp;
    int worst;

    opp = 1 - me;
    best = -9999999;
    bm = -1;
    n = gen_moves(b, me, m);
    for (i = 0; i < n; i = i + 1) {
        f = m[i] / 100;
        t = m[i] % 100;
        fx = f % BW;
        fy = f / BW;
        tx = t % BW;
        ty = t / BW;
        if (legal_move(b, fx, fy, tx, ty) == 0) continue;

        for (k = 0; k < BW * BH; k = k + 1) trial[k] = b[k];
        trial[ty * BW + tx] = trial[fy * BW + fx];
        trial[fy * BW + fx] = 0;

        sc = material(trial, me) - material(trial, opp);
        sc = sc + pos_bonus(b[fy * BW + fx] % 10, fy, tx, ty, me);

        if (in_check(trial, opp) == 1) {
            sc = sc + 40;                       /* 将军本身有收益 */
            /* Giving check is itself worth something */
            if (has_legal(trial, opp) == 0) return m[i];   /* 将死 / 困毙：立刻走 */
            /* Checkmate / stalemate: play it right now */
        }

        /* 对方能吃回多少 —— **只算"吃我这一步落的这个子"**。
         * How much can the opponent take back -- counting **only "capturing the piece I just landed"**.
         *
         * 原来是"对方在这个**新局面**里最好的一口"（在整盘上找最大的吃），
         * It used to be "the opponent's best bite in this **new position**" (the largest capture anywhere on the board),
         * 那会把**与我这一步无关**的既有威胁一并算进来：开局时红炮正照着黑马
         * which pulled in pre-existing threats **unrelated to my move**: in the opening a red cannon bears down on a black horse
         * （隔着黑炮当炮架），于是"随便走一步"和"炮打马"都要扣掉那 400 ——
         * (with a black cannon as the screen), so "any random move" and "cannon takes horse" both lost that 400 --
         * 两者都扣 = 等于没扣，两台候选剩下的差别就只有"炮打马多赚 400"，
         * both lost it, which cancels out, and the only difference left between the two candidates was "cannon takes horse gains 400",
         * 电脑于是**见马就换炮**（拿 450 的炮去吃 400 的马，白亏 50），
         * so the computer **traded its cannon for any horse it saw** (a 450 cannon for a 400 horse, a net loss of 50),
         * 正好与它上面那句注释"不会白送子"相反。实测（`.scratch/chessprobe/aiscore.c`
         * the exact opposite of the comment above it, "never gives pieces away". Measured (`.scratch/chessprobe/aiscore.c`
         * 把 44 个候选的评分逐项打出来）：26 个"随便走"全是 −358 上下，
         * printed the score of all 44 candidates one by one): the 26 "random" moves were all around -358,
         * 而两处"炮打马"是 −1 —— 于是每一步都挑炮打马。桌面上头一局就是这么开的。
         * while the two "cannon takes horse" moves were -1 -- so it picked cannon-takes-horse every single time. The first desktop game opened exactly that way.
         *
         * 判据收在**落点**上才是那个"吃**回**"：对方能不能吃掉我刚落的这个子。
         * Narrowing the test to the **landing square** is what makes it a real "**re**capture": can the opponent take the piece I just moved there?
         * 于是"把子送进对方嘴里"被扣分（本意），而"我这步与那口无关的旧账"不再重复计。
         * So "feeding a piece to the opponent" is penalised (the intent), while "old debts unrelated to this move" are no longer counted twice. */
        worst = 0;
        on = gen_moves(trial, opp, om);
        for (j = 0; j < on; j = j + 1) {
            if (om[j] % 100 != ty * BW + tx) continue;   /* 落点以外的吃子与我这一步无关 */
            /* Captures elsewhere have nothing to do with this move */
            f = om[j] / 100;
            if (legal_move(trial, f % BW, f / BW, tx, ty) == 0) continue;
            worst = piece_value(trial[ty * BW + tx] % 10);   /* 送掉的就是刚走的这个子 */
            /* What is given away is exactly the piece just moved */
            break;
        }
        sc = sc - worst * 9 / 10;
        sc = sc + ui_math_rand(9);                   /* 同分抖动，免得每局一样 */
        /* Tie-break jitter so that no two games look the same */
        if (sc > best) {
            best = sc;
            bm = m[i];
        }
    }
    return bm;
}

/* 开局摆子。红在下（y=9 底线）、黑在上（y=0 底线），与棋盘画法一致。 */
/* Set up the opening position. Red at the bottom (back rank y=9), black at the top (back rank y=0), matching how the board is drawn. */
void init_board(int* b) {
    int i;

    for (i = 0; i < BW * BH; i = i + 1) b[i] = 0;
    /* 黑方（上） */
    /* Black (top) */
    b[0 * BW + 0] = S_BLK * 10 + T_R;
    b[0 * BW + 1] = S_BLK * 10 + T_N;
    b[0 * BW + 2] = S_BLK * 10 + T_B;
    b[0 * BW + 3] = S_BLK * 10 + T_A;
    b[0 * BW + 4] = S_BLK * 10 + T_K;
    b[0 * BW + 5] = S_BLK * 10 + T_A;
    b[0 * BW + 6] = S_BLK * 10 + T_B;
    b[0 * BW + 7] = S_BLK * 10 + T_N;
    b[0 * BW + 8] = S_BLK * 10 + T_R;
    b[2 * BW + 1] = S_BLK * 10 + T_C;
    b[2 * BW + 7] = S_BLK * 10 + T_C;
    b[3 * BW + 0] = S_BLK * 10 + T_P;
    b[3 * BW + 2] = S_BLK * 10 + T_P;
    b[3 * BW + 4] = S_BLK * 10 + T_P;
    b[3 * BW + 6] = S_BLK * 10 + T_P;
    b[3 * BW + 8] = S_BLK * 10 + T_P;
    /* 红方（下） */
    /* Red (bottom) */
    b[9 * BW + 0] = S_RED * 10 + T_R;
    b[9 * BW + 1] = S_RED * 10 + T_N;
    b[9 * BW + 2] = S_RED * 10 + T_B;
    b[9 * BW + 3] = S_RED * 10 + T_A;
    b[9 * BW + 4] = S_RED * 10 + T_K;
    b[9 * BW + 5] = S_RED * 10 + T_A;
    b[9 * BW + 6] = S_RED * 10 + T_B;
    b[9 * BW + 7] = S_RED * 10 + T_N;
    b[9 * BW + 8] = S_RED * 10 + T_R;
    b[7 * BW + 1] = S_RED * 10 + T_C;
    b[7 * BW + 7] = S_RED * 10 + T_C;
    b[6 * BW + 0] = S_RED * 10 + T_P;
    b[6 * BW + 2] = S_RED * 10 + T_P;
    b[6 * BW + 4] = S_RED * 10 + T_P;
    b[6 * BW + 6] = S_RED * 10 + T_P;
    b[6 * BW + 8] = S_RED * 10 + T_P;
}

/* ═══════════════════════ 界面 ═══════════════════════ */
/* ═══════════════════════ UI (interface) ═══════════════════════ */

/* ⚠ 英文用**单字母**（K/A/B/N/R/C/P，国际象棋习惯），红黑同一字母 —— 两边靠**墨色**
 * ⚠ English uses **single letters** (K/A/B/N/R/C/P, international-chess convention) and red and black share the same
   区分（红子 COL_RED、黑子 COL_BLACK），这正是通行做法，而且棋子格只有 cell*3/5 那么宽，
 * letter -- the two sides are told apart by **ink colour** (red pieces COL_RED, black pieces COL_BLACK). That is the
   写全称（"General"/"Advisor"…）会撑破格子。
 * usual approach, and the piece cell is only cell*3/5 wide, so full names ("General"/"Advisor"...) would burst the cell. */
char* name_of(int v) {
    int tp;

    tp = v % 10;
    if (v / 10 == S_RED) {
        if (tp == T_K) return g_lang == 0 ? "帅" : "K";
        if (tp == T_A) return g_lang == 0 ? "仕" : "A";
        if (tp == T_B) return g_lang == 0 ? "相" : "B";
        if (tp == T_N) return g_lang == 0 ? "马" : "N";
        if (tp == T_R) return g_lang == 0 ? "车" : "R";
        if (tp == T_C) return g_lang == 0 ? "炮" : "C";
        return g_lang == 0 ? "兵" : "P";
    }
    if (tp == T_K) return g_lang == 0 ? "将" : "K";
    if (tp == T_A) return g_lang == 0 ? "士" : "A";
    if (tp == T_B) return g_lang == 0 ? "象" : "B";
    if (tp == T_N) return g_lang == 0 ? "马" : "N";
    if (tp == T_R) return g_lang == 0 ? "车" : "R";
    if (tp == T_C) return g_lang == 0 ? "炮" : "C";
    return g_lang == 0 ? "卒" : "P";
}

/* 像素 → 行列号；越界返回 -1（与 gomoku 的 hit_col 同一套取整）。 */
/* Pixel -> line index; returns -1 when out of range (the same rounding as gomoku's hit_col). */
int hit_line(int p, int base, int cell) {
    int c;

    c = (p - base + cell / 2) / cell;
    if (c < 0) return -1;
    return c;
}

/* 画棋盘 + 棋子 + 选中/可走点 + 顶部状态行。**不调 ui_2d_present** ——
 * Draw the board + pieces + selection/legal targets + the top status line. **Does not call ui_2d_present** --
 * 底部的按钮由 main 画（按钮矩形是 main 算的，画与命中判定必须同一份数据）。
 * the bottom buttons are drawn by main (main computes the rectangles, and drawing and hit-testing must share the same data). */
void draw_board(int* b, int bx, int by, int cell, int sel, char* status) {
    int i;
    int x;
    int y;
    int cx;
    int cy;
    int r;
    int v;
    int s;
    int ink;
    int ring;
    int ds;
    int dx;
    int dy;

    ui_2d_clear(COL_BG);

    /* 木盘底 */
    /* The wooden board slab */
    ui_2d_rect(bx - cell * 3 / 5, by - cell * 3 / 5,
            cell * 8 + cell * 6 / 5, cell * 9 + cell * 6 / 5,
            COL_BOARD, 1, 0, 6);

    /* 横线 10 条（贯通） */
    /* 10 horizontal lines (running all the way across) */
    for (y = 0; y < BH; y = y + 1) {
        ui_2d_line(bx, by + y * cell, bx + 8 * cell, by + y * cell, COL_LINE, 1);
    }
    /* 竖线 9 条：最外两条贯通，里面的在河界处断开 */
    /* 9 vertical lines: the two outermost run all the way, the inner ones break at the river */
    for (x = 0; x < BW; x = x + 1) {
        if (x == 0 || x == BW - 1) {
            ui_2d_line(bx + x * cell, by, bx + x * cell, by + 9 * cell, COL_LINE, 1);
        } else {
            ui_2d_line(bx + x * cell, by, bx + x * cell, by + 4 * cell, COL_LINE, 1);
            ui_2d_line(bx + x * cell, by + 5 * cell, bx + x * cell, by + 9 * cell, COL_LINE, 1);
        }
    }
    /* 两座九宫的斜线 */
    /* The diagonals of the two palaces */
    ui_2d_line(bx + 3 * cell, by, bx + 5 * cell, by + 2 * cell, COL_LINE, 1);
    ui_2d_line(bx + 5 * cell, by, bx + 3 * cell, by + 2 * cell, COL_LINE, 1);
    ui_2d_line(bx + 3 * cell, by + 7 * cell, bx + 5 * cell, by + 9 * cell, COL_LINE, 1);
    ui_2d_line(bx + 5 * cell, by + 7 * cell, bx + 3 * cell, by + 9 * cell, COL_LINE, 1);

    /* 楚河汉界（写在两段竖线的空档里，竖排读着别扭，横排即可） */
    /* The river "Chu He Han Jie" (written in the gap between the two broken vertical lines; vertical text reads awkwardly, horizontal is fine) */
    /* ⚠ 英文只能给 "Chu"/"Han"（楚河/汉界的专名）—— 河界那一格只有 1 cell 宽（约 35px），
     * ⚠ The English can only be "Chu"/"Han" (the proper names) -- that cell is only 1 cell wide (about 35px),
       而 "RIVER"/"BORDER" 在 cell*2/5 的字号下正好占满乃至溢出。
     * and "RIVER"/"BORDER" at the cell*2/5 font size would exactly fill it or overflow. */
    ui_2d_text_v(bx + 2 * cell, by + 4 * cell + cell / 2, g_lang == 0 ? "楚河" : "Chu", COL_LINE,
              cell * 2 / 5, VML_ANCHOR_CENTER, VML_VANCHOR_MIDDLE, 0);
    ui_2d_text_v(bx + 6 * cell, by + 4 * cell + cell / 2, g_lang == 0 ? "汉界" : "Han", COL_LINE,
              cell * 2 / 5, VML_ANCHOR_CENTER, VML_VANCHOR_MIDDLE, 0);

    /* 棋子 */
    /* Pieces */
    r = cell * 2 / 5;
    if (r < 5) r = 5;
    for (i = 0; i < BW * BH; i = i + 1) {
        v = b[i];
        if (v == 0) continue;
        cx = bx + (i % BW) * cell;
        cy = by + (i / BW) * cell;
        s = v / 10;
        if (s == S_RED) { ink = COL_RED; ring = COL_RED; }
        else { ink = COL_BLACK; ring = COL_BLACK; }
        ui_2d_circle(cx, cy, r, ring, 0, 2);          /* 外圈 */
        /* Outer ring */
        ui_2d_circle(cx, cy, r - 2, COL_PIECE, 1, 0); /* 内底 */
        /* Inner fill */
        ui_2d_text_v(cx, cy, name_of(v), ink, cell * 3 / 5,
                  VML_ANCHOR_CENTER, VML_VANCHOR_MIDDLE, VML_FONT_BOLD);
        /* 最后一步的落点画个小圈，方便看清电脑刚走哪儿 */
        /* Draw a small ring on the last move's landing square so you can see where the computer just moved */
        if (i == sel && sel >= 0) ui_2d_circle(cx, cy, r + 3, COL_SEL, 0, 2);
    }

    /* 选中子的可走点（**现算**，不另存一份 —— 画出来的点与真正可行的点必然一致） */
    /* Legal targets of the selected piece (**computed on the spot**, never cached -- the dots drawn and the actually legal targets can never disagree) */
    if (sel >= 0) {
        x = sel % BW;
        y = sel / BW;
        for (i = 0; i < BW * BH; i = i + 1) {
            dx = i % BW;
            dy = i / BW;
            if (legal_move(b, x, y, dx, dy) == 0) continue;
            ds = cell / 8;
            if (ds < 3) ds = 3;
            if (b[i] != 0) ui_2d_circle(bx + dx * cell, by + dy * cell, r, COL_SEL, 0, 3);
            else ui_2d_circle(bx + dx * cell, by + dy * cell, ds, COL_SEL, 1, 0);
        }
    }

    /* 顶部状态行 */
    /* The top status line */
    ui_2d_text_v(bx, 6, status, COL_TEXT, 15, VML_ANCHOR_LEFT, VML_VANCHOR_TOP, VML_FONT_BOLD);
}

/* btns = {重开x,y,w,h, 悔棋x,y,w,h} —— 画与命中判定读同一个数组，不各算一遍。 */
/* btns = {restart x,y,w,h, undo x,y,w,h} -- drawing and hit-testing read the same array instead of each computing its own. */
void draw_buttons(int* btns) {
    int i;
    int x;

    i = 0;
    while (i < 2) {
        x = i * 4;
        ui_2d_rect(btns[x], btns[x + 1], btns[x + 2], btns[x + 3], COL_BTN, 1, 0, 8);
        if (i == 0) {
            ui_2d_text_v(btns[x] + btns[x + 2] / 2, btns[x + 1] + btns[x + 3] / 2,
                      g_lang == 0 ? "重开" : "Restart",
                      COL_BTN_TX, 16, VML_ANCHOR_CENTER, VML_VANCHOR_MIDDLE, 0);
        } else {
            ui_2d_text_v(btns[x] + btns[x + 2] / 2, btns[x + 1] + btns[x + 3] / 2,
                      g_lang == 0 ? "悔棋" : "Undo",
                      COL_BTN_TX, 16, VML_ANCHOR_CENTER, VML_VANCHOR_MIDDLE, 0);
        }
        i = i + 1;
    }
}

int pt_in(int px, int py, int x, int y, int w, int h) {
    if (px < x || px > x + w) return 0;
    if (py < y || py > y + h) return 0;
    return 1;
}

/* ═══════════════════════ 主程序 ═══════════════════════ */
/* ═══════════════════════ Main program ═══════════════════════ */

#ifndef CHESS_LIB_ONLY

/* ── 音效：**用 ui_audio_beep 单音**（v0.96.508 换回来） ──────────────
 * ── Sound: **single-tone ui_audio_beep** (switched back in v0.96.508) ──────────────
 *
 * ⚠⚠ 这里一度改用共享库的音序器（`ui_sfx_add`），**真机上破音**，换回来了。
 * ⚠⚠ This once used the shared library's sequencer (`ui_sfx_add`) and **broke up on a real device**, so it was switched back.
 *   原因不是音序器本身，是**这里配的音**：
 *   The cause was not the sequencer itself but **the sounds configured here**:
 *     · 将军 / 赢 / 输 三组都是**多个音叠着响**（将军 2 声部、胜负各 3 声部），
 *     · the check / win / lose groups all **sounded several notes at once** (2 voices for check, 3 each for win and lose)
 *       音量还开到了 90–95 —— 多声部一叠加就顶到**削波**；
 *       with the volume up at 90-95 -- stacked voices pushed straight into **clipping**;
 *     · `sfx_lose` 的末音是 `45`(A2 = **110Hz**)，而 `vml_ui.h` 里白纸黑字
 *     · the last note of `sfx_lose` was `45` (A2 = **110Hz**), and `vml_ui.h` says in plain words
 *       写着「低音别写太低…手机外放在 200Hz 以下衰减很快，C2 出来是"噗"一声闷响，
 *       "do not write bass too low ... a phone speaker rolls off fast below 200Hz, so C2 comes out as a dull thump,
 *       **玩家听着像没响**」。这一条正是冲着这类写法写的。
 *       **the player hears it as silence**". That note was written exactly for this kind of code.
 *
 *   `ui_audio_beep` 是**单通道**的（后一个音掐掉前一个）⇒ 一个事件永远只有一个音在响，
 *   `ui_audio_beep` is **single-channel** (a new tone cuts the previous one) => one event never has more than one tone sounding,
 *   **结构上不可能削波**。连锁反应的代价也不大：将军/胜负本来就是"一声提示"，
 *   **so clipping is structurally impossible**. The knock-on cost is small: check and win/lose are "one cue" anyway
 *   不需要和弦。
 *   and need no chord.
 *
 * ⚠ 频率沿用换音序器之前那一版（v0.96.490 之前就在用的），**别随手调** ——
 * ⚠ The frequencies are the ones from before the sequencer switch (already in use before v0.96.490) -- **do not tweak them casually**;
 *   调了要真机听。分得清"谁走的"靠的是音区：
 *   if you change them, listen on a real device. Telling "whose move it was" apart relies on the pitch range:
 *   选子 760 / 你走子 880 / 电脑走子 560 / 悔棋成 700 / 悔棋不成 300 /
 *   pick 760 / your move 880 / computer move 560 / undo ok 700 / undo rejected 300 /
 *   新局 900 / 将军 1200 / 赢 1320 长 / 输 240 长。
 *   new game 900 / check 1200 / win 1320 long / lose 240 long. */

void sfx_pick(void)   { ui_audio_beep(760, 20); }
void sfx_move(void)   { ui_audio_beep(880, 25); }
void sfx_aimove(void) { ui_audio_beep(560, 25); }

/* 悔棋：成不成一听就知道（成了清亮、没得悔低闷）。 */
/* Undo: you hear at once whether it worked (clear when it did, dull when there was nothing to undo). */
void sfx_undo_ok(void) { ui_audio_beep(700, 40); }
void sfx_undo_no(void) { ui_audio_beep(300, 60); }

void sfx_newgame(void) { ui_audio_beep(900, 70); }

/* 将军：**必须让玩家立刻知道**的事 —— 比周围所有音都高、都长。 */
/* Check: something the player **must know immediately** -- higher and longer than every sound around it. */
void sfx_check(void) { ui_audio_beep(1200, 90); }

/* 赢：高、长，明亮；输：低、长。**两个方向，不会听错。** */
/* Win: high and long, bright; lose: low and long. **Two opposite directions, impossible to mishear.** */
void sfx_win(void) {
    ui_audio_beep(1320, 320);
    ui_vibrate_simple(60, 0);
}
void sfx_lose(void) {
    ui_audio_beep(240, 420);
    ui_vibrate_simple(220, 0);
}

int main(void) {
    int b[CELLS];
    int msg[4];
    int btns[8];
    int sw;
    int sh;
    int cell;
    int bx;
    int by;
    int t;
    int x;
    int y;
    int col;
    int row;
    int idx;
    int sel;
    int t0;
    int r;
    int i;
    int f;
    int tt;
    int over;          /* 0 进行中 / 1 人胜 / 2 电脑胜 / 3 和棋（理论上有） */
    /* 0 in progress / 1 human wins / 2 computer wins / 3 draw (in theory) */
    int turn;          /* 当前该谁走：S_RED / S_BLK */
    /* Whose turn it is: S_RED / S_BLK */
    int aiFrom;
    int aiTo;
    int hf[4];         /* 悔棋栈：最近 4 个半回合（人/电脑各 2） */
    /* Undo stack: the last 4 half-moves (2 each for human and computer) */
    int ht[4];
    int hc[4];
    int hn;
    char* status;

    init_board(b);

    /* 竖屏 + 不要手柄区：棋盘是竖着看的，全程触摸，手柄一个都用不上 */
    /* Portrait + no gamepad area: the board is read vertically and everything is touch, so the gamepad is of no use at all */
    g_lang = ui_scr_get_language();
    ui_win_open_ex(g_lang == 0 ? "象棋" : "Chess", 0, 0, VML_WIN_PORTRAIT, VML_WIN_NO_GAMEPAD);
    sw = ui_scr_get_w();
    sh = ui_scr_get_h();
    if (sw <= 0) sw = 380;
    if (sh <= 0) sh = 700;
    ui_scr_set_keep_on(1);

    /* 布局：顶部一行状态、底部一行按钮，中间给棋盘。宽高**分开算**再取小者 */
    /* Layout: one status line at the top, one button row at the bottom, the board in between. Width and height are **computed separately** and the smaller wins */
    cell = sw / 11;
    if ((sh - 110) / 12 < cell) cell = (sh - 110) / 12;
    if (cell < 8) cell = 8;
    bx = (sw - 8 * cell) / 2;
    by = 30 + (sh - 110 - 9 * cell) / 2;
    if (by < 30) by = 30;

    /* 两个按钮并排居中放在底部 */
    /* The two buttons sit side by side, centred, at the bottom */
    btns[2] = sw / 3;
    btns[3] = 40;
    btns[0] = sw / 2 - btns[2] - 6;
    btns[1] = sh - btns[3] - 10;
    btns[6] = btns[2];
    btns[7] = btns[3];
    btns[4] = sw / 2 + 6;
    btns[5] = btns[1];

    sel = -1;
    over = 0;
    turn = S_RED;
    hn = 0;
    /* ⚠ 顶部状态行是**左对齐、不折行**的一条（`ui_2d_text_v(bx, 6, …)`，字号 15）——
     * ⚠ The top status line is a single **left-aligned, non-wrapping** line (`ui_2d_text_v(bx, 6, ...)`, font size 15) --
       可用宽度约 sw − bx ≈ 338px，英文最长的 "Computer has no move. Red wins"（30 字符
     * the usable width is about sw - bx ~ 338px, and the longest English string, "Computer has no move. Red wins" (30 characters
       ≈ 225px）也在里面。再加词就会溢出，别再往这些串上添字。
     * ~ 225px), already fits. Add more words and it overflows, so do not put more text on these strings. */
    status = g_lang == 0 ? "你执红先行，点自己的子再点落点" : "You are red — tap a piece";

    t0 = ui_dlg_msg(g_lang == 0 ? "象棋" : "Chess",
                    g_lang == 0 ? "你执红先行。点亮自己的棋子，再点亮要去的点；底部可悔棋。"
                                : "You are red and move first. Tap your piece, then tap where it goes. Undo is at the bottom.",
                    VML_DLG_INFO);

    draw_board(b, bx, by, cell, sel, status);
    draw_buttons(btns);
    ui_2d_present();

    while (ui_win_is_closed() == 0) {
        t = ui_msg_wait(msg, 0);
        if (t == 0) continue;
        if (t == VML_MSG_WINDOWCLOSE) break;

        /* ⚠ **只认 TOUCHDOWN，绝不把 MOUSEDOWN 一起认** —— 这一条是本程序"完全下不了"的根因。
         * ⚠ **Accept TOUCHDOWN only, never MOUSEDOWN as well** -- this was the root cause of the program being "completely unplayable".
         *
         * 手机上一个物理点按会投出**两条**消息：先一条触摸、紧跟一条**坐标相同**的鼠标
         * One physical tap on a phone posts **two** messages: first a touch, then immediately a mouse event with the **same
         * （`DrawWindowPage.PostTouch` 里那个 `calls.PostInput(MouseDown…)` 在
         *  coordinates** (the `calls.PostInput(MouseDown...)` in `DrawWindowPage.PostTouch` sits **outside**
         *  `if (wantTouch)` **之外**，凡是图形窗口两族都发；`VmlUi.SuppressTouch` 只对
         *  `if (wantTouch)`, so every graphics window gets both families; `VmlUi.SuppressTouch` only suppresses
         *  电脑屏窗口抑制触摸）。桌面脚手架下我按 `touchdown`+`mousedown` 成对投进去复现了：
         *  touch for computer-screen windows). On the desktop harness I reproduced it by posting `touchdown` + `mousedown` as a pair:
         * 一次点按只听到 760 那一声（选中），选中位随即被抹掉 —— 因为第二次进到
         * one tap produced only the 760 beep (selection), and the selection was immediately wiped -- because the second event reached
         * `else if (idx == sel) sel = -1;` 那条分支，把刚选中的子**取消**掉了。
         * the `else if (idx == sel) sel = -1;` branch and **deselected** the piece just chosen.
         *
         * 五子棋同样是"两族都认"，却看不出来：它落子后 `if (b[idx] != EMPTY) continue;`
         * Gomoku accepts both families too and never showed it: after placing a stone its `if (b[idx] != EMPTY) continue;`
         * 天然幂等，重复那一下是空操作。**本程序是"先选子、再点落点"的两步状态机，
         * is naturally idempotent, so the repeat is a no-op. **This program is a two-step state machine ("pick a piece, then tap the
         * 对同一个点重复输入不幂等**，于是「点自己的子」永远只闪一下、根本选不中。
         * destination"), so repeating input on the same square is not idempotent** -- which is why "tap your own piece" only ever flashed and never selected.
         *
         * 鼠标那条不认即可 —— 它已经被这次 `ui_msg_wait` 取走，不会留在队列里变成下一次点按。
         * Simply not accepting the mouse one is enough -- this `ui_msg_wait` has already consumed it, so it cannot stay in the queue and become the next tap.
         * 约定与 `calc.c` 一致（它也只认 TOUCHDOWN），`vml_ui.h` 头部的示例同样如此。
         * The convention matches `calc.c` (which also accepts TOUCHDOWN only) and the example at the top of `vml_ui.h`. */
        if (t != VML_MSG_TOUCHDOWN) continue;

        x = msg[1];
        y = msg[2];

        /* 按钮 */
        /* Buttons */
        if (pt_in(x, y, btns[4], btns[5], btns[6], btns[7]) == 1) {
            if (hn >= 2 && over == 0) {
                /* 悔一个整回合（电脑 + 人）—— 只退一半的话又轮不到人走，等于白给。
                 * Undo one whole turn (computer + human) -- rewinding only half would leave it the computer's turn again, which is useless.
                 * 恢复顺序**不能反**：先把子从落点搬回起点，再把落点还原成"原来被吃的那个子"
                 * The restore order **must not be reversed**: first move the piece from the landing square back to the origin, then restore the landing
                 * （没吃就是 0 = 空）。反过来的话读到的落点已经被自己覆盖了。
                 * square to "whatever piece was captured there" (0 = empty if nothing was). Reversed, the landing square you read has already been overwritten by your own piece. */
                i = 0;
                while (i < 2) {
                    hn = hn - 1;
                    b[hf[hn]] = b[ht[hn]];
                    b[ht[hn]] = hc[hn];
                    i = i + 1;
                }
                sel = -1;
                turn = S_RED;
                status = g_lang == 0 ? "已悔一步，该你走" : "Undone. Your move";
                sfx_undo_ok();
            } else {
                sfx_undo_no();
            }
            draw_board(b, bx, by, cell, sel, status);
            draw_buttons(btns);
            ui_2d_present();
            continue;
        }
        if (pt_in(x, y, btns[0], btns[1], btns[2], btns[3]) == 1) {
            init_board(b);
            sel = -1;
            over = 0;
            turn = S_RED;
            hn = 0;
            status = g_lang == 0 ? "新的一局，你执红先行" : "New game. You are red";
            sfx_newgame();
            ui_msg_clear();
            draw_board(b, bx, by, cell, sel, status);
            draw_buttons(btns);
            ui_2d_present();
            continue;
        }

        if (over != 0) continue;

        /* 棋盘 */
        /* The board */
        col = hit_line(x, bx, cell);
        row = hit_line(y, by, cell);
        if (col < 0 || col >= BW || row < 0 || row >= BH) { sel = -1; }
        else {
            idx = row * BW + col;
            if (turn != S_RED) {
                /* 电脑在走，忽略触摸 */
                /* The computer is moving, ignore touch */
            } else if (sel < 0) {
                if (b[idx] != 0 && b[idx] / 10 == S_RED) {
                    sel = idx;
                    sfx_pick();
                }
            } else if (idx == sel) {
                sel = -1;
            } else if (b[idx] != 0 && b[idx] / 10 == S_RED) {
                sel = idx;
                sfx_pick();
            } else if (legal_move(b, sel % BW, sel / BW, col, row) == 1) {
                /* 人走子 */
                /* The human plays a move */
                f = sel;
                tt = idx;
                if (hn >= 4) {
                    /* 栈满：整体前移一位（只留最近 4 个半回合，够悔两次） */
                    /* Stack full: shift everything along by one (keep only the last 4 half-moves, enough for two undos) */
                    i = 0;
                    while (i < 3) {
                        hf[i] = hf[i + 1];
                        ht[i] = ht[i + 1];
                        hc[i] = hc[i + 1];
                        i = i + 1;
                    }
                    hn = 3;
                }
                hf[hn] = f;
                ht[hn] = tt;
                hc[hn] = b[tt];
                hn = hn + 1;
                b[tt] = b[f];
                b[f] = 0;
                sel = -1;
                turn = S_BLK;
                sfx_move();
                ui_vibrate_simple(15, 0);

                if (in_check(b, S_BLK) == 1) sfx_check();
                if (has_legal(b, S_BLK) == 0) {
                    over = 1;
                    status = g_lang == 0 ? "将死！红方胜" : "Checkmate! Red wins";
                } else {
                    status = g_lang == 0 ? "电脑思考中…" : "Computer thinking...";
                    draw_board(b, bx, by, cell, sel, status);
                    draw_buttons(btns);
                    ui_2d_present();

                    /* 电脑走 */
                    /* The computer's move */
                    r = ai_pick(b, S_BLK);
                    if (r < 0) {
                        over = 1;
                        status = g_lang == 0 ? "电脑无棋可走，红方胜" : "Computer has no move. Red wins";
                    } else {
                        aiFrom = r / 100;
                        aiTo = r % 100;
                        if (hn >= 4) {
                            i = 0;
                            while (i < 3) {
                                hf[i] = hf[i + 1];
                                ht[i] = ht[i + 1];
                                hc[i] = hc[i + 1];
                                i = i + 1;
                            }
                            hn = 3;
                        }
                        hf[hn] = aiFrom;
                        ht[hn] = aiTo;
                        hc[hn] = b[aiTo];
                        hn = hn + 1;
                        b[aiTo] = b[aiFrom];
                        b[aiFrom] = 0;
                        sfx_aimove();
                        turn = S_RED;

                        if (in_check(b, S_RED) == 1) {
                            sfx_check();
                            ui_vibrate_simple(30, 0);
                            status = g_lang == 0 ? "将军！该你走" : "Check! Your move";
                        } else {
                            status = g_lang == 0 ? "该你走" : "Your move";
                        }
                        if (has_legal(b, S_RED) == 0) {
                            over = 2;
                            status = g_lang == 0 ? "你被将死了，电脑胜" : "Checkmate. Computer wins";
                        }
                    }
                }
            } else {
                sel = -1;
            }
        }

        draw_board(b, bx, by, cell, sel, status);
        draw_buttons(btns);
        ui_2d_present();

        /* 一局结束：收在**这一处**（人将死电脑 / 电脑将死人 / 无棋可走都汇到这里）——
         * End of a game: it all converges **here** (human mates computer / computer mates human / no move available) --
         * 先让终局画面留在屏上，再出声弹框问要不要再来。
         * leave the final position on screen first, then play a sound and ask whether to play again. */
        if (over != 0) {
            if (over == 1) {
                sfx_win();
                r = ui_dlg_msg(g_lang == 0 ? "象棋" : "Chess",
                               g_lang == 0 ? "恭喜，你赢了！再来一局？" : "You win! Play again?",
                               VML_DLG_QUESTION);
            } else {
                sfx_lose();
                r = ui_dlg_msg(g_lang == 0 ? "象棋" : "Chess",
                               g_lang == 0 ? "电脑赢了。再来一局？" : "Computer wins. Play again?",
                               VML_DLG_QUESTION);
            }
            if (r == 1) break;                 /* 弹框失败(-1)也当"再来"，别把局面卡死 */
            /* A failed dialog (-1) also counts as "play again" -- do not leave the position stuck */
            init_board(b);
            sel = -1;
            over = 0;
            turn = S_RED;
            hn = 0;
            status = g_lang == 0 ? "新的一局，你执红先行" : "New game. You are red";
            ui_msg_clear();
            sfx_newgame();
            draw_board(b, bx, by, cell, sel, status);
            draw_buttons(btns);
            ui_2d_present();
        }
    }

    ui_scr_set_keep_on(0);
    ui_timer_set(120, 0);
    t = ui_msg_wait(msg, 400);
    ui_win_close();
    return 0;
}

#endif /* CHESS_LIB_ONLY */
