/* pacman.c —— 《吃豆人》C 版，跑在手机端 VML 上
 * pacman.c — "Pac-Man" in C, running on the mobile VML.
 *
 * 编译运行：vml run examples/c/pacman.c
 * Build and run: vml run examples/c/pacman.c
 *
 * 操作：**屏幕手柄的方向键**（← ↓ → ↑）改变方向，走到岔口自动转向。吃到所有豆子过关。
 * Controls: the **arrow keys of the on-screen gamepad** (← ↓ → ↑) set the direction; at a fork it turns by itself. Eat every pellet to clear the level.
 *
 * ## 为什么是吃豆人（而不是继续做射击游戏）
 * ## Why Pac-Man (and not another shooter)
 *
 * 屏幕手柄在宿主侧**只有一个按键槽**（`DrawWindowPage._padDownKey`：按新键会先替旧键
 * On the host side the on-screen gamepad has **only one key slot** (`DrawWindowPage._padDownKey`: pressing a new key first sends
 * 补一条 KeyUp）⇒ **两个键同时按做不到**。这砍掉了一整类玩法：
 * a compensating KeyUp for the old one) ⇒ **holding two keys at once is impossible**. That kills a whole class of gameplay:
 * 平台跳跃要"跑动中起跳"（mario.c 栽在这上面）、射击要"边移动边开火"。
 * a platformer needs "jump while running" (mario.c fell over on exactly this), a shooter needs "fire while moving".
 * 而**吃豆人天生只按一个方向键** —— 手柄的限制在这里正好不是限制。
 * Whereas **Pac-Man only ever presses one direction at a time** — here the gamepad's limitation is precisely not a limitation.
 *
 * ## 为什么这个画面"稳"（对比 starfall.c 的教训）
 * ## Why this screen is "stable" (compared with the lessons of starfall.c)
 *
 * 上一版《星陨》栽在两条上：① **漏了 `ui_2d_clear`** ⇒ 每帧叠上一帧，糊成一团还闪退；
 * The previous version, "Starfall", fell over on two things: ① **it forgot `ui_2d_clear`** ⇒ each frame was stacked on the previous one, smearing into a blob and then crashing;
 * ② 渐变几何按**像素**传（那个接口要的是千分之一归一化）⇒ 焦点塌成一角，其余全黑。
 * ② the gradient geometry was passed in **pixels** (that interface wants thousandths, normalized) ⇒ the focus collapsed into one corner and everything else went black.
 * 这一版**刻意不用渐变、不用旋转多边形**：全是平面高对比色块 + 圆 + 圆角矩形，
 * This version **deliberately uses no gradients and no rotated polygons**: everything is flat high-contrast colour blocks + circles + rounded rectangles,
 * 几何只有"格 × 格边长"一种算法。**能出错的地方少了，能看清的地方就多了。**
 * and the geometry has only one algorithm, "tile × tile side length". **Fewer places to go wrong means more places you can see clearly.**
 *
 * ## 关卡为什么是"梯子形"
 * ## Why the levels are "ladder-shaped"
 *
 * 手绘复杂迷宫极容易画出**封死的口袋**（豆子永远吃不到，游戏无法通关），
 * A hand-drawn complex maze very easily ends up with **sealed-off pockets** (pellets you can never reach, so the game can never be finished),
 * 而只看矩阵是看不出来的。这里用**构造式**布局，连通性由构造保证：
 * and you cannot see that just by looking at the matrix. This uses a **constructive** layout, where connectivity is guaranteed by construction:
 *   · 第 1/4/7/10/13/16/19 行是**整条横廊**（列 1..17 全通）
 *   · rows 1/4/7/10/13/16/19 are **full-width corridors** (columns 1..17 all open)
 *   · 相邻横廊之间在**固定的 6 个列**（1/4/7/11/14/17）有竖井
 *   · adjacent corridors are joined by vertical shafts at **6 fixed columns** (1/4/7/11/14/17)
 * ⇒ 任意两条横廊都连通，任意豆子都吃得到。自测里有一条实际的**泛洪验证**钉着这件事。
 * ⇒ any two corridors are connected and every pellet can be reached. A real **flood-fill verification** in the self-test pins this down.
 *
 * ## 三条 C 前端硬约束（同 tetris.c / mario.c / starfall.c）
 * ## Three hard constraints of the C frontend (same as tetris.c / mario.c / starfall.c)
 *
 * 1. ⚠ `${}` 只认局部变量（**2026-10-04 复验仍成立**：全局量一个装载指令都不生成、参数静默丢）
 * 1. ⚠ `${}` only recognises local variables (**still holds, re-checked 2026-10-04**: a global emits no load at all and the argument is silently dropped)
 * 2. 全局变量与全局 int 数组读写都正常。
 * 2. Reading and writing global variables and global int arrays both work fine.
 * 3. ~~⚠ `#define` 不支持反斜杠续行~~ ✅ **已修好（2026-10-04 复验）**：续行宏 = **5**、多行宏 = **42**、字符串续行 = **hello**；
 * 3. ~~⚠ `#define` does not support backslash line continuation~~ ✅ **fixed (re-checked 2026-10-04)**: continued macro = **5**, multi-line macro = **42**, continued string = **hello**;
 *    ⚠ 顶点数组必须是**具名全局数组**（复合字面量**仍不支持** —— 2026-10-04 复验：现在直接编译报错，不再静默）。
 *    ⚠ a vertex array must be a **named global array** (compound literals are **still unsupported** — re-checked 2026-10-04: now a compile error, no longer silent).
 */

#include <vml_ui.h>
#include <stdlib.h>

/* ── 迷宫尺寸与图块 ─────────────────────────────────────── */
/* ── Maze dimensions and tiles ─────────────────────────────────────── */

#define MW      19          /* 列 */
// columns
#define MH      21          /* 行 */
// rows
#define T_EMPTY 0
#define T_WALL  1
#define T_DOT   2
#define T_POW   3

/* 7 条全通横廊所在的行 */
/* The rows where the 7 full-width corridors are */
/* 1 4 7 10 13 16 19 */

int MAZE[MW * MH] = {
    /*  0 */ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
    /*  1 */ 3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,
    /*  2 */ 1,2,1,1,2,1,1,2,1,1,1,2,1,1,2,1,1,2,1,
    /*  3 */ 1,2,1,1,2,1,1,2,1,1,1,2,1,1,2,1,1,2,1,
    /*  4 */ 1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,
    /*  5 */ 1,2,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,2,1,
    /*  6 */ 1,2,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,2,1,
    /*  7 */ 1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,
    /*  8 */ 1,2,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,2,1,
    /*  9 */ 1,2,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,2,1,
    /* 10 */ 1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,
    /* 11 */ 1,2,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,2,1,
    /* 12 */ 1,2,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,2,1,
    /* 13 */ 1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,
    /* 14 */ 1,2,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,2,1,
    /* 15 */ 1,2,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,2,1,
    /* 16 */ 1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,
    /* 17 */ 1,2,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,2,1,
    /* 18 */ 1,2,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,2,1,
    /* 19 */ 1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,
    /* 20 */ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
};

/* ── 配色：平面高对比，不用渐变 ─────────────────────────── */
/* ── Colors: flat and high-contrast, no gradients ─────────────────────────── */

#define C_BG      0xFF0A0A14
#define C_WALL    0xFF2B4BC8
#define C_WALL_IN 0xFF14205E
#define C_DOT     0xFFFFE9B0
#define C_POW     0xFFFFC24B
#define C_PAC     0xFFFFD93B
#define C_PAC_M   0xFF0A0A14
#define C_GHOST0  0xFFFF4D4D
#define C_GHOST1  0xFF4DE1FF
#define C_GHOST2  0xFFFFB8DE
#define C_GHOST3  0xFFFFA24D
#define C_FRIGHT  0xFF3A4BD8
#define C_EYE_W   0xFFFFFFFF
#define C_EYE_B   0xFF14204A
#define C_HUD     0xCC05050E
#define C_TEXT    0xFFE8EEFF
#define C_DIM     0xFF8FA3C8
#define C_WARN    0xFFFF6B6B

#define NG 4                /* 鬼数 */
// number of ghosts
#define TICK 40             /* 一拍 40ms */
// one tick is 40ms

/* ── 状态 ───────────────────────────────────────────────── */
/* ── State ───────────────────────────────────────────────── */

int g_lang;                  /* 界面语言：开局查一次（ui_scr_get_language 是 syscall，别每帧调） */
// UI language: queried once at startup (ui_scr_get_language is a syscall, don't call it every frame)
int W, H, TILE, OX, OY;
int frame, state;           /* 0=玩 1=暂停 2=死 3=过关 */
// 0=playing 1=paused 2=died 3=level clear
int acc;                    /* 运动墙钟欠账（毫秒）—— 按墙钟补步，用户 2026-10-04 口径 */
                            /* Motion wall-clock debt in ms -- steps are caught up from the wall clock (user's 2026-10-04 rule) */
int lastMs;                 /* 上次补步取的墙钟时刻（进主循环时初始化） */
                            /* wall-clock time of the last catch-up step (initialized when entering the main loop) */
int score, best, level, lives;
int dotsLeft;

int pxp, pyp;               /* 吃豆人像素坐标（中心）*/
// Pac-Man's pixel coordinates (center)
int pdir, pwant;            /* 当前朝向 / 期望朝向（0上 1右 2下 3左）*/
// current direction / wanted direction (0 up, 1 right, 2 down, 3 left)
int mouth;                  /* 嘴张合动画相位 */
// mouth open/close animation phase
int fright;                 /* 剩余惊吓拍数 */
// remaining frightened ticks
int pdead;                  /* 死亡动画计时 */
// death animation timer

int gx[NG], gy[NG], gdir[NG];              /* 鬼：像素坐标 / 朝向 */
// ghosts: pixel coordinates / direction

/* 各鬼的**出场拍数** —— 必须错开。
 * Each ghost's **entry tick** — they must be staggered.
 * 四只鬼的出生点挨在一起（都在迷宫中部那一行），不错开就是"开局一起扑过来"：
 * The four ghosts spawn right next to each other (all on the maze's middle row), so without staggering they "all pounce at once" the moment the game starts:
 * 实测玩家在第 133 拍（约 5 秒）就掉一条命，连熟悉操作的时间都没有。
 * measured, the player loses a life by tick 133 (about 5 seconds), with no time even to get used to the controls.
 * 0 = 一开始就在场上。
 * 0 = on the board from the very start.
 */
int grel[NG] = { 0, 70, 160, 260 };

/* 游走（scatter）阶段各自的角 —— 原作就是这个机制，追击与游走交替 */
/* Each ghost's own corner during the scatter phase — the original game works this way, chase and scatter alternating */
int gcorner_x[NG] = { 1, 17, 1, 17 };
int gcorner_y[NG] = { 1, 1, 19, 19 };

char nbuf[16];

/* 方向增量：上 右 下 左 */
/* Direction deltas: up, right, down, left */
int DX[4] = { 0, 1, 0, -1 };
int DY[4] = { -1, 0, 1, 0 };

/* ── 小工具 ─────────────────────────────────────────────── */
/* ── Small helpers ─────────────────────────────────────────────── */

char* num_str(int v) {
    int i;
    int n;
    if (v < 0) { nbuf[0] = '-'; nbuf[1] = '0'; nbuf[2] = 0; return nbuf; }
    i = 0;
    if (v == 0) { nbuf[0] = '0'; nbuf[1] = 0; return nbuf; }
    while (v > 0) { nbuf[i] = '0' + (v % 10); i = i + 1; v = v / 10; }
    nbuf[i] = 0;
    n = 0;
    while (n < i / 2) { char t; t = nbuf[n]; nbuf[n] = nbuf[i - 1 - n]; nbuf[i - 1 - n] = t; n = n + 1; }
    return nbuf;
}

int tile_at(int c, int r) {
    if (c < 0 || c >= MW || r < 0 || r >= MH) return T_WALL;
    return MAZE[r * MW + c];
}

int is_wall(int c, int r) { return tile_at(c, r) == T_WALL; }

/* 格 → 像素中心 */
/* Tile → pixel center */
int cx_of(int c) { return OX + c * TILE + TILE / 2; }
int cy_of(int r) { return OY + r * TILE + TILE / 2; }

/* 像素点落在哪一格 */
/* Which tile a pixel falls into */
int col_of(int x) { int v = (x - OX) / TILE; if (v < 0) v = 0; if (v >= MW) v = MW - 1; return v; }
int row_of(int y) { int v = (y - OY) / TILE; if (v < 0) v = 0; if (v >= MH) v = MH - 1; return v; }

int abs_i(int v) { if (v < 0) return -v; return v; }
int rnd(int n) { if (n <= 0) return 0; return ui_math_rand(n); }

/* ── 音效：**用 ui_audio_beep 单音**（v0.96.509 统一换回来） ────────────
 * ── Sound effects: **single tones via ui_audio_beep** (switched back uniformly in v0.96.509) ────────────
 *
 * ⚠⚠ 这些音一度走共享库的音序器（`ui_sfx_add`），**真机上破音**，全部换回来了。
 * ⚠⚠ These sounds once went through the shared library's sequencer (`ui_sfx_add`) and **distorted on real hardware**, so they were all switched back.
 *   破音的是**这里配的音**：死亡/过关都是**几个音先后响**，末音还拖了 14 拍
 *   What distorted was **the sound configured here**: death / level-clear are **several tones sounding one after another**, and the last one dragged on for 14 ticks
 *   （一拍 33ms ≈ 460ms）—— 长音加外放就是"滋滋"。
 *   (one tick 33ms ≈ 460ms) — a long tone through a loudspeaker is exactly that "buzz".
 *   `ui_audio_beep` 是**单通道**的（后一个音掐掉前一个）⇒ 一个事件永远只有一个音在响，
 *   `ui_audio_beep` is **single-channel** (the next tone cuts off the previous one) ⇒ one event only ever has one tone sounding,
 *   **结构上不可能削波、也不会长音叠加**。代价是没有音色。
 *   so **clipping and long tones stacking up are structurally impossible**. The price is: no timbre.
 *
 * ⚠ 频率取整块的**首音**（吃豆最高最轻），时长取整块时长、封顶 320ms。
 * ⚠ The frequency is the block's **first tone** (eating a pellet is the highest and the lightest), the duration is the block's duration, capped at 320ms.
 *   **胜负是例外**：赢取整块**最高音**、输取**最低音** —— 「不看屏幕也分得出输赢」
 *   **Win/lose is the exception**: winning takes the block's **highest tone**, losing the **lowest** — "you can tell win from loss without looking at the screen"
 *   就靠这一条（五子棋/象棋两版也是这么配的：赢 1320 / 输 240）。
 *   rests on exactly this (the gomoku/chess versions are configured the same way: win 1320 / lose 240).
 *   低音不低于 C3(131Hz) —— 手机外放在 200Hz 以下衰减很快，玩家听着像没响。
 *   The low tone never goes below C3(131Hz) — a phone loudspeaker rolls off fast below 200Hz, so the player hears it as if nothing played.
 */

void sfx_dot(void) { ui_audio_beep(2093, 33); }        /* 吃豆：高频动作，必须又轻又短 */
// pellet: a high-frequency action, so it must be light and short
void sfx_pow(void) { ui_audio_beep(262, 165); }       /* 大力丸：有分量的一声 */
// power pellet: one weighty tone
void sfx_eat(void) { ui_audio_beep(1047, 231); }      /* 吃鬼：高而中长 = 爽 */
// eating a ghost: high and medium-long = satisfying
void sfx_die(void) {                            /* 死亡（输方）：**最低音**、最长 */
// death (the losing side): the **lowest tone**, the longest
    ui_audio_beep(131, 320);
    ui_vibrate_simple(220, 0);
}
void sfx_win(void) { ui_audio_beep(1047, 320); }      /* 过关（赢方）：**最高音**、最长 */
// level clear (the winning side): the **highest tone**, the longest

/* ── 开局 ───────────────────────────────────────────────── */
/* ── Starting a round ───────────────────────────────────────────────── */


void load_level(void) {
    int i;
    dotsLeft = 0;
    i = 0;
    while (i < MW * MH) {
        if (MAZE[i] == T_DOT || MAZE[i] == T_POW) dotsLeft = dotsLeft + 1;
        i = i + 1;
    }
}

void respawn(void) {
    int i;
    pxp = cx_of(9);
    pyp = cy_of(19);
    pdir = 3;                 /* 朝左，和原作一样 */
    // facing left, same as the original
    pwant = 3;
    mouth = 0;
    pdead = 0;
    fright = 0;
    i = 0;
    while (i < NG) {
        gx[i] = cx_of(8 + i);
        gy[i] = cy_of(10);
        gdir[i] = 0;
        i = i + 1;
    }
}

void new_game(void) {
    int i;
    score = 0;
    level = 1;
    lives = 3;
    state = 0;
    frame = 0;
    load_level();
    respawn();
}

/* ── 移动 ───────────────────────────────────────────────── */
/* ── Movement ───────────────────────────────────────────────── */

/* 能不能从格 (c,r) 朝 d 走一格 */
/* Whether one can step from tile (c,r) in direction d */
int can_go(int c, int r, int d) {
    return is_wall(c + DX[d], r + DY[d]) == 0;
}

/* 是否**正落在格心**上（精确判定，不给容差）。
 * Whether we are **exactly on a tile center** (an exact test, with no tolerance).
 *
 * ⚠⚠ 这条判据前后错了两次，两次都是**位置模型**的问题、不是笔误：
 * ⚠⚠ This criterion was wrong twice, and both times it was a **position model** problem, not a typo:
 *
 * ① 第一版写的是"坐标是不是格边长（TILE）的整数倍" —— 那是**格线**，而吃豆人停的是
 * ① The first version said "is the coordinate a multiple of the tile side (TILE)" — but that is a **tile boundary**, whereas Pac-Man stops on
 *    **格心**（`cx_of` = 原点 + c*TILE + TILE/2）⇒ 转向永远不触发、豆子永远吃不到。
 *    a **tile center** (`cx_of` = origin + c*TILE + TILE/2) ⇒ turning never triggered and pellets could never be eaten.
 * ② 第二版改成"离格心 ≤3px"，可移动还是**一次走 spd 像素**：`col_of()` 一越过格线就
 * ② The second version changed it to "within ≤3px of the tile center", but movement still **travelled spd pixels at a time**: the moment `col_of()` crossed a boundary it
 *    报下一列，"前面是墙"随即在**离格心 9px 的格线上**把人钉死。实测 400 拍
 *    reported the next column, and "there is a wall ahead" then pinned the player **on a tile boundary 9px from the center**. Measured over 400 ticks,
 *    `pdir` 恒为 1、`pxp` 恒为 316 —— 而 col17 的格心是 **325**；不吸附、不转向、
 *    `pdir` stayed at 1 and `pxp` stayed at 316 — while the tile center of col17 is **325**; no snapping, no turning,
 *    人再也不动（用户看到的正是"按键有反应、人不动"）。
 *    and the player never moved again (exactly the "the keys respond but the player does not move" that the user saw).
 *
 * 现在两处一起改：**逐像素推进 + 精确格心判定**（见 `move_pac`）。
 * Now both are changed together: **pixel-by-pixel advance + exact tile-center test** (see `move_pac`).
 * 「落在格心上」于是成了构造保证 —— 从格心出发、每次只走 1px，必然**精确经过**格心，
 * "Being on a tile center" thus becomes a guarantee by construction — starting from a center and moving 1px at a time necessarily **passes exactly through** the centers,
 * 与速度无关。旧写法在 `spd` 不整除 `TILE` 的关卡（4/5/7…）还会累积漂移。
 * independently of speed. The old approach also accumulated drift on levels where `spd` does not divide `TILE` evenly (4/5/7…).
 */
int at_center(int v, int origin) {
    int r = (v - origin) % TILE;
    if (r < 0) r = r + TILE;
    return r == TILE / 2;
}

/* 在格 (c,r) 吃豆。凑到格心才判 —— 与转向同一时刻，语义干净。*/
/* Eat the pellet at tile (c,r). Only judged once we reach the tile center — the same moment as turning, so the semantics stay clean. */
void eat_at(int c, int r) {
    if (tile_at(c, r) == T_DOT) {
        MAZE[r * MW + c] = T_EMPTY;
        dotsLeft = dotsLeft - 1;
        score = score + 10;
        sfx_dot();
    } else if (tile_at(c, r) == T_POW) {
        MAZE[r * MW + c] = T_EMPTY;
        dotsLeft = dotsLeft - 1;
        score = score + 50;
        fright = 140;
        sfx_pow();
    }
}

void move_pac(void) {
    int spd;
    int i;
    int c;
    int r;

    spd = 3 + level / 3;
    if (spd > 9) spd = 9;

    /* 一拍走 spd 像素，但**一像素一判**：只有"此刻正落在格心"才允许转向、吃豆、停下。
     * spd pixels are travelled per tick, but **each pixel is judged on its own**: turning, eating and stopping are only allowed "when we are right now on a tile center".
     * 到格心顺手吸附一次（把任何偏差归零），保证位置永远精确落在格心网格上。
     * Reaching a center also snaps once (zeroing any deviation), so the position always lands exactly on the tile-center grid.
     */
    i = 0;
    while (i < spd) {
        c = col_of(pxp);
        r = row_of(pyp);

        if (at_center(pxp, OX) && at_center(pyp, OY)) {
            pxp = cx_of(c);
            pyp = cy_of(r);
            eat_at(c, r);
            if (pwant != pdir && can_go(c, r, pwant)) pdir = pwant;
            if (!can_go(c, r, pdir)) return;      /* 前面是墙：停在格心 */
            // a wall ahead: stop on the tile center
        }

        pxp = pxp + DX[pdir];
        pyp = pyp + DY[pdir];
        i = i + 1;
    }
}

/* 鬼：朝吃豆人方向选一条能走的路（不原地掉头，除非无路可走）*/
/* Ghosts: pick a walkable route towards Pac-Man (no turning back on the spot, unless there is no way through) */
void move_ghost(int i) {
    int c;
    int r;
    int spd;
    int k;
    int d;
    int best;
    int bestv;
    int v;
    int tx;
    int ty;

    spd = 2 + level / 2;
    if (spd > 4) spd = 4;

    if (frame < grel[i]) return;     /* 还没到出场时间 */
    // not due to enter the board yet

    /* 与吃豆人**同一套走法**：逐像素推进，只在格心做抉择。
     * **The same walk as Pac-Man's**: advance pixel by pixel and only make choices at tile centers.
     * （旧版"不在格心就一次挪 2px"同样会卡在格线上，而且 spd=4 不整除 18 时还会漂。）
     * (The old "shift 2px at a time when not on a center" also got stuck on tile boundaries, and it drifted when spd=4 did not divide 18 evenly.)
     */
    k = 0;
    while (k < spd) {
        if (at_center(gx[i], OX) && at_center(gy[i], OY)) {
            c = col_of(gx[i]);
            r = row_of(gy[i]);
            gx[i] = cx_of(c);
            gy[i] = cy_of(r);

            /* 候选方向：能走的四个方向，排除掉头 */
            /* Candidate directions: the four walkable directions, excluding turning back */
            /* 目标格：追击段盯着吃豆人，游走段各自回自己的角（原作就有这个交替）。
             * Target tile: during the chase phase they fix on Pac-Man, during the scatter phase each returns to its own corner (the original alternates in exactly this way).
             * 没有游走段的话四只鬼会**无休止死追** —— 自测里那个贪心机器人
             * Without a scatter phase the four ghosts **chase relentlessly without end** — the greedy bot in the self-test
             * 4000 拍死了 42 次（约合 4 秒一条命），人玩只会更难受。
             * died 42 times in 4000 ticks (about one life every 4 seconds), and a human player would only have a worse time of it.
             */
            tx = col_of(pxp);
            ty = row_of(pyp);
            if ((frame / 300) % 2 == 1) { tx = gcorner_x[i]; ty = gcorner_y[i]; }

            best = -1;
            bestv = 1000000;
            d = 0;
            while (d < 4) {
                if (can_go(c, r, d)) {
                    if (d != ((gdir[i] + 2) & 3) || best < 0) {
                        /* 惊吓时逃（远离），平时靠近目标 */
                        /* When frightened, flee (move away); otherwise, approach the target */
                        v = abs_i(c + DX[d] - tx) + abs_i(r + DY[d] - ty);
                        if (fright > 0) v = -v;
                        v = v + rnd(2);              /* 一点随机，免得四只鬼叠在一起 */
                        // a touch of randomness so the four ghosts don't stack up on top of each other
                        if (v < bestv) { bestv = v; best = d; }
                    }
                }
                d = d + 1;
            }
            if (best < 0) best = (gdir[i] + 2) & 3;   /* 死路：掉头 */
            // dead end: turn back
            gdir[i] = best;
        }

        gx[i] = gx[i] + DX[gdir[i]];
        gy[i] = gy[i] + DY[gdir[i]];
        k = k + 1;
    }
}

void lose_life(void) {
    lives = lives - 1;
    state = 2;
    sfx_die();
    if (lives <= 0) {
        /* 保持 state=2，由 START 重开 */
        /* Keep state=2; START restarts the game */
    }
}

/* ── 每拍 ───────────────────────────────────────────────── */
/* ── Per tick ───────────────────────────────────────────────── */

void tick(void) {
    int i;
    int hit;

    if (state != 0) return;
    frame = frame + 1;
    mouth = (mouth + 1) % 8;

    if (fright > 0) fright = fright - 1;

    move_pac();

    i = 0;
    while (i < NG) {
        move_ghost(i);
        i = i + 1;
    }

    /* 与鬼碰撞 */
    /* Collision with the ghosts */
    i = 0;
    while (i < NG) {
        if (abs_i(gx[i] - pxp) < TILE * 3 / 4 && abs_i(gy[i] - pyp) < TILE * 3 / 4) {
            if (fright > 0) {
                /* 吃掉鬼：送回中间 */
                /* Eating a ghost: send it back to the middle */
                gx[i] = cx_of(9);
                gy[i] = cy_of(10);
                gdir[i] = 0;
                score = score + 200;
                sfx_eat();
            } else {
                lose_life();
                return;
            }
        }
        i = i + 1;
    }

    /* 过关 */
    /* Level clear */
    if (dotsLeft <= 0) {
        level = level + 1;
        state = 3;
        sfx_win();
        return;
    }
}

/* ── 绘制 ───────────────────────────────────────────────── */
/* ── Drawing ───────────────────────────────────────────────── */

void draw_maze(void) {
    int c;
    int r;
    int x;
    int y;
    int t;
    int pw;
    r = 0;
    while (r < MH) {
        c = 0;
        while (c < MW) {
            t = tile_at(c, r);
            x = OX + c * TILE;
            y = OY + r * TILE;
            if (t == T_WALL) {
                ui_2d_rect(x, y, TILE, TILE, C_WALL, 1, 0, 2);
                /* 内层深色：让墙看着有厚度，而不是一片蓝 */
                /* Inner darker layer: gives the wall some thickness instead of a flat slab of blue */
                ui_2d_rect(x + 2, y + 2, TILE - 4, TILE - 4, C_WALL_IN, 1, 0, 1);
            } else if (t == T_DOT) {
                pw = TILE / 6;
                if (pw < 2) pw = 2;
                ui_2d_circle(x + TILE / 2, y + TILE / 2, pw, C_DOT, 1, 0);
            } else if (t == T_POW) {
                pw = TILE / 3 + (mouth < 4 ? 1 : 0);   /* 轻微脉动 */
                // a slight pulse
                ui_2d_circle(x + TILE / 2, y + TILE / 2, pw, C_POW, 1, 0);
                ui_2d_circle(x + TILE / 2, y + TILE / 2, pw / 2, 0xFFFFFFFF, 1, 0);
            }
            c = c + 1;
        }
        r = r + 1;
    }
}

/* 嘴楔子：具名全局数组装顶点（复合字面量仍不支持，2026-10-04 复验会直接编译报错）*/
/* Mouth wedge: a named global array holds the vertices (compound literals are not supported) */
int wedge[8];

void ui_polygon_wedge(int cx, int cy, int r, int dir) {
    if (dir == 0) {            /* 朝右的楔 */
        // a wedge pointing right
        wedge[0] = cx;      wedge[1] = cy;
        wedge[2] = cx + r;  wedge[3] = cy - r;
        wedge[4] = cx + r;  wedge[5] = cy + r;
    } else if (dir == 1) {     /* 朝左 */
        // pointing left
        wedge[0] = cx;      wedge[1] = cy;
        wedge[2] = cx - r;  wedge[3] = cy - r;
        wedge[4] = cx - r;  wedge[5] = cy + r;
    } else if (dir == 2) {     /* 朝上 */
        // pointing up
        wedge[0] = cx;      wedge[1] = cy;
        wedge[2] = cx - r;  wedge[3] = cy - r;
        wedge[4] = cx + r;  wedge[5] = cy - r;
    } else {                   /* 朝下 */
        // pointing down
        wedge[0] = cx;      wedge[1] = cy;
        wedge[2] = cx - r;  wedge[3] = cy + r;
        wedge[4] = cx + r;  wedge[5] = cy + r;
    }
    ui_2d_polygon(wedge, 3, C_BG, 0, 0, "");
}
void draw_pac(void) {
    int r;
    int a;
    int open;
    open = (mouth < 4) ? 1 : 0;

    r = TILE * 2 / 5;
    if (r < 4) r = 4;
    ui_2d_circle(pxp, pyp, r, C_PAC, 1, 0);

    /* 嘴：用背景色的三角"咬"掉一口。方向按 pdir 分四种写死 ——
     * Mouth: a triangle in the background colour "bites" a piece out. The direction is hard-coded as four cases on pdir —
     * 省掉三角函数，也省掉"旋转多边形"那类几何风险。
     * which saves both trigonometry and that class of geometric risk, "rotating polygons".
     */
    if (open != 0) {
        if (pdir == 1) {                       /* 右 */
            // right
            ui_polygon_wedge(pxp, pyp, r + 2, 0);
        } else if (pdir == 3) {                /* 左 */
            // left
            ui_polygon_wedge(pxp, pyp, r + 2, 1);
        } else if (pdir == 0) {                /* 上 */
            // upwards
            ui_polygon_wedge(pxp, pyp, r + 2, 2);
        } else {                               /* 下 */
            // down
            ui_polygon_wedge(pxp, pyp, r + 2, 3);
        }
    }
}


void draw_ghosts(void) {
    int i;
    int r;
    int ex;
    int ey;
    int body;
    i = 0;
    while (i < NG) {
        r = TILE * 2 / 5;
        if (r < 4) r = 4;
        body = C_FRIGHT;
        if (fright <= 0) {
            if (i == 0) body = C_GHOST0;
            else if (i == 1) body = C_GHOST1;
            else if (i == 2) body = C_GHOST2;
            else body = C_GHOST3;
        }
        /* 身体：上半圆 + 下方方块（不用路径，纯圆+矩形拼）*/
        /* Body: a top semicircle + a block below (no paths, just a circle and a rectangle put together) */
        ui_2d_circle(gx[i], gy[i] - r / 3, r, body, 1, 0);
        ui_2d_rect(gx[i] - r, gy[i] - r / 3, r * 2, r + r / 3, body, 1, 0, 0);
        /* 裙摆：三个小方块 */
        /* Skirt: three small blocks */
        ui_2d_rect(gx[i] - r, gy[i] + r - 3, r * 2 / 3, 4, C_BG, 1, 0, 0);
        ui_2d_rect(gx[i] + r / 3, gy[i] + r - 3, r * 2 / 3, 4, C_BG, 1, 0, 0);

        /* 眼 */
        /* Eyes */
        ex = r / 2;
        ey = r / 3;
        ui_2d_circle(gx[i] - ex, gy[i] - ey, r / 3, C_EYE_W, 1, 0);
        ui_2d_circle(gx[i] + ex, gy[i] - ey, r / 3, C_EYE_W, 1, 0);
        ui_2d_circle(gx[i] - ex + DX[gdir[i]] * 2, gy[i] - ey + DY[gdir[i]] * 2, r / 6, C_EYE_B, 1, 0);
        ui_2d_circle(gx[i] + ex + DX[gdir[i]] * 2, gy[i] - ey + DY[gdir[i]] * 2, r / 6, C_EYE_B, 1, 0);
        i = i + 1;
    }
}

void draw_hud(void) {
    int i;
    ui_2d_rect(0, 0, W, 30, C_HUD, 1, 0, 0);
    ui_text_set_font(13, VML_FONT_BOLD, C_TEXT, VML_ANCHOR_LEFT);
    ui_2d_text_cur(8, 7, num_str(score));
    ui_text_set_font(10, 0, C_DIM, VML_ANCHOR_LEFT);
    ui_2d_text_cur(8, 20, g_lang == 0 ? "分数" : "Score");

    ui_text_set_font(11, 0, C_DIM, VML_ANCHOR_CENTER);
    /* "Level"（≈28px）比"第"（11px）宽，但两边（关卡数字 / "剩·Left"）都还留着空隙 */
    /* "Level" (≈28px) is much wider than the one-character label (11px), but both sides (the level number / the "Left" label) still keep a gap */
    ui_2d_text_cur(W / 2 - 26, 12, g_lang == 0 ? "第" : "Level");
    ui_text_set_font(13, VML_FONT_BOLD, C_TEXT, VML_ANCHOR_CENTER);
    ui_2d_text_cur(W / 2, 12, num_str(level));
    ui_text_set_font(11, 0, C_DIM, VML_ANCHOR_CENTER);
    ui_2d_text_cur(W / 2 + 34, 12, g_lang == 0 ? "剩" : "Left");
    ui_text_set_font(13, VML_FONT_BOLD, C_POW, VML_ANCHOR_CENTER);
    ui_2d_text_cur(W / 2 + 62, 12, num_str(dotsLeft));

    /* 命：黄圆点 */
    /* Lives: yellow dots */
    i = 0;
    while (i < lives && i < 5) {
        ui_2d_circle(W - 18 - i * 18, 15, 6, C_PAC, 1, 0);
        i = i + 1;
    }

    if (fright > 0) {
        ui_text_set_font(12, VML_FONT_BOLD, C_GHOST1, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, 42, g_lang == 0 ? "鬼可吃！" : "Eat the ghosts!");
    }

    if (state == 1) {
        ui_2d_rect(0, H / 2 - 36, W, 72, 0xCC000000, 1, 0, 0);
        ui_text_set_font(22, VML_FONT_BOLD, C_TEXT, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, H / 2 - 24, g_lang == 0 ? "暂停" : "Paused");
        ui_text_set_font(12, 0, C_DIM, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, H / 2 + 6, g_lang == 0 ? "SELECT 继续" : "SELECT to resume");
    }
    if (state == 2) {
        ui_2d_rect(0, H / 2 - 56, W, 112, 0xCC000000, 1, 0, 0);
        ui_text_set_font(22, VML_FONT_BOLD, C_WARN, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, H / 2 - 38, g_lang == 0 ? "被抓住了" : "Caught!");
        ui_text_set_font(13, 0, C_TEXT, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, H / 2 - 4, g_lang == 0 ? "分数" : "Score");
        ui_text_set_font(18, VML_FONT_BOLD, C_PAC, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, H / 2 + 20, num_str(score));
        ui_text_set_font(12, 0, C_DIM, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, H / 2 + 46, g_lang == 0 ? "START 重新开始" : "START to restart");
    }
    if (state == 3) {
        ui_2d_rect(0, H / 2 - 44, W, 88, 0xE6000000, 1, 0, 0);
        ui_text_set_font(22, VML_FONT_BOLD, C_POW, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, H / 2 - 26, g_lang == 0 ? "过关！" : "Level clear!");
        ui_text_set_font(13, 0, C_TEXT, VML_ANCHOR_CENTER);
        ui_2d_text_cur(W / 2, H / 2 + 4, g_lang == 0 ? "START 进入下一关" : "START for next level");
    }
}

void draw_all(void) {
    /* ⚠ **必须先 clear**：所有绘制都是往图元表追加，只有它会清空。
     * ⚠ **It must be cleared first**: every drawing call appends to the figure list, and only this one empties it.
     * 漏了就是"每帧叠上一帧"—— 画面糊掉，且图元表无限增长直到闪退
     * Miss it out and "each frame is stacked on the previous one" — the image smears, and the figure list grows without bound until the app crashes
     * （starfall.c 第一版就栽在这上面，见那边文件头的记录）。
     * (the first version of starfall.c fell over on exactly this, see the note at the top of that file).
     */
    ui_2d_clear(C_BG);

    draw_maze();
    draw_pac();
    draw_ghosts();
    draw_hud();
    ui_2d_present();
}

/* ── 输入 ───────────────────────────────────────────────── */
/* ── Input ───────────────────────────────────────────────── */

void on_key(int k) {
    if (k == VML_KEY_SELECT || k == VML_KEY_PAUSE) {
        if (state == 0) state = 1;
        else if (state == 1) state = 0;
        draw_all();
        return;
    }
    if (k == VML_KEY_ENTER) {
        if (state == 3) {
            /* 下一关：重铺豆子，鬼回中，吃豆人回原位 */
            /* Next level: lay the pellets out again, ghosts back to the middle, Pac-Man back to his start */
            state = 0;
            frame = 0;               /* 重置出场计时，否则新关卡一开始四只鬼就全在场 */
            // reset the entry timer, otherwise all four ghosts are on the board the moment a new level starts
            respawn();
            load_level();
            draw_all();
            return;
        }
        /* 死亡或暂停中按 START：整局重开 */
        /* Pressing START while dead or paused: restart the whole game */
        new_game();
        draw_all();
        return;
    }
    if (state != 0) return;

    if (k == VML_KEY_UP) pwant = 0;
    else if (k == VML_KEY_RIGHT) pwant = 1;
    else if (k == VML_KEY_DOWN) pwant = 2;
    else if (k == VML_KEY_LEFT) pwant = 3;
}

/* ── 主循环 ─────────────────────────────────────────────── */
/* ── Main loop ─────────────────────────────────────────────── */

int main(void) {
    int msg[4];
    int t;
    int sw;
    int sh;
    int tw;
    int th;
    int mx;
    int my;

    g_lang = ui_scr_get_language();
    ui_win_open(g_lang == 0 ? "吃豆人" : "Pac-Man", 0, 0);
    sw = ui_scr_get_w();
    sh = ui_scr_get_h();
    if (sw <= 0) sw = 360;
    if (sh <= 0) sh = 620;
    ui_scr_set_keep_on(1);

    W = sw;
    H = sh;

    /* 格边长：横竖都装得下 19×21，再居中 */
    /* Tile side length: fit 19×21 both ways, then center */
    tw = (W - 8) / MW;
    th = (H - 40) / MH;
    TILE = tw;
    if (th < TILE) TILE = th;
    if (TILE < 8) TILE = 8;
    OX = (W - MW * TILE) / 2;
    OY = 34 + ((H - 34) - MH * TILE) / 2;
    if (OX < 0) OX = 0;
    if (OY < 34) OY = 34;

    new_game();
    draw_all();

    ui_timer_set(TICK, 0);

    /* ⚠ 中文这条整句写在同一行里：跨行拼接时后续几段会落在没有 `g_lang == 0` 的行上
       ⚠ The Chinese sentence is kept whole on a single line: split across lines, the later fragments would land on lines without `g_lang == 0`
       （语言审计脚本按行认），而相邻字面量拼接本来就只是 C 的语法糖。
       (the language audit script decides by line), and concatenating adjacent literals is just C syntactic sugar anyway.
       */
    if (g_lang == 0) ui_dlg_msg("吃豆人", "用屏幕下方的方向键控制：按哪个方向就走哪个方向，到岔口自动转。吃光所有豆子过关；吃到大豆子（金圈）后可以反过来吃鬼。START 重开、SELECT 暂停。", VML_DLG_INFO);
    else             ui_dlg_msg("Pac-Man", "Use the on-screen arrows: press a direction and it turns at the next opening. Clear all dots to advance; after a power pellet the ghosts are edible. START restart, SELECT pause.", VML_DLG_INFO);

    /* 墙钟起点：进主循环时取（不在模块级/全局初始化里调 syscall） */
    /* wall-clock origin: sampled when entering the main loop (no syscall at module/global-init level) */
    acc = 0;
    lastMs = ui_time_get_ms();

    while (ui_win_is_closed() == 0) {
        /* 这一帧该走几步：**按墙钟欠账**（各平台定时器分辨率不同 ⇒ 不能拿"收到几拍"当时间）。
         * how many steps this frame takes: **driven by the wall-clock deficit** (timer resolution differs per platform => never treat "ticks received" as time).
         * 用户 2026-10-04 口径：所有游戏时钟取系统毫秒数。
         * user's 2026-10-04 rule: all game clocks read system milliseconds. */
        {
            int now2 = ui_time_get_ms();
            acc = acc + (now2 - lastMs);
            lastMs = now2;
            if (acc > TICK * 8) acc = 0;        /* 卡太久别一次补一大串（防瞬移） */
                                                /* do not catch up in one huge burst after a long stall (prevents teleporting) */
            if (state != 0) acc = 0;            /* 暂停/死/过关不累积 */
                                                /* no accumulation while paused/dead/level-cleared */
            if (acc >= TICK) {
                while (acc >= TICK) { acc = acc - TICK; tick(); }
                draw_all();
            }
        }

        t = ui_msg_wait(msg, 0);
        if (t == 0) continue;
        if (t == VML_MSG_WINDOWCLOSE) break;

        if (t == VML_MSG_TIMER) {
            /* 定时器**只负责唤醒**（推进在上面按墙钟欠账走） */
            /* the timer **only wakes us up** (advancement happens above, driven by the wall-clock deficit) */
            continue;
        }
        /* 触摸也当方向键用：点屏幕上半/下半/左半/右半 —— 手柄之外的备用操作 */
        /* Touch also acts as the direction keys: tap the top/bottom/left/right half of the screen — a fallback beyond the gamepad */
        if (t == VML_MSG_TOUCHDOWN || t == VML_MSG_MOUSEDOWN) {
            if (state == 0) {
                mx = msg[1] - pxp;
                my = msg[2] - pyp;
                if (abs_i(mx) > abs_i(my)) { if (mx > 0) pwant = 1; else pwant = 3; }
                else { if (my > 0) pwant = 2; else pwant = 0; }
            }
            continue;
        }
        if (t == VML_MSG_KEYDOWN) {
            if (msg[1] == VML_KEY_ESCAPE) break;
            on_key(msg[1]);
            continue;
        }
    }

    ui_scr_set_keep_on(0);
    ui_sfx_panic();
    ui_win_close();
    return 0;
}
