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("");
}