v0.01
This commit is contained in:
commit
234e769f4d
34 files changed
+4667
No files matched your search
@@ -0,0 +1,40 @@
|
||||
// 机器人试玩:真实物理下向右推进,看存档点是否会被自然触发
|
||||
const handlers = {};
|
||||
const addL = (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); };
|
||||
globalThis.addEventListener = addL;
|
||||
globalThis.window = { addEventListener: addL };
|
||||
const key = (t, c) => (handlers[t] || []).forEach(fn => fn({ code: c, preventDefault() { }, repeat: false }));
|
||||
const down = c => key('keydown', c);
|
||||
const up = c => key('keyup', c);
|
||||
|
||||
const { LEVELS } = await import('../js/levels/index.js');
|
||||
const { Level } = await import('../js/level.js');
|
||||
const { Player } = await import('../js/player.js');
|
||||
const { Input } = await import('../js/input.js');
|
||||
|
||||
const id = process.argv[2] || 'j1';
|
||||
const d = LEVELS.find(l => l.id === id);
|
||||
const L = new Level(d);
|
||||
const P = new Player(L);
|
||||
let deaths = 0;
|
||||
P.onDeath = () => deaths++;
|
||||
|
||||
down('ArrowRight');
|
||||
let jumpPhase = 0;
|
||||
for (let i = 0; i < 3000; i++) {
|
||||
// 简易 AI:遇到前方障碍/刺就跳,滞空一会儿再补二段跳
|
||||
const ahead = L.solidRect(P.x + 3, P.y, P.w, P.h) || L.deadlyRect(P.x + 20, P.y + P.h - 6, 12, 8);
|
||||
const gap = !L.solidRect(P.x + 6, P.y + P.h + 2, P.w, 4);
|
||||
if (P.grounded && (ahead || gap)) { down('ShiftLeft'); jumpPhase = i; }
|
||||
if (!P.grounded && i - jumpPhase === 12) { up('ShiftLeft'); down('ShiftLeft'); }
|
||||
if (i - jumpPhase === 30) up('ShiftLeft');
|
||||
L.update(1, P, null);
|
||||
for (const b of L.bosses) b.update(1, P);
|
||||
P.update(1);
|
||||
Input.endFrame();
|
||||
if (P.savePoint) break;
|
||||
}
|
||||
const saves = L.entities.filter(e => e.type === 'save');
|
||||
console.log(`${id}: 存档点 ${saves.filter(s => s.active).length}/${saves.length} 被激活, x=${P.x.toFixed(0)}, 死亡=${deaths}`);
|
||||
saves.forEach(s => console.log(` (${s.tx},${s.ty}) active=${s.active}`));
|
||||
console.log(` savePoint=${P.savePoint ? `(${P.savePoint.x.toFixed(0)},${P.savePoint.y.toFixed(0)})` : 'null'} spawn=(${L.spawn.x},${L.spawn.y})`);
|
||||
@@ -0,0 +1,55 @@
|
||||
// 关卡数据格式校验
|
||||
import tutorial from '../js/levels/tutorial.js';
|
||||
import jump from '../js/levels/jump.js';
|
||||
import endurance from '../js/levels/endurance.js';
|
||||
import boss from '../js/levels/boss.js';
|
||||
import troll from '../js/levels/troll.js';
|
||||
|
||||
const KNOWN = new Set([...'#@=M%~-*V^v<>SGFXPJWTOCBD! .']);
|
||||
const ALL = [...tutorial, ...jump, ...endurance, ...boss, ...troll];
|
||||
let bad = 0;
|
||||
|
||||
console.log('=== 关卡格式 ===');
|
||||
for (const l of ALL) {
|
||||
const w = l.width;
|
||||
const h = l.grid.length;
|
||||
const tag = `${l.id} ${l.name}`;
|
||||
if (h < 19) { console.log(`! ${tag}: 高度 ${h} 行(建议 >=19,画布高 19 格)`); bad++; }
|
||||
if (h > 60) { console.log(`! ${tag}: 高度 ${h} 行过大`); bad++; }
|
||||
l.grid.forEach((row, y) => {
|
||||
if (row.length !== w) { console.log(`! ${tag} row${y}: len=${row.length} 应为 ${w}`); bad++; }
|
||||
for (const ch of row) if (!KNOWN.has(ch)) { console.log(`! ${tag} row${y}: 未知字符 '${ch}'`); bad++; }
|
||||
});
|
||||
const all = l.grid.join('');
|
||||
const cnt = c => all.split(c).length - 1;
|
||||
const s = cnt('S'), x = cnt('X'), b = cnt('B');
|
||||
if (s !== 1) { console.log(`! ${tag}: S=${s}(必须且只能有一个出生点)`); bad++; }
|
||||
if (l.type === 'boss') { if (b < 1) { console.log(`! ${tag}: BOSS关缺 B`); bad++; } }
|
||||
else if (x < 1) { console.log(`! ${tag}: X=${x}(缺终点)`); bad++; }
|
||||
|
||||
if (l.turrets) {
|
||||
const o = cnt('O');
|
||||
if (o !== l.turrets.length) { console.log(`! ${tag}: O=${o} 但 turrets=${l.turrets.length}`); bad++; }
|
||||
}
|
||||
if (l.camzones) {
|
||||
const c = cnt('C');
|
||||
if (c > l.camzones.length) { console.log(`! ${tag}: C=${c} camzones=${l.camzones.length}`); bad++; }
|
||||
}
|
||||
if (l.warps) {
|
||||
const ww = cnt('W');
|
||||
if (ww !== l.warps.length) { console.log(`! ${tag}: W=${ww} 但 warps=${l.warps.length}`); bad++; }
|
||||
}
|
||||
if (l.bosses) {
|
||||
if (b !== l.bosses.length) { console.log(`! ${tag}: B=${b} 但 bosses=${l.bosses.length}`); bad++; }
|
||||
}
|
||||
if (l.messages) {
|
||||
const t = cnt('T');
|
||||
if (t > l.messages.length) { console.log(`! ${tag}: T=${t} 但 messages=${l.messages.length}`); bad++; }
|
||||
}
|
||||
const g = cnt('G'), f = cnt('F');
|
||||
console.log(` ${tag.padEnd(24)} ${String(w).padStart(3)}x${String(h).padStart(2)} S=${s} X=${x} G=${g} F=${f} B=${b} ${l.type}`);
|
||||
}
|
||||
|
||||
console.log(`\n共 ${ALL.length} 关`);
|
||||
console.log(bad ? `❌ 发现 ${bad} 个问题` : '✅ 全部关卡格式 OK');
|
||||
process.exit(bad ? 1 : 0);
|
||||
@@ -0,0 +1,89 @@
|
||||
// DOM 环境冒烟:mock 浏览器 API,跑通入口 main.js 的初始化 + 若干帧
|
||||
const noop = () => { };
|
||||
const els = new Map();
|
||||
|
||||
function mkEl(id = '') {
|
||||
const el = {
|
||||
id, tagName: 'DIV', value: '', textContent: '', innerHTML: '',
|
||||
style: {}, dataset: {}, children: [], width: 800, height: 608,
|
||||
classList: { add: noop, remove: noop, toggle: noop, contains: () => false },
|
||||
addEventListener: noop, removeEventListener: noop, appendChild: noop,
|
||||
querySelector: () => mkEl(), querySelectorAll: () => [],
|
||||
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 608 }),
|
||||
getContext: () => ctx2d,
|
||||
focus: noop, click: noop,
|
||||
};
|
||||
return el;
|
||||
}
|
||||
const ctx2d = new Proxy({
|
||||
canvas: { width: 800, height: 608 },
|
||||
measureText: () => ({ width: 12 }),
|
||||
createLinearGradient: () => ({ addColorStop: noop }),
|
||||
createRadialGradient: () => ({ addColorStop: noop }),
|
||||
getImageData: () => ({ data: [] }),
|
||||
}, {
|
||||
get(t, k) { return k in t ? t[k] : noop; },
|
||||
set(t, k, v) { t[k] = v; return true; },
|
||||
});
|
||||
|
||||
const handlers = {};
|
||||
const addL = (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); };
|
||||
|
||||
globalThis.window = {
|
||||
addEventListener: addL, removeEventListener: noop,
|
||||
AudioContext: null, webkitAudioContext: null,
|
||||
innerWidth: 1280, innerHeight: 800,
|
||||
};
|
||||
globalThis.addEventListener = addL;
|
||||
globalThis.removeEventListener = noop;
|
||||
globalThis.confirm = () => true;
|
||||
globalThis.alert = noop;
|
||||
globalThis.localStorage = {
|
||||
_d: {},
|
||||
getItem(k) { return this._d[k] ?? null; },
|
||||
setItem(k, v) { this._d[k] = String(v); },
|
||||
removeItem(k) { delete this._d[k]; },
|
||||
};
|
||||
globalThis.document = {
|
||||
getElementById(id) { if (!els.has(id)) els.set(id, mkEl(id)); return els.get(id); },
|
||||
querySelector(s) { return mkEl(s); },
|
||||
querySelectorAll() { return []; },
|
||||
createElement(t) { const e = mkEl(); e.tagName = t.toUpperCase(); return e; },
|
||||
addEventListener: addL,
|
||||
body: mkEl('body'),
|
||||
};
|
||||
let frames = 0;
|
||||
globalThis.requestAnimationFrame = (fn) => {
|
||||
if (frames++ > 12) return 0;
|
||||
setTimeout(() => fn(performance.now() + frames * 16.7), 0);
|
||||
return frames;
|
||||
};
|
||||
|
||||
try {
|
||||
await import('../js/main.js');
|
||||
console.log('✅ main.js 初始化成功');
|
||||
} catch (e) {
|
||||
console.log('❌ main.js 初始化失败: ' + e.message);
|
||||
console.log(e.stack.split('\n').slice(0, 5).join('\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 等几帧
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
const { LEVELS } = await import('../js/levels/index.js');
|
||||
const api = globalThis.window.__IW;
|
||||
console.log('✅ 已跑 ' + frames + ' 帧,状态=' + (api ? api.G.state : '?'));
|
||||
if (api) {
|
||||
// 逐个关卡启动一次,确认不炸
|
||||
let bad = 0;
|
||||
for (const lv of LEVELS) {
|
||||
try {
|
||||
api.startLevel(lv, lv.index ?? -1);
|
||||
for (let i = 0; i < 90; i++) { /* 由 rAF 驱动,这里只验证构造 */ }
|
||||
if (!api.G.level || !api.G.player) throw new Error('未创建关卡');
|
||||
} catch (e) { console.log(' ✗ ' + lv.id + ': ' + e.message); bad++; }
|
||||
}
|
||||
console.log(bad ? `❌ ${bad} 个关卡启动失败` : `✅ 全部 ${LEVELS.length} 关均可启动`);
|
||||
}
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,54 @@
|
||||
// 用真实引擎(Player/Level)测量跳跃水平距离,核对求解器的物理近似是否可信
|
||||
const handlers = {};
|
||||
const addL = (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); };
|
||||
globalThis.addEventListener = addL;
|
||||
globalThis.window = { addEventListener: addL };
|
||||
const key = (t, c) => (handlers[t] || []).forEach(fn => fn({ code: c, preventDefault() { }, repeat: false }));
|
||||
|
||||
const { Level } = await import('../js/level.js');
|
||||
const { Player } = await import('../js/player.js');
|
||||
const { Input } = await import('../js/input.js');
|
||||
|
||||
// 一块 40 格长的平地,右端断开
|
||||
const flat = {
|
||||
id: 'test', name: 'T', type: 'jump', width: 44,
|
||||
grid: (() => {
|
||||
const rows = [];
|
||||
rows.push('#'.repeat(44));
|
||||
for (let i = 0; i < 16; i++) rows.push('#' + '.'.repeat(42) + '#');
|
||||
rows.push('#' + '#'.repeat(42) + '#'); // 整条实心平台
|
||||
rows.push('#'.repeat(44));
|
||||
return rows;
|
||||
})(),
|
||||
};
|
||||
|
||||
function measure(djFrame) {
|
||||
const L = new Level(flat);
|
||||
const P = new Player(L);
|
||||
P.x = 8 * 32; P.y = 16 * 32 - P.h; // 站在 row17 平台上(平台行 17)
|
||||
P.vx = 0; P.vy = 0; P.grounded = true; P.jumps = 0;
|
||||
const x0 = P.x;
|
||||
key('keydown', 'ArrowRight');
|
||||
key('keydown', 'ShiftLeft');
|
||||
let f = 0, landed = -1, released = false;
|
||||
for (let i = 0; i < 200; i++) {
|
||||
f++;
|
||||
// 二段跳需要先松开再按下(跨帧),否则不算一次新的按下
|
||||
if (djFrame && f === djFrame) { key('keyup', 'ShiftLeft'); released = true; }
|
||||
if (released && f === djFrame + 1) { key('keydown', 'ShiftLeft'); released = false; }
|
||||
L.update(1, P, null);
|
||||
P.update(1);
|
||||
Input.endFrame();
|
||||
if (i > 2 && P.grounded) { landed = P.x; break; }
|
||||
if (P.dead) break;
|
||||
}
|
||||
key('keyup', 'ArrowRight'); key('keyup', 'ShiftLeft');
|
||||
Input.endFrame();
|
||||
return { dist: landed >= 0 ? landed - x0 : null, f };
|
||||
}
|
||||
|
||||
console.log('真实引擎跳跃水平距离(起跳点 x 到落点 x,单位 px / 格)');
|
||||
for (const dj of [0, 6, 12, 18, 24, 30]) {
|
||||
const r = measure(dj);
|
||||
console.log(` 二段跳时机=${String(dj).padStart(2)} 帧 -> ${r.dist === null ? '未落地' : r.dist.toFixed(1) + 'px = ' + (r.dist / 32).toFixed(2) + ' 格 (' + r.f + ' 帧)'}`);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// 回归测试:R 键必须"自杀回存档点",不能把整关重开(否则存档点白碰)
|
||||
const noop = () => { };
|
||||
const els = new Map();
|
||||
function mkEl(id = '') {
|
||||
return {
|
||||
id, tagName: 'DIV', value: '', textContent: '', innerHTML: '',
|
||||
style: {}, dataset: {}, children: [], width: 800, height: 608,
|
||||
classList: { add: noop, remove: noop, toggle: noop, contains: () => false },
|
||||
addEventListener: noop, removeEventListener: noop, appendChild: noop,
|
||||
querySelector: () => mkEl(), querySelectorAll: () => [],
|
||||
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 608 }),
|
||||
getContext: () => ctx2d, focus: noop, click: noop,
|
||||
};
|
||||
}
|
||||
const ctx2d = new Proxy({
|
||||
canvas: { width: 800, height: 608 },
|
||||
measureText: () => ({ width: 12 }),
|
||||
createLinearGradient: () => ({ addColorStop: noop }),
|
||||
createRadialGradient: () => ({ addColorStop: noop }),
|
||||
getImageData: () => ({ data: [] }),
|
||||
}, { get(t, k) { return k in t ? t[k] : noop; }, set(t, k, v) { t[k] = v; return true; } });
|
||||
|
||||
const handlers = {};
|
||||
const addL = (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); };
|
||||
const key = (t, c) => (handlers[t] || []).forEach(fn => fn({ code: c, preventDefault() { }, repeat: false }));
|
||||
|
||||
let clock = 1000;
|
||||
globalThis.performance = { now: () => clock };
|
||||
let rafCb = null;
|
||||
globalThis.requestAnimationFrame = fn => { rafCb = fn; return 1; };
|
||||
|
||||
globalThis.window = {
|
||||
addEventListener: addL, removeEventListener: noop,
|
||||
AudioContext: null, webkitAudioContext: null, innerWidth: 1280, innerHeight: 800,
|
||||
};
|
||||
globalThis.addEventListener = addL;
|
||||
globalThis.removeEventListener = noop;
|
||||
globalThis.confirm = () => true;
|
||||
globalThis.alert = noop;
|
||||
globalThis.localStorage = {
|
||||
_d: {}, getItem(k) { return this._d[k] ?? null; },
|
||||
setItem(k, v) { this._d[k] = String(v); }, removeItem(k) { delete this._d[k]; },
|
||||
};
|
||||
globalThis.document = {
|
||||
getElementById(id) { if (!els.has(id)) els.set(id, mkEl(id)); return els.get(id); },
|
||||
querySelector: s => mkEl(s), querySelectorAll: () => [],
|
||||
createElement: t => mkEl(), addEventListener: addL, body: mkEl('body'),
|
||||
};
|
||||
|
||||
await import('../js/main.js');
|
||||
const { LEVELS } = await import('../js/levels/index.js');
|
||||
const api = globalThis.window.__IW;
|
||||
|
||||
let fail = 0;
|
||||
const err = m => { console.log(' ✗ ' + m); fail++; };
|
||||
const step = n => { for (let i = 0; i < n; i++) { clock += 17; const f = rafCb; rafCb = null; if (f) f(clock); } };
|
||||
|
||||
const d = LEVELS.find(l => l.id === 'j1');
|
||||
api.startLevel(d, 0);
|
||||
step(10);
|
||||
|
||||
const L0 = api.G.level, P = api.G.player;
|
||||
// 手动吃一个存档点(模拟玩家走到那里)
|
||||
const s = L0.entities.filter(e => e.type === 'save')[1];
|
||||
P.x = s.x + s.w / 2 - P.w / 2; P.y = s.y + s.h - P.h;
|
||||
step(3);
|
||||
const cp = P.savePoint;
|
||||
console.log(` 存档点激活=${!!cp} 位置=(${cp?.x.toFixed(0)},${cp?.y.toFixed(0)}) 关卡出生点x=${L0.spawn.x.toFixed(0)}`);
|
||||
if (!cp) err('存档点未激活');
|
||||
|
||||
// ---- 按 R ----
|
||||
const deaths0 = api.G.deaths;
|
||||
key('keydown', 'KeyR');
|
||||
step(2);
|
||||
key('keyup', 'KeyR');
|
||||
step(60);
|
||||
|
||||
const survivedLevel = api.G.level === L0;
|
||||
console.log(` 按 R 后:关卡对象未被重建=${survivedLevel} 玩家x=${api.G.player.x.toFixed(0)} 死亡=${deaths0}→${api.G.deaths}`);
|
||||
if (!survivedLevel) err('R 键把整关重开了,存档进度丢失');
|
||||
if (Math.abs(api.G.player.x - cp.x) > 4) err(`R 键后没有回到存档点(x=${api.G.player.x.toFixed(0)} 期望 ${cp.x.toFixed(0)})`);
|
||||
if (api.G.deaths !== deaths0 + 1) err('R 键没有计入死亡数');
|
||||
if (Math.abs(api.G.player.x - L0.spawn.x) < 4) err('R 键后被送回了关卡起点');
|
||||
|
||||
// ---- 按 Backspace = 整关重来 ----
|
||||
key('keydown', 'Backspace');
|
||||
step(2);
|
||||
key('keyup', 'Backspace');
|
||||
step(10);
|
||||
console.log(` 按 Backspace 后:关卡对象被重建=${api.G.level !== L0} 玩家x=${api.G.player.x.toFixed(0)}`);
|
||||
if (api.G.level === L0) err('Backspace 没有整关重开');
|
||||
if (Math.abs(api.G.player.x - api.G.level.spawn.x) > 4) err('Backspace 后玩家不在出生点');
|
||||
|
||||
console.log(fail ? `\n❌ ${fail} 项失败` : '\n✅ 按键语义正确:R=回存档点,Backspace=整关重来');
|
||||
process.exit(fail ? 1 : 0);
|
||||
@@ -0,0 +1,110 @@
|
||||
// 诊断:打印可达站立点的最大 x(按行),找出断连位置
|
||||
const handlers = {};
|
||||
const addL = (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); };
|
||||
globalThis.addEventListener = addL;
|
||||
globalThis.window = { addEventListener: addL };
|
||||
|
||||
const { LEVELS } = await import('../js/levels/index.js');
|
||||
const { Level } = await import('../js/level.js');
|
||||
|
||||
const TILE = 32, PW = 14, PH = 18;
|
||||
const GRAV = 0.4, MAXFALL = 9, JUMP = 8.5, DJUMP = 7.6, HSPD = 3;
|
||||
|
||||
function solidAt(L, br, x, y, w, h, down, prevBottom) {
|
||||
const x0 = Math.floor(x / TILE), x1 = Math.floor((x + w - 0.001) / TILE);
|
||||
const y0 = Math.floor(y / TILE), y1 = Math.floor((y + h - 0.001) / TILE);
|
||||
for (let ty = y0; ty <= y1; ty++) for (let tx = x0; tx <= x1; tx++) {
|
||||
const t = L.tileAt(tx, ty);
|
||||
if (t === 7) { if (down && prevBottom !== undefined && prevBottom <= ty * TILE + 6) return true; continue; }
|
||||
if (L.isSolidTile(t, tx, ty)) return true;
|
||||
}
|
||||
for (const b of br) if (x < b.x + b.w && x + w > b.x && y < b.y + b.h && y + h > b.y) return true;
|
||||
return false;
|
||||
}
|
||||
function canStand(L, br, fx, fy) {
|
||||
if (fx < 0 || fx + PW > L.pxW) return false;
|
||||
if (solidAt(L, br, fx, fy - PH, PW, PH, false)) return false;
|
||||
if (!solidAt(L, br, fx, fy - PH + 1, PW, PH, true, fy - 1)) return false;
|
||||
if (L.deadlyRect(fx, fy - PH, PW, PH)) return false;
|
||||
return true;
|
||||
}
|
||||
function jump(L, br, fx, fy, vx0, djump, flip) {
|
||||
let x = fx, y = fy - PH, vy = -JUMP, vx = vx0, t = 0, used = false;
|
||||
while (t++ < 240) {
|
||||
if (djump && !used && t === djump) { vy = -DJUMP; used = true; }
|
||||
if (flip && t === flip) vx = -vx;
|
||||
vy = Math.min(vy + GRAV, MAXFALL);
|
||||
if (vx !== 0) { const nx = x + vx; if (!solidAt(L, br, nx, y, PW, PH, false)) x = nx; else vx = 0; }
|
||||
const prevBottom = y + PH;
|
||||
let rem = Math.abs(vy), dy = Math.sign(vy), landed = false;
|
||||
while (rem > 0.0001) {
|
||||
const m = Math.min(1, rem); const ny = y + dy * m;
|
||||
if (!solidAt(L, br, x, ny, PW, PH, dy > 0, prevBottom)) { y = ny; rem -= m; }
|
||||
else { if (dy > 0) landed = true; vy = 0; break; }
|
||||
}
|
||||
if (L.deadlyRect(x, y, PW, PH)) return null;
|
||||
if (y > L.pxH + 120 || y < -240) return null;
|
||||
if (landed) return { fx: x, fy: y + PH };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function fall(L, br, fx, fy, dir) {
|
||||
let x = fx + dir * 3, y = fy - PH, vy = 0, t = 0;
|
||||
if (solidAt(L, br, x, y, PW, PH, false)) return null;
|
||||
while (t++ < 400) {
|
||||
vy = Math.min(vy + GRAV, MAXFALL);
|
||||
let rem = Math.abs(vy), landed = false;
|
||||
while (rem > 0.0001) {
|
||||
const m = Math.min(1, rem); const ny = y + m;
|
||||
if (!solidAt(L, br, x, ny, PW, PH, true, y + PH)) { y = ny; rem -= m; }
|
||||
else { landed = true; break; }
|
||||
}
|
||||
if (L.deadlyRect(x, y, PW, PH)) return null;
|
||||
if (y > L.pxH + 120) return null;
|
||||
if (landed) return { fx: x, fy: y + PH };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = process.argv[2] || 'j6';
|
||||
const d = LEVELS.find(l => l.id === id);
|
||||
const L = new Level(d);
|
||||
const br = [];
|
||||
for (let y = 0; y < L.h; y++) for (let x = 0; x < L.w; x++) if (L.tiles[y][x] === 6) L.tiles[y][x] = 1;
|
||||
for (const m of L.movers) {
|
||||
const xs = m.path.map(p => p.x).concat([m.x]), ys = m.path.map(p => p.y).concat([m.y]);
|
||||
const x0 = Math.min(...xs), x1 = Math.max(...xs) + m.w, y0 = Math.min(...ys), y1 = Math.max(...ys) + m.h;
|
||||
br.push({ x: x0, y: y0, w: x1 - x0, h: y1 - y0 });
|
||||
}
|
||||
let sx = L.spawn.x, sy = L.spawn.y + PH, start = null;
|
||||
for (let k = 0; k < 400; k++) { if (canStand(L, br, sx, sy + k)) { start = { fx: sx, fy: sy + k }; break; } if (sy + k > L.pxH) break; }
|
||||
const key = s => `${Math.round(s.fx / 6)},${Math.round(s.fy / 6)}`;
|
||||
const seen = new Set([key(start)]); const q = [start]; let n = 0;
|
||||
const byRow = new Map();
|
||||
const note = s => {
|
||||
const r = Math.round(s.fy / TILE);
|
||||
const cur = byRow.get(r) || { max: -1, min: 1e9 };
|
||||
byRow.set(r, { max: Math.max(cur.max, s.fx), min: Math.min(cur.min, s.fx) });
|
||||
};
|
||||
note(start);
|
||||
while (q.length && n < 40000) {
|
||||
const s = q.shift(); n++;
|
||||
for (const d2 of [-HSPD, HSPD]) {
|
||||
const nx = s.fx + d2;
|
||||
if (canStand(L, br, nx, s.fy)) { const ns = { fx: nx, fy: s.fy }; const k = key(ns); if (!seen.has(k)) { seen.add(k); q.push(ns); note(ns); } }
|
||||
}
|
||||
for (const d2 of [-1, 1]) { const r = fall(L, br, s.fx, s.fy, d2); if (r && canStand(L, br, r.fx, r.fy)) { const k = key(r); if (!seen.has(k)) { seen.add(k); q.push(r); note(r); } } }
|
||||
for (const vx of [-HSPD, 0, HSPD]) for (const dj of [0, 6, 12, 18, 24, 30]) {
|
||||
const r = jump(L, br, s.fx, s.fy, vx, dj, 0);
|
||||
if (r && canStand(L, br, r.fx, r.fy)) { const k = key(r); if (!seen.has(k)) { seen.add(k); q.push(r); note(r); } }
|
||||
if (vx !== 0) { const r2 = jump(L, br, s.fx, s.fy, vx, dj, 22); if (r2 && canStand(L, br, r2.fx, r2.fy)) { const k = key(r2); if (!seen.has(k)) { seen.add(k); q.push(r2); note(r2); } } }
|
||||
}
|
||||
}
|
||||
console.log(`${id} ${d.name} ${L.w}x${L.h} 可达站立行数=${byRow.size} 状态=${seen.size}`);
|
||||
[...byRow.keys()].sort((a, b) => b - a).forEach(r => {
|
||||
const v = byRow.get(r);
|
||||
console.log(` row ${String(r).padStart(2)} (y=${r * TILE}): x ${(v.min / TILE).toFixed(1)} .. ${(v.max / TILE).toFixed(1)} 格`);
|
||||
});
|
||||
// 打印目标
|
||||
for (const e of L.entities) if (e.type === 'goal' || e.type === 'save')
|
||||
console.log(` ${e.type}#${e.no ?? ''} at col ${e.tx}, row ${e.ty}`);
|
||||
@@ -0,0 +1,31 @@
|
||||
// 诊断:每个存档点的放置环境(脚下是什么 / 重生点是否致命 / 是否在路径上)
|
||||
const handlers = {};
|
||||
const addL = (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); };
|
||||
globalThis.addEventListener = addL;
|
||||
globalThis.window = { addEventListener: addL };
|
||||
|
||||
const { LEVELS } = await import('../js/levels/index.js');
|
||||
const { Level } = await import('../js/level.js');
|
||||
const { Player } = await import('../js/player.js');
|
||||
const { Input } = await import('../js/input.js');
|
||||
const { T } = await import('../js/level.js');
|
||||
|
||||
const NAME = { 0: '空', 1: '块', 2: '草', 3: '砖', 4: '金属', 5: '冰', 6: '水', 7: '单向', 8: '即死', 9: '消失', 10: '上刺', 11: '下刺', 12: '左刺', 13: '右刺' };
|
||||
|
||||
for (const d of LEVELS) {
|
||||
const L = new Level(d);
|
||||
const P = new Player(L);
|
||||
const saves = L.entities.filter(e => e.type === 'save');
|
||||
if (!saves.length) { console.log(`${d.id.padEnd(3)} ${d.name.padEnd(16)} ⚠ 无存档点`); continue; }
|
||||
const lines = [];
|
||||
for (const s of saves) {
|
||||
const below = L.tileAt(s.tx, s.ty + 1);
|
||||
const above = L.tileAt(s.tx, s.ty - 1);
|
||||
// 重生点
|
||||
const rx = s.x + s.w / 2 - P.w / 2, ry = s.y + s.h - P.h;
|
||||
const deadly = L.deadlyRect(rx, ry, P.w, P.h);
|
||||
const solid = L.solidRect(rx, ry, P.w, P.h);
|
||||
lines.push(`(${s.tx},${s.ty}) 脚=${NAME[below] || below} 头=${NAME[above] || above}${deadly ? ' ❌重生即死' : ''}${solid ? ' ❌卡墙里' : ''}`);
|
||||
}
|
||||
console.log(`${d.id.padEnd(3)} ${d.name.padEnd(16)} ${saves.length} 个: ${lines.join(' | ')}`);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// 存档点专项测试:触碰 -> 激活 -> 死亡 -> 回到存档点
|
||||
const handlers = {};
|
||||
const addL = (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); };
|
||||
globalThis.addEventListener = addL;
|
||||
globalThis.window = { addEventListener: addL };
|
||||
const key = (type, code) => (handlers[type] || []).forEach(fn => fn({ code, preventDefault() { }, repeat: false }));
|
||||
const down = c => key('keydown', c);
|
||||
const up = c => key('keyup', c);
|
||||
|
||||
const { LEVELS } = await import('../js/levels/index.js');
|
||||
const { Level } = await import('../js/level.js');
|
||||
const { Player } = await import('../js/player.js');
|
||||
const { Input } = await import('../js/input.js');
|
||||
|
||||
let fail = 0;
|
||||
const err = m => { console.log(' ✗ ' + m); fail++; };
|
||||
|
||||
for (const d of LEVELS) {
|
||||
const L = new Level(d);
|
||||
const P = new Player(L);
|
||||
const saves = L.entities.filter(e => e.type === 'save');
|
||||
if (!saves.length) { console.log(` ${d.id} 无存档点`); continue; }
|
||||
|
||||
// 依次把玩家瞬移到每个存档点,检查是否激活
|
||||
for (const s of saves) {
|
||||
P.x = s.x + s.w / 2 - P.w / 2;
|
||||
P.y = s.y + s.h - P.h;
|
||||
P.vx = 0; P.vy = 0; P.dead = false; P.alive = true;
|
||||
L.update(1, P, null);
|
||||
P.update(1);
|
||||
Input.endFrame();
|
||||
if (!s.active) err(`${d.id} 存档点(${s.tx},${s.ty}) 触碰后未激活`);
|
||||
if (!P.savePoint) err(`${d.id} 存档点(${s.tx},${s.ty}) 未写入 player.savePoint`);
|
||||
}
|
||||
|
||||
// 死亡后应回到最后一个存档点
|
||||
const last = saves[saves.length - 1];
|
||||
const want = { x: last.x + last.w / 2 - P.w / 2, y: last.y + last.h - P.h };
|
||||
const spawn = L.spawn;
|
||||
P.kill();
|
||||
for (let i = 0; i < 45; i++) { L.update(1, P, null); P.update(1); Input.endFrame(); }
|
||||
const dx = Math.abs(P.x - want.x), dy = Math.abs(P.y - want.y);
|
||||
const backToStart = Math.abs(P.x - spawn.x) < 3 && Math.abs(P.y - spawn.y) < 3;
|
||||
const ok = dx < 2 && dy < 2;
|
||||
console.log(` ${d.id.padEnd(3)} 存档=${saves.length} 重生=(${P.x.toFixed(0)},${P.y.toFixed(0)}) 期望=(${want.x.toFixed(0)},${want.y.toFixed(0)}) ${ok ? 'OK' : (backToStart ? '❌回到了起点' : '❌位置不符')}`);
|
||||
if (!ok) err(`${d.id} 死亡后未回到存档点`);
|
||||
|
||||
// 重生后是否立刻死亡
|
||||
const alive0 = P.alive;
|
||||
for (let i = 0; i < 20; i++) { L.update(1, P, null); P.update(1); Input.endFrame(); }
|
||||
if (alive0 && !P.alive) err(`${d.id} 在存档点重生后立刻死亡`);
|
||||
|
||||
// 存档点状态是否在软重置后保留
|
||||
if (!last.active) err(`${d.id} 软重置后存档点激活状态丢失`);
|
||||
}
|
||||
|
||||
console.log(fail ? `\n❌ ${fail} 项失败` : '\n✅ 存档点全部正常');
|
||||
process.exit(fail ? 1 : 0);
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
// 无头冒烟测试:构造全部关卡 + 模拟输入跑物理
|
||||
const handlers = {};
|
||||
const addL = (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); };
|
||||
globalThis.addEventListener = addL;
|
||||
globalThis.window = { addEventListener: addL };
|
||||
|
||||
const key = (type, code) => (handlers[type] || []).forEach(fn => fn({ code, preventDefault() { }, repeat: false }));
|
||||
const down = c => key('keydown', c);
|
||||
const up = c => key('keyup', c);
|
||||
|
||||
const { LEVELS } = await import('../js/levels/index.js');
|
||||
const { Level } = await import('../js/level.js');
|
||||
const { Player } = await import('../js/player.js');
|
||||
const { Input } = await import('../js/input.js');
|
||||
|
||||
let fail = 0;
|
||||
const err = m => { console.log(' ✗ ' + m); fail++; };
|
||||
|
||||
console.log('=== 关卡构造 ===');
|
||||
for (const d of LEVELS) {
|
||||
try {
|
||||
const L = new Level(d);
|
||||
const P = new Player(L);
|
||||
// 静止 60 帧
|
||||
for (let i = 0; i < 60; i++) { L.update(1, P, null); P.update(1); Input.endFrame(); }
|
||||
const grounded = P.grounded, alive = P.alive;
|
||||
console.log(` ${d.id.padEnd(3)} ${String(d.name).padEnd(16)} 实体=${String(L.entities.length).padStart(3)} boss=${L.bosses.length} 平台=${L.movers.length} 站住=${grounded ? 'Y' : 'N'} 存活=${alive ? 'Y' : 'N'}`);
|
||||
if (!grounded && d.type !== 'boss') err(`${d.id} 出生点悬空(60帧后未着地)`);
|
||||
if (!alive) err(`${d.id} 出生即死`);
|
||||
|
||||
// 模拟:向右跑 + 反复跳跃 400 帧
|
||||
down('ArrowRight');
|
||||
for (let i = 0; i < 400; i++) {
|
||||
if (i % 40 === 0) down('ShiftLeft');
|
||||
if (i % 40 === 8) up('ShiftLeft');
|
||||
if (i % 40 === 12) { down('ShiftLeft'); }
|
||||
if (i % 40 === 16) up('ShiftLeft');
|
||||
L.update(1, P, null);
|
||||
for (const b of L.bosses) b.update(1, P);
|
||||
P.update(1);
|
||||
Input.endFrame();
|
||||
}
|
||||
up('ArrowRight'); up('ShiftLeft');
|
||||
const moved = P.x;
|
||||
if (!Number.isFinite(P.x) || !Number.isFinite(P.y)) err(`${d.id} 坐标 NaN`);
|
||||
if (Math.abs(P.vx) > 20 || Math.abs(P.vy) > 40) err(`${d.id} 速度异常 vx=${P.vx} vy=${P.vy}`);
|
||||
console.log(` → 400帧后 x=${moved.toFixed(0)} y=${P.y.toFixed(0)} 死亡=${G_DEATHS(P)}`);
|
||||
} catch (e) {
|
||||
err(`${d.id} 运行异常: ${e.message}\n${e.stack?.split('\n')[1] || ''}`);
|
||||
}
|
||||
}
|
||||
function G_DEATHS() { return ''; }
|
||||
|
||||
console.log('\n=== 死亡与重生 ===');
|
||||
{
|
||||
const L = new Level(LEVELS.find(l => l.id === 'j1'));
|
||||
const P = new Player(L);
|
||||
let deaths = 0;
|
||||
P.onDeath = () => deaths++;
|
||||
for (let i = 0; i < 30; i++) { L.update(1, P, null); P.update(1); Input.endFrame(); }
|
||||
P.kill();
|
||||
for (let i = 0; i < 80; i++) { L.update(1, P, null); P.update(1); Input.endFrame(); }
|
||||
console.log(` 死亡计数=${deaths} 重生后 alive=${P.alive} 位置=(${P.x.toFixed(0)},${P.y.toFixed(0)})`);
|
||||
if (!P.alive) err('死亡后未复活');
|
||||
if (deaths !== 1) err('死亡回调未触发');
|
||||
}
|
||||
|
||||
console.log('\n=== 射击与 BOSS ===');
|
||||
{
|
||||
const L = new Level(LEVELS.find(l => l.id === 'b1'));
|
||||
const P = new Player(L);
|
||||
const boss = L.bosses[0];
|
||||
const hp0 = boss.hp;
|
||||
P.hasGun = true;
|
||||
for (let i = 0; i < 600; i++) {
|
||||
if (i % 12 === 0) P.shootCool = 0, P.shoot();
|
||||
L.update(1, P, null);
|
||||
for (const b of L.bosses) b.update(1, P);
|
||||
P.update(1);
|
||||
Input.endFrame();
|
||||
if (boss.hp <= 0) break;
|
||||
}
|
||||
console.log(` BOSS hp ${hp0} → ${boss.hp} 弹幕数=${L.ebullets.length} 阶段=${boss.phase}`);
|
||||
if (boss.hp >= hp0) err('子弹没有对 BOSS 造成伤害');
|
||||
}
|
||||
|
||||
console.log('\n=== 坑向事件 ===');
|
||||
{
|
||||
const L = new Level(LEVELS.find(l => l.id === 'r4'));
|
||||
const P = new Player(L);
|
||||
let died = 0; P.onDeath = () => died++;
|
||||
// 走到传送门 (r4: col 44, row 17)
|
||||
P.x = 44 * 32; P.y = 17 * 32;
|
||||
for (let i = 0; i < 4; i++) { L.update(1, P, null); P.update(1); Input.endFrame(); }
|
||||
console.log(` 传送门:玩家被送到 x=${P.x.toFixed(0)} (起点 x=${L.spawn.x})`);
|
||||
if (Math.abs(P.x - L.spawn.x) > 4) err('传送门未送回起点');
|
||||
}
|
||||
|
||||
console.log(fail ? `\n❌ ${fail} 项失败` : '\n✅ 全部通过');
|
||||
process.exit(fail ? 1 : 0);
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
// 可达性求解器:用真实物理常量做 BFS,验证终点 / 存档点能否到达
|
||||
const handlers = {};
|
||||
const addL = (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); };
|
||||
globalThis.addEventListener = addL;
|
||||
globalThis.window = { addEventListener: addL };
|
||||
|
||||
const { LEVELS } = await import('../js/levels/index.js');
|
||||
const { Level } = await import('../js/level.js');
|
||||
|
||||
// 可用 DJ=0,12,24 收紧二段跳时机选项,检验关卡是不是只有"某一帧"能过
|
||||
const DJSET = (process.env.DJ || '0,6,12,18,24,30').split(',').map(Number);
|
||||
const TILE = 32, PW = 14, PH = 18;
|
||||
const GRAV = 0.4, MAXFALL = 9, JUMP = 8.5, DJUMP = 7.6, HSPD = 3;
|
||||
|
||||
// ---- 碰撞(单向平台:只有下落且上一帧还在平台面之上才挡)----
|
||||
function solidAt(L, br, x, y, w, h, down, prevBottom) {
|
||||
const x0 = Math.floor(x / TILE), x1 = Math.floor((x + w - 0.001) / TILE);
|
||||
const y0 = Math.floor(y / TILE), y1 = Math.floor((y + h - 0.001) / TILE);
|
||||
for (let ty = y0; ty <= y1; ty++) {
|
||||
for (let tx = x0; tx <= x1; tx++) {
|
||||
const t = L.tileAt(tx, ty);
|
||||
if (t === 7) {
|
||||
if (down && prevBottom !== undefined && prevBottom <= ty * TILE + 6) return true;
|
||||
continue;
|
||||
}
|
||||
if (L.isSolidTile(t, tx, ty)) return true;
|
||||
}
|
||||
}
|
||||
for (const b of br)
|
||||
if (x < b.x + b.w && x + w > b.x && y < b.y + b.h && y + h > b.y) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const overlap = (a, b) => a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
||||
|
||||
/** 站立位置 (fx,fy)=脚底中心左端;返回是否合法 */
|
||||
function canStand(L, br, fx, fy) {
|
||||
if (fx < 0 || fx + PW > L.pxW) return false;
|
||||
if (solidAt(L, br, fx, fy - PH, PW, PH, false)) return false;
|
||||
if (!solidAt(L, br, fx, fy - PH + 1, PW, PH, true, fy - 1)) return false;
|
||||
if (L.deadlyRect(fx, fy - PH, PW, PH)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 模拟一次跳跃:返回 {fx,fy} 落点 或 null;途中记录是否碰到目标 */
|
||||
function jump(L, br, fx, fy, vx0, djump, flip, targets) {
|
||||
let x = fx, y = fy - PH;
|
||||
let vy = -JUMP, vx = vx0, t = 0, used = false;
|
||||
while (t++ < 240) {
|
||||
if (djump && !used && t === djump) { vy = -DJUMP; used = true; }
|
||||
if (flip && t === flip) vx = -vx;
|
||||
vy = Math.min(vy + GRAV, MAXFALL);
|
||||
|
||||
if (vx !== 0) {
|
||||
const nx = x + vx;
|
||||
if (!solidAt(L, br, nx, y, PW, PH, false)) x = nx; else vx = 0;
|
||||
}
|
||||
const prevBottom = y + PH;
|
||||
let rem = Math.abs(vy), dy = Math.sign(vy), landed = false;
|
||||
while (rem > 0.0001) {
|
||||
const m = Math.min(1, rem);
|
||||
const ny = y + dy * m;
|
||||
if (!solidAt(L, br, x, ny, PW, PH, dy > 0, prevBottom)) { y = ny; rem -= m; }
|
||||
else { if (dy > 0) landed = true; vy = 0; break; }
|
||||
}
|
||||
if (L.deadlyRect(x, y, PW, PH)) return null;
|
||||
if (targets) {
|
||||
const box = { x, y, w: PW, h: PH };
|
||||
for (const tg of targets) if (!tg.hit && overlap(box, tg.rect)) tg.hit = true;
|
||||
}
|
||||
if (y > L.pxH + 120 || y < -240) return null;
|
||||
if (landed) return { fx: x, fy: y + PH };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 走下平台:原地自由落体 */
|
||||
function fall(L, br, fx, fy, dir, targets) {
|
||||
let x = fx + dir * 3, y = fy - PH, vy = 0, t = 0;
|
||||
if (solidAt(L, br, x, y, PW, PH, false)) return null;
|
||||
while (t++ < 400) {
|
||||
vy = Math.min(vy + GRAV, MAXFALL);
|
||||
const prevBottom = y + PH;
|
||||
let rem = Math.abs(vy), landed = false;
|
||||
while (rem > 0.0001) {
|
||||
const m = Math.min(1, rem);
|
||||
const ny = y + m;
|
||||
if (!solidAt(L, br, x, ny, PW, PH, true, prevBottom)) { y = ny; rem -= m; }
|
||||
else { landed = true; break; }
|
||||
}
|
||||
if (L.deadlyRect(x, y, PW, PH)) return null;
|
||||
if (targets) {
|
||||
const box = { x, y, w: PW, h: PH };
|
||||
for (const tg of targets) if (!tg.hit && overlap(box, tg.rect)) tg.hit = true;
|
||||
}
|
||||
if (y > L.pxH + 120) return null;
|
||||
if (landed) return { fx: x, fy: y + PH };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function solve(d) {
|
||||
const L = new Level(d);
|
||||
const br = [];
|
||||
// 水:视为可站立的表面(游泳近似)
|
||||
for (let y = 0; y < L.h; y++)
|
||||
for (let x = 0; x < L.w; x++)
|
||||
if (L.tiles[y][x] === 6) L.tiles[y][x] = 1;
|
||||
// 移动平台:整条路径当作桥(乐观)
|
||||
for (const m of L.movers) {
|
||||
const xs = m.path.map(p => p.x).concat([m.x]);
|
||||
const ys = m.path.map(p => p.y).concat([m.y]);
|
||||
const x0 = Math.min(...xs), x1 = Math.max(...xs) + m.w;
|
||||
const y0 = Math.min(...ys), y1 = Math.max(...ys) + m.h;
|
||||
br.push({ x: x0, y: y0, w: x1 - x0, h: y1 - y0 });
|
||||
}
|
||||
|
||||
const targets = [];
|
||||
for (const e of L.entities) {
|
||||
if (e.type === 'goal' || e.type === 'save' || e.type === 'apple')
|
||||
targets.push({ type: e.type, idx: e.idx, rect: { x: e.x, y: e.y, w: e.w, h: e.h }, hit: false });
|
||||
}
|
||||
|
||||
// 起点:从出生点往下找落脚点
|
||||
let sx = L.spawn.x, sy = L.spawn.y + PH;
|
||||
let start = null;
|
||||
for (let k = 0; k < 400; k++) {
|
||||
if (canStand(L, br, sx, sy + k)) { start = { fx: sx, fy: sy + k }; break; }
|
||||
if (sy + k > L.pxH) break;
|
||||
}
|
||||
if (!start) return { err: '出生点下方 400px 内找不到落脚点', targets };
|
||||
|
||||
const key = s => `${Math.round(s.fx / 6)},${Math.round(s.fy / 6)}`;
|
||||
const seen = new Set([key(start)]);
|
||||
const q = [start];
|
||||
let n = 0, steps = 0;
|
||||
|
||||
// 起跳时就检查一次(原地也算到过)
|
||||
while (q.length && n < 40000) {
|
||||
const s = q.shift(); n++;
|
||||
const box = { x: s.fx, y: s.fy - PH, w: PW, h: PH };
|
||||
for (const tg of targets) if (!tg.hit && overlap(box, tg.rect)) tg.hit = true;
|
||||
|
||||
// 走路
|
||||
for (const d2 of [-HSPD, HSPD]) {
|
||||
const nx = s.fx + d2;
|
||||
if (canStand(L, br, nx, s.fy)) {
|
||||
const ns = { fx: nx, fy: s.fy };
|
||||
const k = key(ns);
|
||||
if (!seen.has(k)) { seen.add(k); q.push(ns); }
|
||||
}
|
||||
}
|
||||
// 走下悬崖
|
||||
for (const d2 of [-1, 1]) {
|
||||
const r = fall(L, br, s.fx, s.fy, d2, targets);
|
||||
steps++;
|
||||
if (r && canStand(L, br, r.fx, r.fy)) {
|
||||
const k = key(r);
|
||||
if (!seen.has(k)) { seen.add(k); q.push(r); }
|
||||
}
|
||||
}
|
||||
// 跳跃(含二段跳时机 / 空中转向)
|
||||
for (const vx of [-HSPD, 0, HSPD]) {
|
||||
for (const dj of DJSET) {
|
||||
const r = jump(L, br, s.fx, s.fy, vx, dj, 0, targets);
|
||||
steps++;
|
||||
if (r && canStand(L, br, r.fx, r.fy)) {
|
||||
const k = key(r);
|
||||
if (!seen.has(k)) { seen.add(k); q.push(r); }
|
||||
}
|
||||
if (vx !== 0) {
|
||||
const r2 = jump(L, br, s.fx, s.fy, vx, dj, 22, targets);
|
||||
steps++;
|
||||
if (r2 && canStand(L, br, r2.fx, r2.fy)) {
|
||||
const k = key(r2);
|
||||
if (!seen.has(k)) { seen.add(k); q.push(r2); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { states: seen.size, steps, targets };
|
||||
}
|
||||
|
||||
// ---------------- 运行 ----------------
|
||||
let fail = 0;
|
||||
console.log('=== 可达性求解 ===');
|
||||
for (const d of LEVELS) {
|
||||
if (d.type === 'boss') { console.log(` ${d.id.padEnd(3)} ${d.name.padEnd(17)} (BOSS 关跳过)`); continue; }
|
||||
let r;
|
||||
try { r = solve(d); }
|
||||
catch (e) { console.log(` ✗ ${d.id} 求解异常: ${e.message}`); fail++; continue; }
|
||||
if (r.err) { console.log(` ✗ ${d.id.padEnd(3)} ${d.name.padEnd(17)} ${r.err}`); fail++; continue; }
|
||||
|
||||
const goal = r.targets.find(t => t.type === 'goal');
|
||||
const saves = r.targets.filter(t => t.type === 'save');
|
||||
const apples = r.targets.filter(t => t.type === 'apple');
|
||||
const okSave = saves.filter(s => s.hit).length;
|
||||
const okApple = apples.filter(a => a.hit).length;
|
||||
const badSave = saves.filter(s => !s.hit).map(s => `#${s.idx}`);
|
||||
const flag = goal && goal.hit ? ' ' : '✗';
|
||||
if (!goal || !goal.hit) fail++;
|
||||
if (badSave.length) fail++;
|
||||
console.log(` ${flag} ${d.id.padEnd(3)} ${d.name.padEnd(17)} 终点=${goal ? (goal.hit ? 'OK' : '不可达') : '无'} 存档=${okSave}/${saves.length}${badSave.length ? ' 缺:' + badSave.join(',') : ''}${apples.length ? ` 苹果=${okApple}/${apples.length}` : ''} (状态${r.states} 模拟${r.steps})`);
|
||||
}
|
||||
console.log(fail ? `\n❌ ${fail} 项不可达` : '\n✅ 所有关卡终点与存档点均可达');
|
||||
process.exit(fail ? 1 : 0);
|
||||
Reference in new issue
Block a user