From 5b3a9db98662bd9fb8a7953dc54f9e3ebe4d0582 Mon Sep 17 00:00:00 2001 From: troytt <47798984@qq.com> Date: Thu, 17 Sep 2026 11:29:06 +0000 Subject: [PATCH] =?UTF-8?q?fix(maze):=20=E8=AE=A9=20UNIMPLEMENTED=5FPASSES?= =?UTF-8?q?=20=E5=8F=8D=E6=98=A0=E7=9C=9F=E5=AE=9E=E7=BC=BA=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `UNIMPLEMENTED_PASSES` 此前被硬编码为 `[]`,注释声称 11 个核心地牢 pass 全部 实现,于是 `stats.unimplementedPasses` 在每一关都无条件报告「零缺口」——包括 那些 `specials` 表为空、根本不生成楼梯房间的 level type。 改为从 `MAZE_LEVEL_TYPE_PROFILES` 实测推导: - 新增 `unimplementedPassesFor(levelTypeName)`,按 level type 返回缺口: 没有 profile 行,或 `specials` 为空且不是 `singleRoom` 定制关。 - `UNIMPLEMENTED_PASSES` 改为所有 level type 缺口的并集,因此非空。 - `generateMaze` 的 `stats.unimplementedPasses` 改报本关自己的缺口, 而不是全局常量;`unresolvedRoles` / `notes` 本来就已随 stats 返回。 同时修正两处已经与代码不符的注释:`maze.ts` 里「per-act 装饰 pass 均未复现」 (它们已在 `maze-special-passes.ts` 中实现),以及 `pack-act-assets.ts` 里 「第二幕下水道 / 墓穴 / 第三幕地牢 specials 为空」(这三者现在都有 specials, 真正为空的是 Harem、Basement、Spider、Act 5 Lava)。 测试:原 `UNIMPLEMENTED_PASSES is completely empty` 断言的是被修正的错误行为, 改为断言它恰好列出所有 `specials` 为空的 level type;另加 4 个用例覆盖 按 level type 查询与 `generateMaze` 的 stats 透出。 TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1 --- scripts/pack-act-assets.ts | 7 ++-- src/game/maze.ts | 52 ++++++++++++++++++++++--- tests/maze-special-passes.test.ts | 64 +++++++++++++++++++++++++++++-- 3 files changed, 110 insertions(+), 13 deletions(-) diff --git a/scripts/pack-act-assets.ts b/scripts/pack-act-assets.ts index f7b9cc4..ebb47fc 100644 --- a/scripts/pack-act-assets.ts +++ b/scripts/pack-act-assets.ts @@ -730,9 +730,10 @@ function buildSceneLinks( placedSeams.add(edge.to) } - // 5. Last resort. Several level types — the Act 2 sewers, the tombs, the - // Act 3 dungeons — have an empty `specials` table in `MAZE_LEVEL_TYPE_PROFILES` - // because their DRLG staircase pass has not been transcribed, so they + // 5. Last resort. Several level types — the Act 2 Harem and Basement, the + // Act 3 Spider Cavern, the Act 5 Infernal Pit — have an empty `specials` + // table in `MAZE_LEVEL_TYPE_PROFILES` because their DRLG staircase pass + // has not been transcribed (`unimplementedPassesFor` names them), so they // stamp no stair room, and their pieces carry no marker either. Leaving // those edges unplaced makes a level you can enter and never leave, which // is a worse lie than an approximate staircase. Each one is spread to a diff --git a/src/game/maze.ts b/src/game/maze.ts index df5dd3a..c6b70fd 100644 --- a/src/game/maze.ts +++ b/src/game/maze.ts @@ -594,8 +594,11 @@ function entries(plainSides: readonly string[] = NESW): Omit candidate.levelTypeName === levelTypeName) + if (profile === undefined) return [`DRLGMAZE_GenerateLevel profile (${levelTypeName})`] + const gaps: string[] = [] + if (profile.specials.length === 0 && profile.singleRoom === undefined) { + gaps.push(`DRLGMAZE_GenerateLevel specials table (${levelTypeName})`) + } + return gaps +} + +/** + * Every gap {@link unimplementedPassesFor} can name, across all level types. + * + * Non-empty while any level type is still missing its replacement table. Prefer + * the per-level-type function: a caller that reports this constant for one level + * is reporting other levels' gaps as its own. + */ +export const UNIMPLEMENTED_PASSES: readonly string[] = MAZE_LEVEL_TYPE_PROFILES + .flatMap(profile => unimplementedPassesFor(profile.levelTypeName)) /* ------------------------------------------------------------------------- * * Geometry @@ -2327,7 +2367,7 @@ export function generateMaze(request: MazeRequest): MazeResult { } : null, unresolvedRoles: stats.unresolvedRoles, - unimplementedPasses: UNIMPLEMENTED_PASSES, + unimplementedPasses: unimplementedPassesFor(levelTypeName), maxCellsX, maxCellsY, boundsRejected: stats.boundsRejected ?? 0, diff --git a/tests/maze-special-passes.test.ts b/tests/maze-special-passes.test.ts index c79a5da..9931269 100644 --- a/tests/maze-special-passes.test.ts +++ b/tests/maze-special-passes.test.ts @@ -13,7 +13,8 @@ * 9. DRLGMAZE_PlaceAct1Barracks (Monastery Barracks Smith Forge & Court Connect) * 10. DRLGMAZE_PlaceAct4Lava (River of Flame Hellforge, Bridges 1 & 2, Warp, Lava X fillers) * 11. DRLGMAZE_ScanReplaceSpecialAct2SewersPresets (Lut Gholein Sewers Radament's Lair, Dual Entrances, Waypoint) - * 12. Full generateMaze integration tests verifying specialPassesApplied and UNIMPLEMENTED_PASSES == []. + * 12. Full generateMaze integration tests verifying specialPassesApplied and the + * per-level-type `unimplementedPasses` gap list. */ import { describe, expect, test } from 'vitest' @@ -21,7 +22,9 @@ import type { Ds1, Ds1Cell, Ds1Object } from '../src/formats/ds1.ts' import { generateMaze, classifyMazePieceName, + MAZE_LEVEL_TYPE_PROFILES, UNIMPLEMENTED_PASSES, + unimplementedPassesFor, type MazePiece, type MazePieceKind, type MazeRequest, @@ -106,11 +109,64 @@ function createStandardPieceSet(levelTypeName: string, extraPieces: MazePiece[] } describe('Diablo II DRLG Maze Special Passes', () => { - test('UNIMPLEMENTED_PASSES is completely empty', () => { - expect(UNIMPLEMENTED_PASSES).toEqual([]) - expect(UNIMPLEMENTED_PASSES.length).toBe(0) + // Was `expect(UNIMPLEMENTED_PASSES).toEqual([])`: the constant was hardcoded + // to `[]`, so it asserted a claim the profile table contradicts. The gap list + // is now derived, so the honest assertion is "it names exactly the level types + // whose `specials` table is still empty". + test('UNIMPLEMENTED_PASSES names every level type with no transcribed specials table', () => { + const emptySpecials = MAZE_LEVEL_TYPE_PROFILES + .filter(profile => profile.specials.length === 0 && profile.singleRoom === undefined) + .map(profile => profile.levelTypeName) + expect(emptySpecials.length).toBeGreaterThan(0) + expect(UNIMPLEMENTED_PASSES).toEqual( + emptySpecials.map(name => `DRLGMAZE_GenerateLevel specials table (${name})`), + ) }) + test('unimplementedPassesFor is empty for a fully transcribed level type', () => { + expect(unimplementedPassesFor('Act 1 - Cave')).toEqual([]) + expect(unimplementedPassesFor('Act 2 - Tomb')).toEqual([]) + }) + + test('unimplementedPassesFor reports the gap for a level type with no specials', () => { + expect(unimplementedPassesFor('Act 3 - Spider')) + .toEqual(['DRLGMAZE_GenerateLevel specials table (Act 3 - Spider)']) + }) + + test('unimplementedPassesFor reports a level type that has no profile at all', () => { + expect(unimplementedPassesFor('Act 9 - Nowhere')) + .toEqual(['DRLGMAZE_GenerateLevel profile (Act 9 - Nowhere)']) + }) + + test('generateMaze reports the level type gap, not a global constant', () => { + const spider = generateMaze({ + levelId: 84, + levelName: 'Spider Cavern', + sectionSize: 10, + minRooms: 5, + merge: 500, + seed: 8484, + pieces: createStandardPieceSet('Act 3 - Spider'), + }) + expect(spider.stats.unimplementedPasses) + .toEqual(['DRLGMAZE_GenerateLevel specials table (Act 3 - Spider)']) + // The other two diagnostic channels must travel with it. + expect(Array.isArray(spider.stats.unresolvedRoles)).toBe(true) + expect(Array.isArray(spider.stats.notes)).toBe(true) + + const cave = generateMaze({ + levelId: 8, + levelName: 'Den of Evil', + sectionSize: 10, + minRooms: 5, + merge: 500, + seed: 8484, + pieces: createStandardPieceSet('Act 1 - Cave'), + }) + expect(cave.stats.unimplementedPasses).toEqual([]) + }) + + /* ----------------------------------------------------------------------- * * 1. Act 2 - Tomb * ----------------------------------------------------------------------- */