← 都来码 / Dolaima← 例程索引javascript/app/maze.js原文

javascript/app/maze.js · 40600 字节 · 下载原文

// 迷宫寻路 —— 用 **JavaScript** 写的手机游戏(走格子 / 迷宫品类)

// Maze Runner -- a mobile game written in **JavaScript** (grid / maze genre)

//

// 玩法:程序随机挖一座迷宫(深度优先 + 显式栈),从左上角走到右下角,步数越少越好。

// Gameplay: the program carves a random maze (depth-first with an explicit stack); walk from the top-left to the bottom-right in as few steps as possible.

// 按 `H` 提示下一步(BFS 从出口算最短距离),走到出口弹出成绩单 + 排行榜。

// Press `H` for a hint (BFS from the exit computes shortest distances); reaching the exit pops a score card plus a leaderboard.

//

// ## 操作

// ## Controls

//

//   · 方向键 / WASD 走一步(撞墙有闷响,不扣分)

//   · Arrow keys / WASD take one step (a wall is a dull thud, no penalty)

//   · **点迷宫里的任意一格** —— 沿那条唯一的通路朝它走一步(手机上的主要操作方式)

//   · **Tapping any cell in the maze** -- walks one step toward it along the one route (the main way to play on a phone)

//   · `H` 提示 · `R` / 回车 换一座新迷宫 · `Esc` 退出

//   · `H` hint, `R` / Enter a new maze, `Esc` quit

//

// ## 为什么这个游戏**没有定时器**

// ## Why this game has **no timer**

//

// 它是回合制的:画面只在「走了一步 / 按了键」之后才变。

// It is turn-based: the picture only changes after "a step was taken / a key was pressed".

// 一座 11×15 的迷宫有 ≈190 段墙,每帧重画一遍是纯浪费(手机上每帧要几百次绘制调用)。

// An 11x15 maze has about 190 wall segments; redrawing every frame is pure waste (hundreds of draw calls per frame on a phone).

// ⇒ 没有 `ui_timer_set`,主循环就阻塞在 `ui_msg_fetch_wait(0)` 上;这与「真·实时游戏必须开定时器」不矛盾。

// => There is no `ui_timer_set`; the main loop simply blocks on `ui_msg_fetch_wait(0)`. That does not contradict "a real-time game must start a timer".

//

// ## 这一门前端的四条硬约束(全部实测,照抄 `typing_rain.js` 那份的写法)

// ## Four hard constraints of this frontend (all measured; the same discipline as `typing_rain.js`)

//

//   ① ~~**文件顶层的语句一条都不执行**(有 `function main()` 时入口标签就是 `main`)⇒ 全局状态全在 `newRound()`/`main()` 里赋值。~~

//   ① ~~**No top-level statement runs** (with `function main()` present the entry label *is* `main`) => global state is assigned inside `newRound()`/`main()`.~~

//      ✅ **已修好(2026-10-04 复验)**:顶层语句现在会跑(实测顶层 `console.log("TOP");` 先打出 `TOP`,`var G = 5;` 读回 5)。

//      ✅ **fixed (re-checked 2026-10-04)**: top-level statements now run (measured: a top-level `console.log("TOP");` prints `TOP`; a top-level `var G = 5;` reads back 5).

//      ⚠ **2026-10-13 补完**:2026-10-04 那次只修好了「顶层**没有**显式调 `main()`」这一种形状;

//         顶层写了 `main();` 的仍然红(`var K = 88;` 在 `main()` 里读回 0)—— 因为那时入口标签仍与用户

//         那个 `function main` 撞名。本次把入口换成顶层块**自己的**标签(`__js_toplevel`)⇒ 两种形状都对。

//      ⚠ **completed 2026-10-13**: the 2026-10-04 fix only covered programs whose top level does *not*

//         explicitly call `main()`; with a top-level `main();` the variable still read back 0, because the

//         entry label still collided with the user's `function main`. The entry is now the top-level block's

//         **own** label (`__js_toplevel`), so both shapes work.

//      ⇒ 本文件**照旧**把全局状态放 `newRound()`/`main()` 里赋值(能跑就别动;新写的代码不必再绕)。

//      => this file **still** assigns global state inside `newRound()`/`main()` (if it runs, leave it alone; newly written code need not work around it).

//   ② **函数里的 `var` 是全局标签,只有形参是真局部** ⇒ 每个函数用前缀命名自己的 `var`(`g*`、`dk*`…)。

//   ② **A `var` inside a function is a global label; only parameters are truly local** => every function prefixes its own `var` names (`g*`, `dk*`, ...).

//   ③ ~~**常量必须 `#define`**(`var C_X = …` 在文件级永远是 0;颜色 0 = alpha 全透明 = 黑屏)。~~

//   ③ ~~**Constants must be `#define`s** (a file-level `var C_X = …` is always 0; colour 0 = fully transparent = a black screen).~~

//      ~~⚠ 而且**未定义的名字不报错**:本轮把 `C_BLUE` 拼漏了,它被当成变量(= 0)编了过去,~~

//      ~~Warning: **an undefined name is not an error** either: `C_BLUE` was mistyped here and compiled as a variable (= 0),~~

//      ~~症状是结算里"转弯"那个数字**画了但全透明**(trace-draw 里 `str="4" 0x00000000`)。改一处拼写前先看颜色值。~~

//      ~~the symptom being a "turns" digit that **is drawn but fully transparent** (trace-draw shows `str="4" 0x00000000`). Check the colour value first when something is invisible.~~

//      ✅ **已修好(2026-10-04 复验)**:顶层 `var CTOP = -15724520` 画成 `0xFF101018`(不再 `0x00000000`);

//      ✅ **fixed (re-checked 2026-10-04)**: a top-level `var CTOP = -15724520` draws as `0xFF101018` (no longer `0x00000000`);

//        拼错的名字(`CMISPELLED`)现在报 `error: 未声明的变量 'CMISPELLED' [CodeGen_UndefinedVariable]`,编译失败。

//        a misspelt name (`CMISPELLED`) now reports `error: undeclared variable 'CMISPELLED' [CodeGen_UndefinedVariable]` and fails to compile.

//      ⇒ 本文件照旧全用 `#define`(能跑就别动;新写的代码可以用 `var`)。

//      => this file **still** uses `#define` throughout (if it runs, leave it alone; newly written code may use `var`).

//   ④ **`push` 不扩容、`s.length` 读串的前 4 字节** ⇒ 见下面两条各自的注释。

//   ④ **`push` never grows, and `s.length` reads a string's first four bytes** => see the comments on each below.

//

// ## 数组的定容与「长大」:`bigArr()`

// ## Fixed capacity, and how arrays "grow" here: `bigArr()`

//

// `arr_push` 只把长度 +1 再写一个元素,**没有容量字段**;字面量的容量就是它的格数。

// `arr_push` only increments the length and writes one element; **there is no capacity field**, so a literal's capacity is exactly its slot count.

// 迷宫要 165 格(11×15),写字面量太笨 ⇒ `bigArr()` 用 `a = a.concat(a)` **翻倍八次**造出容量 256 的数组。

// The maze needs 165 cells (11x15) and writing that literal is clumsy => `bigArr()` uses `a = a.concat(a)` **eight times** to build a 256-slot array.

// (`concat` 自己 malloc 一块正好的缓冲,所以这么长大的数组容量是**确定**的。)

// (`concat` mallocs an exactly sized buffer itself, so an array grown this way has a **definite** capacity.)

// 长度头在 `ptr+0`、元素 i 在 `ptr+4+4i`,**从 JS 侧写 `arr[0-1]` 就是写长度头** ⇒ 用它把数组清空。

// The length header is at `ptr+0` and element i at `ptr+4+4i`; **`arr[0-1]` from JS writes that header** => that is how the array is emptied.

//

// ## 跑法

// ## How to run

//   手机   vml run examples/javascript/maze.js

//   phone  vml run examples/javascript/maze.js

//   桌面   vmlcli Examples/javascript/maze.js --vml-home <vml根> --screen 395x578 --trace-draw /tmp/tr.txt

//   desktop vmlcli Examples/javascript/maze.js --vml-home <vml root> --screen 395x578 --trace-draw /tmp/tr.txt


// UI 库声明:**一条显式引用**(本前端实测为**文档级** —— 删掉照样编过、画面一样,见文件尾的说明)。

// UI library declarations: **one explicit reference** (measured as **documentation-level** here -- deleting it still compiles and draws the same; see the note at the end of this file).

#include "vml_ui.js"

// 颜色:~~必须 `#define`~~(✅ **2026-10-04 复验:写 `var` 也不会变 0 了**,实测顶层 `var CTOP = -15724520` 画成 `0xFF101018`;

// Colours: ~~must be `#define`s~~ (✅ **re-checked 2026-10-04: a `var` no longer becomes 0**; measured: a top-level `var CTOP = -15724520` draws as `0xFF101018`;

//   本文件照旧全用 `#define`,能跑就别动;新写的代码可以用 `var`),负数十进制 = 0xAARRGGBB。

//   this file **still** uses `#define` throughout, if it runs leave it alone; newly written code may use `var`); negative decimals equal 0xAARRGGBB.

#define C_BG -15724520
// 0xFF101018 background

#define C_WALL -11890471
// 0xFF4A90D9 wall

#define C_BLUE -11890471
// 0xFF4A90D9 blue (the turn counter)

#define C_DIM -15066588
// 0xFF1A1A24 dim panel

#define C_TEXT -6643536
// 0xFF9AA0B0 body text

#define C_GREEN -11409298
// 0xFF51E86E player

#define C_GOLD -2509750
// 0xFFD9B44A exit

#define C_RED -45747
// 0xFFFF4D4D red

#define C_MARK -8096
// 0xFFFFE060 hint marker

#define C_TRAIL -13669574
// 0xFF2F6B3A trail


// 墙的四个方向位(一个格子的墙用一个位掩码表示)。

// The four wall bits (one cell's walls are one bitmask).

#define N_BIT 1
// north bit

#define E_BIT 2
// east bit

#define S_BIT 4
// south bit

#define W_BIT 8
// west bit


// ── 全局状态(**全部在 newRound()/main() 里赋值** —— 见约束 ①)──

// -- Global state (**all assigned in newRound()/main()** -- see constraint (1)) --

var GW = 0;
// canvas width

var GH = 0;
// canvas height

var GLANG = 0;
// 0 = Chinese

var GCOLS = 0;
// maze columns (**assigned in main()** -- a file-level 11 would be 0 and the first `/ GCOLS` would divide by zero)

var GROWS = 0;
// maze rows (assigned in main() for the same reason)

var GCELL = 0;
// cell size in pixels

var GX0 = 0;
// maze origin x

var GY0 = 0;
// maze origin y

var GPU = 0;
// player column

var GPV = 0;
// player row

var GSTEPS = 0;
// steps taken

var GBEST = 0;
// optimal step count (BFS)

var GLIMIT = 0;
// step limit for this maze (3x the optimal); going past it loses the round

var GOVER = 0;
var GWIN = 0;
var GQUIT = 0;
var GTIER = 0;
// difficulty tier index

var GHU = -1;
// hint column (-1 = none)

var GHV = -1;
var GTURNS = 0;
// turn count

var GSCFAIL = 0;
var MAZE = 0;
var DIST = 0;
var DIST2 = 0;
// distance to a tapped cell (tap-to-walk)

var QUE = 0;
var PATH = 0;
var DIRS = 0;
var LED = 0;
var CAND = 0;
var MULT = 0;
var DOTSX = 0;
var DOTSY = 0;

// 容量 256 的数组:`a = a.concat(a)` 翻倍八次(1→2→4→…→256)。

// A 256-slot array: `a = a.concat(a)` doubles it eight times (1 -> 2 -> 4 -> ... -> 256).

// 每个函数里的 `var` 都是全局标签,所以这几个临时名也必须各用各的(`b1a`…`b8a`)。

// Every `var` in a function is a global label, so these temporaries must each have their own name (`b1a` ... `b8a`).

function bigArr() {
    var b1a = [0];
    b1a = b1a.concat(b1a);
    var b2a = b1a.concat(b1a);
    var b3a = b2a.concat(b2a);
    var b4a = b3a.concat(b3a);
    var b5a = b4a.concat(b4a);
    var b6a = b5a.concat(b5a);
    var b7a = b6a.concat(b6a);
    var b8a = b7a.concat(b7a);
    // 清空长度头 = 空数组(容量 256)

    // Zeroing the length header = an empty array (capacity 256)

    b8a[0 - 1] = 0;
    return b8a;
}

// 容量 4 / 8 的小数组。

// Small arrays of capacity 4 and 8.

function z4() { var z4a = [0, 0, 0, 0]; z4a[0 - 1] = 0; return z4a; }
// four-slot factory

function z8() { var z8a = [0, 0, 0, 0, 0, 0, 0, 0]; z8a[0 - 1] = 0; return z8a; }
// eight-slot factory


// 数字 → 单字符串(本前端没有可用的 intToStr;只能 if 链返回字面量)。

// Digit -> one-character string (no usable intToStr here; an if-chain returning literals is the only way).

function digitStr(pd) {
    if (pd == 0) { return "0"; }
    if (pd == 1) { return "1"; }
    if (pd == 2) { return "2"; }
    if (pd == 3) { return "3"; }
    if (pd == 4) { return "4"; }
    if (pd == 5) { return "5"; }
    if (pd == 6) { return "6"; }
    if (pd == 7) { return "7"; }
    if (pd == 8) { return "8"; }
    return "9";
}

// 画非负整数(逐位查表;字号 20 的一位推进量实测 = 10px,见 typing_rain.js 的同名说明)。

// Draw a non-negative integer (one table lookup per digit; at size 20 a digit advances 10 px, measured -- same helper as in typing_rain.js).

function drawNum(dnx, dny, dnv, dncol, dnsz) {
    var dnnd = 1;
    var dnt = dnv;
    while (dnt >= 10) { dnt = dnt / 10; dnnd = dnnd + 1; }
    var dnpx = dnx;
    var dnk = dnnd;
    while (dnk > 0) {
        dnk = dnk - 1;
        var dnpw = 1;
        var dnq = 0;
        while (dnq < dnk) { dnpw = dnpw * 10; dnq = dnq + 1; }
        ui_2d_text(dnpx, dny, digitStr((dnv / dnpw) % 10), dncol, dnsz, 0);
        dnpx = dnpx + dnsz / 2;
    }
}

// 格心 x / y(画玩家、路点、提示框共用同一套算式)。

// Cell centre x / y (the player, the trail dots and the hint box all use the same formula).

function cellX(pcx) { return GX0 + pcx * GCELL + GCELL / 2; }
// cell centre x

function cellY(pcy) { return GY0 + pcy * GCELL + GCELL / 2; }

// 生成迷宫:递归回溯(递归用**显式栈** —— 本前端没有可靠的深递归)。

// Carve the maze: recursive backtracker (recursion replaced by an **explicit stack** -- deep recursion is not reliable here).

// 这里的 `DIST` 暂且当「访问标记」(1 = 进过),`QUE` 当栈;`solveDist()` 随后会把它整个重写成 BFS 距离。

// Here `DIST` temporarily serves as the "visited" flag (1 = visited) and `QUE` as the stack; `solveDist()` then rewrites it entirely as BFS distances.

// 候选邻格先用 `filter` 收一遍(`filter` 在本游戏里的正当用处),空 ⇒ 回溯。

// The candidate neighbours are narrowed with `filter` (`filter`'s legitimate job in this game); empty => backtrack.

function genMaze() {
    var gnn = GCOLS * GROWS;
    MAZE[0 - 1] = 0;
    var gi = 0;
    // 15 = 四面都是墙

    // 15 = walls on all four sides

    while (gi < gnn) { MAZE.push(15); gi = gi + 1; }

    QUE[0 - 1] = 0;
    DIST[0 - 1] = 0;
    var gj = 0;
    // DIST:-1 = 还没进过迷宫

    // DIST: -1 = not carved into the maze yet

    while (gj < gnn) { DIST.push(0 - 1); gj = gj + 1; }

    QUE.push(0);
    DIST[0] = 1;
    while (QUE.length > 0) {
        var gcur = QUE[QUE.length - 1];
        var gcc = gcur % GCOLS;
        var gcr = gcur / GCOLS;
        // 四个方向里**在界内**的邻格先收齐,再用 filter 去掉已访问的

        // Collect the **in-bounds** neighbours of all four directions first, then drop the visited ones with filter

        CAND[0 - 1] = 0;
        if (gcr > 0) { CAND.push(gcur - GCOLS); }
        if (gcc < GCOLS - 1) { CAND.push(gcur + 1); }
        if (gcr < GROWS - 1) { CAND.push(gcur + GCOLS); }
        if (gcc > 0) { CAND.push(gcur - 1); }
        var gopen = CAND.filter(function (gcq) { return DIST[gcq] < 0; });
        if (gopen.length == 0) {
            QUE.pop();
        } else {
            var gpick = gopen[ui_math_rand(gopen.length)];
            // 拆掉两格之间的那面墙(`& ~bit` 清位)

            // Knock out the wall between the two cells (clear a bit with `& ~bit`)

            if (gpick == gcur - GCOLS) { MAZE[gcur] = MAZE[gcur] & (0 - 2); MAZE[gpick] = MAZE[gpick] & (0 - 5); }
            if (gpick == gcur + 1) { MAZE[gcur] = MAZE[gcur] & (0 - 3); MAZE[gpick] = MAZE[gpick] & (0 - 9); }
            if (gpick == gcur + GCOLS) { MAZE[gcur] = MAZE[gcur] & (0 - 5); MAZE[gpick] = MAZE[gpick] & (0 - 2); }
            if (gpick == gcur - 1) { MAZE[gcur] = MAZE[gcur] & (0 - 9); MAZE[gpick] = MAZE[gpick] & (0 - 3); }
            QUE.push(gpick);
            DIST[gpick] = 1;
        }
    }
    // 额外拆几面墙(档位越高越多)—— 迷宫就有了环路,不再是唯一解

    // Knock out a few extra walls (more at higher tiers) -- the maze gains loops, so the route is no longer unique

    var gk = 0;
    var gwant = GTIER * 3;
    while (gk < gwant) {
        var gcell = ui_math_rand(gnn);
        var gd = ui_math_rand(4);
        var gcc2 = gcell % GCOLS;
        var gcr2 = gcell / GCOLS;
        if (gd == 0 && gcr2 > 0) { MAZE[gcell] = MAZE[gcell] & (0 - 2); MAZE[gcell - GCOLS] = MAZE[gcell - GCOLS] & (0 - 5); }
        if (gd == 1 && gcc2 < GCOLS - 1) { MAZE[gcell] = MAZE[gcell] & (0 - 3); MAZE[gcell + 1] = MAZE[gcell + 1] & (0 - 9); }
        if (gd == 2 && gcr2 < GROWS - 1) { MAZE[gcell] = MAZE[gcell] & (0 - 5); MAZE[gcell + GCOLS] = MAZE[gcell + GCOLS] & (0 - 2); }
        if (gd == 3 && gcc2 > 0) { MAZE[gcell] = MAZE[gcell] & (0 - 9); MAZE[gcell - 1] = MAZE[gcell - 1] & (0 - 3); }
        gk = gk + 1;
    }
}

// BFS:从 `psrc` 往所有格算最短步数,写进 `parr`(-1 = 到不了 / 没访问过)。

// BFS: shortest step count from `psrc` to every cell, written into `parr` (-1 = unreachable / not visited).

// 队列用 `QUE`(push 入队、下标出队)。**数组可以当形参传**(形参是真局部),

// The queue is `QUE` (push to enqueue, an index to dequeue). **An array can be passed as a parameter** (parameters are truly local),

// 所以 `DIST`(到出口的距离,提示用)与 `DIST2`(到任意格的距离,点哪走哪用)共用这一份实现。

// so `DIST` (distance to the exit, for hints) and `DIST2` (distance to an arbitrary cell, for tap-to-walk) share this one implementation.

function bfsFrom(psrc, parr) {
    var bn = GCOLS * GROWS;
    parr[0 - 1] = 0;
    var bi = 0;
    while (bi < bn) { parr.push(0 - 1); bi = bi + 1; }
    QUE[0 - 1] = 0;
    parr[psrc] = 0;
    QUE.push(psrc);
    var bhead = 0;
    while (bhead < QUE.length) {
        var bcur = QUE[bhead];
        bhead = bhead + 1;
        var bd = parr[bcur];
        var bw = MAZE[bcur];
        var bcc = bcur % GCOLS;
        var bcr = bcur / GCOLS;
        if ((bw & N_BIT) == 0 && bcr > 0) { pushDist(parr, bcur - GCOLS, bd + 1); }
        if ((bw & E_BIT) == 0 && bcc < GCOLS - 1) { pushDist(parr, bcur + 1, bd + 1); }
        if ((bw & S_BIT) == 0 && bcr < GROWS - 1) { pushDist(parr, bcur + GCOLS, bd + 1); }
        if ((bw & W_BIT) == 0 && bcc > 0) { pushDist(parr, bcur - 1, bd + 1); }
    }
}
// 入队一个邻格(没访问过才入)。

// Enqueue a neighbour (only if it has not been reached yet).

function pushDist(parr, ppc, ppd) {
    if (parr[ppc] < 0) {
        parr[ppc] = ppd;
        QUE.push(ppc);
    }
}

// 朝 `ptar` 走一步:从目标格 BFS 一遍,挑一个「恰好近一步」的邻居(有唯一解就是唯一的那条路)。

// Take one step toward `ptar`: BFS from the target cell and pick the neighbour exactly one step closer (unique in a perfect maze).

// 这就是手机上的主要操作 —— 点任意一格都沿**那条唯一的通路**朝它挪一步,而不是"点哪跳哪"。

// This is the main mobile gesture: tapping any cell walks one step along **the one route** toward it, instead of teleporting there.

function stepToward(ptar) {
    bfsFrom(ptar, DIST2);
    var here = GPV * GCOLS + GPU;
    if (DIST2[here] <= 0) { return 0; }
    var hw = MAZE[here];
    var hcc = here % GCOLS;
    var hcr = here / GCOLS;
    if ((hw & N_BIT) == 0 && hcr > 0 && DIST2[here - GCOLS] == DIST2[here] - 1) { return tryMove(0); }
    if ((hw & E_BIT) == 0 && hcc < GCOLS - 1 && DIST2[here + 1] == DIST2[here] - 1) { return tryMove(1); }
    if ((hw & S_BIT) == 0 && hcr < GROWS - 1 && DIST2[here + GCOLS] == DIST2[here] - 1) { return tryMove(2); }
    if ((hw & W_BIT) == 0 && hcc > 0 && DIST2[here - 1] == DIST2[here] - 1) { return tryMove(3); }
    return 0;
}

// 走一步。dir: 0=北 1=东 2=南 3=西。撞墙只响一声闷的,不动。

// Take one step. dir: 0=north 1=east 2=south 3=west. A wall only thuds; nothing moves.

function tryMove(pdir) {
    if (GOVER != 0) { return 0; }
    var tw = MAZE[GPV * GCOLS + GPU];
    var tmv = 0;
    if (pdir == 0) { if ((tw & N_BIT) == 0 && GPV > 0) { GPV = GPV - 1; tmv = 1; } }
    if (pdir == 1) { if ((tw & E_BIT) == 0 && GPU < GCOLS - 1) { GPU = GPU + 1; tmv = 1; } }
    if (pdir == 2) { if ((tw & S_BIT) == 0 && GPV < GROWS - 1) { GPV = GPV + 1; tmv = 1; } }
    if (pdir == 3) { if ((tw & W_BIT) == 0 && GPU > 0) { GPU = GPU - 1; tmv = 1; } }
    if (tmv == 0) { ui_audio_beep(220, 70); return 0; }
    GSTEPS = GSTEPS + 1;
    if (DIRS.length < 250) { DIRS.push(pdir); }
    if (PATH.length < 250) { PATH.push(GPV * GCOLS + GPU); }
    ui_audio_beep(660, 50);
    if (GPU == GCOLS - 1 && GPV == GROWS - 1) { winGame(); return 1; }
    // 步数上限 = 最优步数的 3 倍:绕太久算输(有胜就得有负 —— 而且输的音高与赢差两个八度)。

    // Step limit = 3x the optimal count: wandering too long is a loss (a win needs a loss to go with it -- and the tones are two octaves apart).

    if (GSTEPS > GLIMIT) {
        GOVER = 1; GWIN = 0;
        countTurns();
        ui_audio_beep(131, 320);
    }
    return 1;
}

// 到达出口:算成绩、进排行榜、放最高音。

// Reached the exit: score it, record it in the leaderboard, and play the highest tone.

function winGame() {
    GWIN = 1; GOVER = 1;
    ui_audio_beep(1319, 320);
    if (LED.length < 8) { LED.push(GSTEPS); }
    countTurns();
}

// 转弯次数:用 forEach 扫一遍方向序列(回调里改的是全局的 GTURNS)。

// Turn count: one forEach sweep over the direction sequence (the callback writes the global GTURNS).

// 胜负两条路都要算(输的那局也想看得见"绕了多少")。

// Both outcomes compute it (even a lost round wants to show how much wandering happened).

function countTurns() {
    var tnprev = -1;
    GTURNS = 0;
    DIRS.forEach(function (tdir) {
        if (tnprev >= 0 && tdir != tnprev) { GTURNS = GTURNS + 1; }
        tnprev = tdir;
    });
}

// 走到出口时「路线里有没有重复踩过的格子」—— 用 indexOf 判某个格子在 PATH 里是不是首次出现。

// Whether the route revisited a cell -- `indexOf` tells if a cell appears for the first time at this position.

// 迷宫里绕回头很常见,这个数就是"走得绕不绕"的一个侧面判据(成绩单上不画,收尾时打给无头验证)。

// Doubling back is common in a maze, so this counts how winding the route was (not drawn on the card; printed for headless checks).

function countRevisit() {
    var crn = 0;
    var cri = 0;
    while (cri < PATH.length) {
        if (PATH.indexOf(PATH[cri]) != cri) { crn = crn + 1; }
        cri = cri + 1;
    }
    return crn;
}

// 开一局新的:清状态 → 挖迷宫 → 算最短距离。

// Start a new round: clear the state, carve a maze, compute the shortest distances.

function newRound() {
    GPU = 0; GPV = 0; GSTEPS = 0; GOVER = 0; GWIN = 0; GHU = -1; GHV = -1; GTURNS = 0;
    PATH[0 - 1] = 0;
    DIRS[0 - 1] = 0;
    PATH.push(0);
    genMaze();
    // DIST = 每格到出口的最短步数

    // DIST = fewest steps from each cell to the exit

    bfsFrom((GROWS - 1) * GCOLS + (GCOLS - 1), DIST);
    GBEST = DIST[0];
    GLIMIT = GBEST * 3;
}

// 按 H:给下一步的提示(挑一个「离出口恰好近一步」的邻居,BFS 距离已经算好了)。

// Press H: hint the next step (pick the neighbour that is exactly one step closer to the exit, using the BFS distances).

function doHint() {
    if (GOVER != 0) { return; }
    var hcell = GPV * GCOLS + GPU;
    var hd = DIST[hcell];
    GHU = -1; GHV = -1;
    if (hd <= 0) { return; }
    var hw = MAZE[hcell];
    var hcc = hcell % GCOLS;
    var hcr = hcell / GCOLS;
    if ((hw & N_BIT) == 0 && hcr > 0 && DIST[hcell - GCOLS] == hd - 1) { GHU = hcc; GHV = hcr - 1; }
    if (GHU < 0 && (hw & E_BIT) == 0 && hcc < GCOLS - 1 && DIST[hcell + 1] == hd - 1) { GHU = hcc + 1; GHV = hcr; }
    if (GHU < 0 && (hw & S_BIT) == 0 && hcr < GROWS - 1 && DIST[hcell + GCOLS] == hd - 1) { GHU = hcc; GHV = hcr + 1; }
    if (GHU < 0 && (hw & W_BIT) == 0 && hcc > 0 && DIST[hcell - 1] == hd - 1) { GHU = hcc - 1; GHV = hcr; }
    ui_audio_beep(880, 70);
}

// 按键。

// Key handling.

function keyDown(pk) {
    if (pk == 27) { GQUIT = 1; return; }
    if (GOVER != 0) {
        if (pk == 13 || pk == 82) { newRound(); }
        return;
    }
    if (pk == 13 || pk == 82) { newRound(); return; }
    if (pk == 72) { doHint(); return; }
    if (pk == 38 || pk == 87) { tryMove(0); return; }
    if (pk == 39 || pk == 68) { tryMove(1); return; }
    if (pk == 40 || pk == 83) { tryMove(2); return; }
    if (pk == 37 || pk == 65) { tryMove(3); return; }
}

// 点屏幕:点迷宫里的**任意一格**都朝它走一步(贴着的那格就是普通的一步);点 HUD 行 = 提示。

// Tap: any cell inside the maze takes one step toward it (an adjacent cell is simply one plain step); tapping the HUD row hints.

function touchAt(ptx, pty) {
    if (GOVER != 0) { newRound(); return; }
    if (pty < GY0) {
        doHint();
        return;
    }
    var tcc = (ptx - GX0) / GCELL;
    var tcr = (pty - GY0) / GCELL;
    if (tcc < 0 || tcr < 0 || tcc >= GCOLS || tcr >= GROWS) { return; }
    stepToward(tcr * GCOLS + tcc);
}

// 画迷宫:每格只画**北墙与西墙**,再补最后一列 / 最后一行的外墙 —— 一座 11×15 的迷宫约 190 条线。

// Draw the maze: per cell only the **north and west walls**, plus the outer walls of the last column / row -- about 190 lines for an 11x15 maze.

function drawMaze() {
    var dkr = 0;
    while (dkr < GROWS) {
        var dkc = 0;
        while (dkc < GCOLS) {
            var dkv = MAZE[dkr * GCOLS + dkc];
            var dkx = GX0 + dkc * GCELL;
            var dky = GY0 + dkr * GCELL;
            if ((dkv & N_BIT) != 0) { ui_2d_line(dkx, dky, dkx + GCELL, dky, C_WALL, 2); }
            if ((dkv & W_BIT) != 0) { ui_2d_line(dkx, dky, dkx, dky + GCELL, C_WALL, 2); }
            if (dkc == GCOLS - 1 && (dkv & E_BIT) != 0) { ui_2d_line(dkx + GCELL, dky, dkx + GCELL, dky + GCELL, C_WALL, 2); }
            if (dkr == GROWS - 1 && (dkv & S_BIT) != 0) { ui_2d_line(dkx, dky + GCELL, dkx + GCELL, dky + GCELL, C_WALL, 2); }
            dkc = dkc + 1;
        }
        dkr = dkr + 1;
    }
}

// 画一帧(只在「走了一步 / 按键 / 开了新局」之后调用)。

// Draw one frame (only after "a step / a key / a new round").

function drawAll() {
    ui_2d_clear(C_BG);

    // HUD:步数 / 最优 / 档位 / 最佳

    // HUD: steps / optimal / tier / best

    ui_2d_text(8, 4, GLANG == 0 ? "步数" : "Steps", C_TEXT, 13, 0);
    drawNum(48, 2, GSTEPS, C_GREEN, 20);
    ui_2d_text(120, 4, GLANG == 0 ? "最优" : "Best", C_TEXT, 13, 0);
    drawNum(160, 2, GBEST, C_GOLD, 20);
    ui_2d_text(224, 4, GLANG == 0 ? "上限" : "Limit", C_TEXT, 13, 0);
    drawNum(264, 2, GLIMIT, C_RED, 20);
    ui_2d_text(8, 30, GLANG == 0 ? "走到右下角;H 提示 / R 换迷宫 / 点任意格朝它走一步" : "Reach the bottom-right; H hint / R new maze / tap a cell to walk toward it", C_TEXT, 12, 0);

    drawMaze();

    // 走过的路:小方点(PATH 里第 i 个格子)

    // The trail: small squares (the i-th cell in PATH)

    var dai = 0;
    while (dai < PATH.length) {
        var dac = PATH[dai] % GCOLS;
        var dar = PATH[dai] / GCOLS;
        ui_2d_rect(cellX(dac) - 3, cellY(dar) - 3, 6, 6, C_TRAIL, 1, 0, 2);
        dai = dai + 1;
    }

    // 出口 + 玩家 + 提示

    // Exit + player + hint

    ui_2d_rect(cellX(GCOLS - 1) - GCELL / 3, cellY(GROWS - 1) - GCELL / 3, GCELL * 2 / 3, GCELL * 2 / 3, C_GOLD, 1, 0, 4);
    ui_2d_circle(cellX(GPU), cellY(GPV), GCELL / 3, C_GREEN, 1, 0);
    if (GHU >= 0 && GOVER == 0) {
        ui_2d_rect(cellX(GHU) - GCELL / 3, cellY(GHV) - GCELL / 3, GCELL * 2 / 3, GCELL * 2 / 3, C_MARK, 0, 3, 4);
    }

    // 自检失败红条(正常时什么都不画)

    // Self-check failure bar (nothing is drawn when everything passes)

    if (GSCFAIL > 0) {
        ui_2d_rect(0, GH - 6, GSCFAIL * 20, 6, C_RED, 1, 0, 0);
    }

    // 成绩单

    // Score card

    if (GOVER != 0) {
        ui_2d_rect(16, GH / 2 - 84, GW - 32, 168, C_DIM, 1, 0, 10);
        if (GWIN != 0) {
            ui_2d_text(GW / 2, GH / 2 - 72, GLANG == 0 ? "走出迷宫!" : "ESCAPED!", C_GREEN, 22, 1);
        }
        if (GWIN == 0) {
            ui_2d_text(GW / 2, GH / 2 - 72, GLANG == 0 ? "走过头了" : "GAME OVER", C_RED, 22, 1);
        }
        ui_2d_text(GW / 2 - 66, GH / 2 - 40, GLANG == 0 ? "步数" : "Steps", C_TEXT, 13, 0);
        drawNum(GW / 2 - 26, GH / 2 - 42, GSTEPS, C_GREEN, 18);
        ui_2d_text(GW / 2 + 14, GH / 2 - 40, GLANG == 0 ? "最优" : "Best", C_TEXT, 13, 0);
        drawNum(GW / 2 + 54, GH / 2 - 42, GBEST, C_GOLD, 18);
        ui_2d_text(GW / 2 - 66, GH / 2 - 14, GLANG == 0 ? "上限" : "Limit", C_TEXT, 13, 0);
        drawNum(GW / 2 - 26, GH / 2 - 16, GLIMIT, C_RED, 18);
        ui_2d_text(GW / 2 + 14, GH / 2 - 14, GLANG == 0 ? "转弯" : "Turns", C_TEXT, 13, 0);
        drawNum(GW / 2 + 54, GH / 2 - 16, GTURNS, C_BLUE, 18);
        // 排行榜:sort 升序(步数少的在前),reverse 之后取前 3(slice)

        // Leaderboard: sort ascending (fewest steps first), reverse, then take the top 3 (slice)

        var dls = LED.sort();
        dls.reverse();
        var dtop = dls.slice(0, 3);
        ui_2d_text(GW / 2 - 66, GH / 2 + 12, GLANG == 0 ? "排行" : "Top", C_TEXT, 13, 0);
        var dli = 0;
        while (dli < dtop.length) {
            drawNum(GW / 2 - 26 + dli * 44, GH / 2 + 10, dtop[dli], C_MARK, 18);
            dli = dli + 1;
        }
        ui_2d_text(GW / 2, GH / 2 + 44, GLANG == 0 ? "按回车 / 点屏幕 换一座新迷宫" : "Enter / tap for a new maze", C_MARK, 13, 1);
    }
    ui_2d_present();
}

// 自检:把本轮修好的那批方法逐条断言一遍,打「序号*10+通过」。开窗之前跑。

// Self-check: assert every method fixed this round, printing `index*10 + passed`. Runs before the window opens.

function selfcheck() {
    var scf = 0;
    var sci = 0;
    var scs = "Maze Runner";
    if (scs.startsWith("Maze") == 1) { sci = sci + 1; } else { scf = scf + 1; }
    if (scs.startsWith("Runner") == 0) { sci = sci + 1; } else { scf = scf + 1; }
    if (scs.endsWith("Runner") == 1) { sci = sci + 1; } else { scf = scf + 1; }
    if (scs.charAt(0) == 77 && scs.charAt(10) == 114) { sci = sci + 1; } else { scf = scf + 1; }
    if (scs.charAt(99) == 0) { sci = sci + 1; } else { scf = scf + 1; }
    var scu = "north";
    var scu2 = scu.toUpperCase();
    // ⚠ `toUpperCase` **返回新串、原串不变**(清单 BUG-11 修好之后)。

    //   这条从前写的是 `scu.charAt(4) == 72`('H')—— 那是在**利用**"原地改"这个缺陷;

    //   现在改成断言正确的 ECMAScript 语义:新串第 4 位是 'H'、原串第 0 位仍是 'n'。

    if (scu2.charAt(4) == 72 && scu.charAt(0) == 110) { sci = sci + 1; } else { scf = scf + 1; }
    var sct = "  wall  ";
    var sct2 = sct.trim();
    if (sct2.charAt(0) == 119 && sct2.charAt(4) == 0) { sci = sci + 1; } else { scf = scf + 1; }
    var scp = "3";
    var scp2 = scp.padStart(2, "0");
    if (scp2.charAt(0) == 48 && scp2.charAt(1) == 51) { sci = sci + 1; } else { scf = scf + 1; }
    var scq = "3";
    var scq2 = scq.padEnd(2, ".");
    if (scq2.charAt(0) == 51 && scq2.charAt(1) == 46) { sci = sci + 1; } else { scf = scf + 1; }
    var scv = "n,e,s,w";
    var scv2 = scv.split(",");
    if (scv2.length == 4) { sci = sci + 1; } else { scf = scf + 1; }
    // bigArr:concat 翻倍八次必须正好 256 格

    // bigArr: eight concat doublings must land on exactly 256 slots

    var scb = [0];
    var scb2 = scb.concat(scb);
    var scb3 = scb2.concat(scb2);
    var scb4 = scb3.concat(scb3);
    var scb5 = scb4.concat(scb4);
    var scb6 = scb5.concat(scb5);
    var scb7 = scb6.concat(scb6);
    var scb8 = scb7.concat(scb7);
    var scb9 = scb8.concat(scb8);
    if (scb9[0 - 1] == 256) { sci = sci + 1; } else { scf = scf + 1; }
    // 定容数组 + push/pop/indexOf/includes

    // Fixed-capacity array + push/pop/indexOf/includes

    var sca = z8();
    sca.push(7); sca.push(8); sca.push(9);
    if (sca.length == 3 && sca[1] == 8) { sci = sci + 1; } else { scf = scf + 1; }
    if (sca.indexOf(9) == 2 && sca.indexOf(99) == -1) { sci = sci + 1; } else { scf = scf + 1; }
    if (sca.includes(7) == 1 && sca.includes(99) == 0) { sci = sci + 1; } else { scf = scf + 1; }
    if (sca.pop() == 9 && sca.length == 2) { sci = sci + 1; } else { scf = scf + 1; }
    // sort / reverse / slice / concat / join

    var scc = z8();
    scc.push(31); scc.push(11); scc.push(21);
    var scc2 = scc.sort();
    if (scc2[0] == 11 && scc2[2] == 31) { sci = sci + 1; } else { scf = scf + 1; }
    var scc3 = scc.reverse();
    if (scc3[0] == 31 && scc3[2] == 11) { sci = sci + 1; } else { scf = scf + 1; }
    var scd = scc.slice(1, 3);
    if (scd.length == 2 && scd[0] == 21) { sci = sci + 1; } else { scf = scf + 1; }
    var sce1 = z4(); sce1.push(1); sce1.push(2);
    var sce2 = z4(); sce2.push(3); sce2.push(4); sce2.push(5);
    var sce3 = sce1.concat(sce2);
    if (sce3.length == 5 && sce3[4] == 5) { sci = sci + 1; } else { scf = scf + 1; }
    var scj = z4(); scj.push(4); scj.push(5);
    var scjs = scj.join("/");
    if (scjs.charAt(0) == 52 && scjs.charAt(1) == 47) { sci = sci + 1; } else { scf = scf + 1; }
    // forEach / map / filter(曾经拿数组指针当上界)

    // forEach / map / filter (they used to treat the array pointer as the upper bound)

    var scn = 0;
    var scg = z4(); scg.push(2); scg.push(4); scg.push(6);
    scg.forEach(function (scx) { scn = scn + scx; });
    if (scn == 12) { sci = sci + 1; } else { scf = scf + 1; }
    // ⚠ 回调里**不用 `/`**:JS 的 `/` 恒为浮点除法(清单 BUG-07 修好后 `6/2` 是 3.0,

    //   而本前端把无小数的整数存成 32 位整数、`scm[2] == 3` 比的是位模式)—— 改成 `* 2`。

    var scm = scg.map(function (scy) { return scy * 2; });
    if (scm.length == 3 && scm[2] == 12) { sci = sci + 1; } else { scf = scf + 1; }
    var scfl = scg.filter(function (scz) { return scz > 2; });
    if (scfl.length == 2 && scfl[0] == 4) { sci = sci + 1; } else { scf = scf + 1; }
    // parseFloat:正浮点的位模式按整数比大小 = 数值同序(本前端没有浮点算术,只能这样比)

    // parseFloat: for positive floats the bit pattern compares as an integer in numeric order (no float arithmetic here, so this is the only comparison available)

    if (parseFloat("0.5") < parseFloat("1.0")) { sci = sci + 1; } else { scf = scf + 1; }
    if (parseFloat("1.75") > parseFloat("1.5")) { sci = sci + 1; } else { scf = scf + 1; }
    // 位运算(画墙 / 拆墙全靠它)

    // Bit operations (all the wall handling depends on them)

    if ((12 & 10) == 8 && ((12 & N_BIT) == 0) == 1) { sci = sci + 1; } else { scf = scf + 1; }
    if ((15 & (0 - 2)) == 14 && (14 & E_BIT) == 2) { sci = sci + 1; } else { scf = scf + 1; }

    // 两个数分开打:`sci*10+标志` 那种编码在标志为 0 时和"上一档 +1"是同一个数(踩过)。

    // Print the two numbers separately: the `sci*10+flag` encoding collides with "one tier up" when the flag is 0 (this was hit for real).

    console.log("selfcheck checks=", sci, "fails=", scf);
    GSCFAIL = scf;
}

// 入口。

// Entry point.

function main() {
    selfcheck();

    GLANG = ui_scr_get_language();
    // ⚠ 写在文件级会变成 0 —— 见文件头约束 ①

    // Warning: a file-level assignment would become 0 -- see constraint (1) in the header

    GCOLS = 11;
    GROWS = 15;

    // 难度档位表:split → map(parseFloat)。

    // Difficulty tier table: split -> map(parseFloat).

    // ⚠ `parseFloat` 返回的是**单精度浮点的位模式**(本前端没有浮点算术),

    // Note: `parseFloat` returns the **bit pattern of a single-precision float** (there is no float arithmetic here),

    //   但**正浮点的位模式按整数比大小与数值同序** ⇒ 排序 / 比较是可靠的,`MULT[k] < MULT[k+1]` 就是真的递增。

    //   but **for positive floats the bit pattern compares as an integer in numeric order** => sorting and comparing are reliable, and `MULT[k] < MULT[k+1]` really does mean increasing.

    //   游戏只用它**定档数**(`MULT.length`)与**校验表是递增的**(不递增就红),档位索引本身是整数。

    //   The game only uses it for the **tier count** (`MULT.length`) and to **check the table really increases** (otherwise it goes red); the tier index itself is an integer.

    var mnsp = "1.0,1.15,1.3,1.5";
    var mnparts = mnsp.split(",");
    MULT = mnparts.map(function (mns) { return parseFloat(mns); });
    var mni = 1;
    while (mni < MULT.length) {
        if (MULT[mni] <= MULT[mni - 1]) { GSCFAIL = GSCFAIL + 1; }
        mni = mni + 1;
    }
    // 起始档位(1 = 第二档,多拆 3 面墙)

    // Starting tier (1 = the second one, three extra walls knocked out)

    GTIER = 1;

    MAZE = bigArr();
    // 到出口的距离(提示 / 最优步数)

    // Distance to the exit (for hints and the optimal step count)

    DIST = bigArr();
    // 到"点中的那一格"的距离(点哪走哪)

    // Distance to the tapped cell (for tap-to-walk)

    DIST2 = bigArr();
    QUE = bigArr();
    PATH = bigArr();
    DIRS = bigArr();
    DOTSX = bigArr();
    DOTSY = bigArr();
    LED = z8();
    CAND = z4();

    // 开窗时传 0:尺寸交给宿主(用真实可用区)。**开窗之后**再问一次真实画布 —— 开窗前问到的只是估算值。

    // Open with 0: the host decides the size (the real usable area). Ask the **real** canvas only after the window is open -- before that the host can only estimate.

    ui_win_open(GLANG == 0 ? "迷宫寻路" : "Maze Runner", 0, 0);
    ui_scr_set_keep_on(1);

    GW = ui_scr_get_w();
    if (GW <= 0) { GW = 360; }
    GH = ui_scr_get_h();
    if (GH <= 0) { GH = 620; }

    // 格子边长:宽高两个上界取小者(只按宽算的话竖屏底下会切出去)。

    // Cell size: the smaller of the two upper bounds (using width alone would cut the bottom off in portrait).

    GCELL = (GW - 16) / GCOLS;
    var mnh = (GH - 96) / GROWS;
    if (mnh < GCELL) { GCELL = mnh; }
    GX0 = (GW - GCOLS * GCELL) / 2;
    GY0 = 88;

    newRound();

    drawAll();

    while (ui_win_is_closed() == 0) {
        var mnt = ui_msg_fetch_wait(0);
        if (mnt == 10) { break; }
        if (mnt == 1) { keyDown(ui_msg_get_a()); if (GQUIT != 0) { break; } drawAll(); }
        if (mnt == 6) { touchAt(ui_msg_get_a(), ui_msg_get_b()); drawAll(); }
    }

    ui_scr_set_keep_on(0);
    ui_win_close();

    // 收尾:留几个确定性判据(真正的判据看 --trace-draw)。

    // Wrap-up: a few deterministic values (the real evidence is --trace-draw).

    // 最终路线用 map 把格子下标变成屏幕坐标(结算画面之外的第二种用法)。

    // The final route maps cell indices to screen coordinates with `map` (a second use beyond the score card).

    DOTSX[0 - 1] = 0;
    DOTSY[0 - 1] = 0;
    var mnm = PATH.map(function (mnc) { return GX0 + (mnc % GCOLS) * GCELL + GCELL / 2; });
    DOTSX = mnm;
    DOTSY = PATH.map(function (mnc2) { return GY0 + (mnc2 / GCOLS) * GCELL + GCELL / 2; });
    console.log("maze scfail=", GSCFAIL, " steps=", GSTEPS, " best=", GBEST, " tier=", GTIER, " win=", GWIN, " turns=", GTURNS, " revisit=", countRevisit());
    console.log("maze cells=", GCOLS * GROWS, " pathlen=", PATH.length, " dots=", DOTSX.length, " led=", LED.length);
    print("maze route=");
    print(DIRS.join(","));
    println("");
}