// 可达性求解器:用真实物理常量做 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);