/** * Wilderness telemetry must be measured, not invented (issue #58 item 6). * * Every number in `stats` is read by `npm run verify:generators` and by the * in-game HUD to decide whether a level generated correctly. Four of them were * literals or no-op arithmetic, so a level that produced nothing still reported * a plausible-looking map: * * - `wilderness-jungles.ts` reported `junglePathCells: 100` and at least 2 * `riverBridges`, whatever was painted. * - `wilderness-acts.ts` counted Act 4 plateau cells in a loop that painted * nothing for levels 104 and 106. * - `wilderness.ts` reported `subTheme: 0` on the special-preset return path. * - `applySubstitutions` capped `BordType` 0 rows with `Math.min(1, Infinity)`, * a budget that never constrained anything. */ import { describe, expect, test } from 'vitest' import { createCanvas, generateWilderness, stampDs1 } from '../src/game/wilderness.ts' import type { SubstitutionReport, WildernessPiece, WildernessStats, WildernessSubstitution, } from '../src/game/wilderness.ts' import type { Act3TopographyStats, Act4TopographyStats } from '../src/game/wilderness-acts.ts' import { DRLG_GenerateJungles } from '../src/game/wilderness-jungles.ts' import { Rng } from '../src/game/rng.ts' import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts' /** A fresh, empty report collector. */ function makeStats(): WildernessStats { return { substitutions: [], borderPieces: {}, unresolved: [], notes: [], borderStamped: 0, groundCells: 0, groundTile: null, sizeSource: 'test', roadCells: 0, roadSegments: 0, anchors: 0, specialPresets: [], entrances: [], proceduralRoadCells: 0, proceduralDividerCells: 0, proceduralWaterCells: 0, proceduralWallCells: 0, } } /** A solid block of floor, so stamped pieces are measurable. */ function makeMockDs1(width: number, height: number): Ds1 { const cells: Ds1Cell[][] = [] for (let y = 0; y < height; y += 1) { const row: Ds1Cell[] = [] for (let x = 0; x < width; x += 1) { row.push({ walls: [], floors: [{ prop1: 2, sequence: 0, style: 1, unknown1: 0, unknown2: 0, hidden: false }], shadows: [], substitutions: [], }) } cells.push(row) } return { version: 18, width, height, act: 3, substitutionType: 0, wallLayers: 1, floorLayers: 1, cells, objects: [], npcPathOffset: null, } } function makeMockBorderPieces(actPrefix: string): WildernessPiece[] { const borderDs1 = makeMockDs1(8, 8) return Array.from({ length: 8 }, (_unused, i) => ({ name: `${actPrefix} - Border ${String(i + 1)}`, border: true, levels: [borderDs1], })) } describe('Act 3 jungle topography is counted, not assumed', () => { test('a jungle with no presets reports zero ground and zero water', () => { const canvas = createCanvas(64, 192, 1, 1, 1) const stats = makeStats() DRLG_GenerateJungles(canvas, 76, [], new Rng(76001), stats, stampDs1) const topo = stats.act3Topography as Act3TopographyStats // Nothing was stamped, so there is no jungle ground and no river water. // This used to report 100 path cells regardless. expect(topo.junglePathCells).toBe(0) expect(topo.canalCells).toBe(0) // The bridges that exist are the plank crossings the pass paints itself: // the island/main-channel bridges plus both canonical crossings. expect(topo.riverBridges).toBe((stats.bridgesPlaced ?? 0) + 2) // Causeways belong to Kurast, and the jungle pass paints none. expect(topo.causewayCells).toBe(0) }) test('a canvas too short for the second crossing does not count it', () => { const canvas = createCanvas(64, 96, 1, 1, 1) const stats = makeStats() DRLG_GenerateJungles(canvas, 76, [], new Rng(76001), stats, stampDs1) const topo = stats.act3Topography as Act3TopographyStats // The crossing at y = 142 falls off a 96-cell canvas and paints nothing. // `Math.max(2, bridges.length)` used to report it anyway. expect(topo.riverBridges).toBe((stats.bridgesPlaced ?? 0) + 1) }) test('ground and water counts add up to what is on the canvas', () => { const canvas = createCanvas(64, 192, 1, 1, 1) const stats = makeStats() const pieces: WildernessPiece[] = [ { name: 'Act 3 - Jungle Tail', border: false, levels: [makeMockDs1(64, 32)] }, { name: 'Act 3 - Jungle Head', border: false, levels: [makeMockDs1(64, 32)] }, { name: 'Act 3 - Clearing Webby E', border: false, levels: [makeMockDs1(32, 32)] }, { name: 'Act 3 - Jungle NS W', border: false, levels: [makeMockDs1(32, 32)] }, ] DRLG_GenerateJungles(canvas, 76, pieces, new Rng(76001), stats, stampDs1) let painted = 0 for (let y = 0; y < canvas.height; y += 1) { for (let x = 0; x < canvas.width; x += 1) { const cell = canvas.cells[y]![x]! const floor = cell.floors.find( f => !f.hidden && (f.style !== 0 || f.sequence !== 0 || f.prop1 !== 0), ) if (floor === undefined) continue // Bridge planks are decks over the water, reported as bridges. if (floor.style === 4 && floor.prop1 === 2) continue painted += 1 } } const topo = stats.act3Topography as Act3TopographyStats expect(painted).toBeGreaterThan(0) expect(topo.junglePathCells + topo.canalCells).toBe(painted) expect(topo.canalCells).toBeGreaterThan(0) }) }) describe('Act 4 plateaus are only counted where they are painted', () => { test('Outer Steppes (104) reports no plateau cells and names the gap', () => { const result = generateWilderness({ levelId: 104, levelName: 'Outer Steppes', levelTypeName: 'Act 4 - Mesa', sizeX: 80, sizeY: 80, subType: 0, subTheme: 0, seed: 104001, pieces: makeMockBorderPieces('Act 4'), substitutions: [], }) const topo = result.stats.act4Topography as Act4TopographyStats // The 104/106 branch surveys a stride grid and paints nothing; it used to // count every sample as a plateau cell. expect(topo.plateauCells).toBe(0) const notes = result.stats.notes as readonly string[] expect(notes.some(note => note.includes('Act 4 basalt plateaus not implemented'))).toBe(true) }) test('Plains of Despair (105) still counts the prison plateau it paints', () => { const result = generateWilderness({ levelId: 105, levelName: 'Plains of Despair', levelTypeName: 'Act 4 - Mesa', sizeX: 80, sizeY: 80, subType: 0, subTheme: 0, seed: 105001, pieces: makeMockBorderPieces('Act 4'), substitutions: [], }) const topo = result.stats.act4Topography as Act4TopographyStats expect(topo.plateauCells).toBeGreaterThan(20) }) }) describe('the special-preset path reports the real SubTheme', () => { test('a jungle level keeps the SubTheme it was asked for', () => { const pieces: WildernessPiece[] = [ { name: 'Act 3 - Jungle Tail', border: false, levels: [makeMockDs1(64, 32)] }, { name: 'Act 3 - Jungle Head', border: false, levels: [makeMockDs1(64, 32)] }, { name: 'Act 3 - Clearing Webby E', border: false, levels: [makeMockDs1(32, 32)] }, { name: 'Act 3 - Jungle NS W', border: false, levels: [makeMockDs1(32, 32)] }, ] const result = generateWilderness({ levelId: 76, levelName: 'Spider Forest', levelTypeName: 'Act 3 - Jungle', sizeX: 64, sizeY: 192, subType: 0, subTheme: 3, seed: 76001, pieces, substitutions: [], }) // This return path hardcoded 0, so every special level looked like theme 0. expect(result.stats.subTheme).toBe(3) }) test('an out-of-range SubTheme is clamped, like the main path clamps it', () => { const pieces: WildernessPiece[] = [ { name: 'Act 3 - Jungle Tail', border: false, levels: [makeMockDs1(64, 32)] }, { name: 'Act 3 - Clearing Webby E', border: false, levels: [makeMockDs1(32, 32)] }, ] const result = generateWilderness({ levelId: 76, levelName: 'Spider Forest', levelTypeName: 'Act 3 - Jungle', sizeX: 64, sizeY: 192, subType: 0, subTheme: 9, seed: 76002, pieces, substitutions: [], }) expect(result.stats.subTheme).toBe(4) }) }) describe('BordType 0 really is one cluster for the whole level', () => { /** Two clutter rows that both want a whole-level cluster. */ function makeBordType0Rows(): WildernessSubstitution[] { return ['Trees', 'Rocks'].map(name => ({ name, type: 1, gridSize: 1, bordType: 0, dt1Mask: 0, prob: [100, 100, 100, 100, 100], trials: [-1, -1, -1, -1, -1], max: [5, 5, 5, 5, 5], levels: [makeMockDs1(4, 4)], })) } test('the second BordType 0 row gets nothing left to spend', () => { const result = generateWilderness({ levelId: 2, levelName: 'Cold Plains', levelTypeName: 'Act 1 - Wilderness', sizeX: 64, sizeY: 64, subType: 1, subTheme: 0, seed: 2001, pieces: makeMockBorderPieces('Act 1'), substitutions: makeBordType0Rows(), }) const reported = result.stats.substitutions as readonly SubstitutionReport[] const rows = reported.filter(row => row.name === 'Trees' || row.name === 'Rocks') expect(rows).toHaveLength(2) const spent = rows.reduce((sum, row) => sum + row.clusters, 0) // `Math.min(1, Infinity)` handed each row its own cluster, so this used to // be 2: a per-row cap wearing a whole-level cap's name. expect(spent).toBeLessThanOrEqual(1) expect(rows[0]!.clusters).toBe(1) expect(rows[1]!.clusters).toBe(0) }) }) describe('Procedural terrain pass telemetry tracks real cell counts (Issue #62 & #65)', () => { test('Act 1 wilderness tracks procedural road cells with zero artificial dividers (Issue #65)', () => { const result = generateWilderness({ levelId: 2, levelName: 'Cold Plains', levelTypeName: 'Act 1 - Wilderness', sizeX: 64, sizeY: 64, subType: 1, subTheme: 0, seed: 2001, pieces: makeMockBorderPieces('Act 1'), substitutions: [], }) expect(result.stats.proceduralRoadCells).toBeGreaterThan(0) expect(result.stats.proceduralDividerCells).toBe(0) expect(result.stats.proceduralWallCells).toBe(0) // Verify 0 artificial divider wall cells on canvas (walls with style 2, prop1 129) let wallCount = 0 for (let y = 0; y < result.level.height; y += 1) { for (let x = 0; x < result.level.width; x += 1) { const cell = result.level.cells[y]![x]! if (cell.walls.some(w => !w.hidden && w.style === 2 && w.prop1 === 129)) { wallCount += 1 } } } expect(wallCount).toBe(0) }) test('Act 2 desert tracks procedural roads, water (oasis), and walls (canyon + berms)', () => { const rocky = generateWilderness({ levelId: 41, levelName: 'Rocky Waste', levelTypeName: 'Act 2 - Desert', sizeX: 64, sizeY: 64, subType: 0, subTheme: 0, seed: 41001, pieces: makeMockBorderPieces('Act 2'), substitutions: [], }) expect(rocky.stats.proceduralRoadCells).toBeGreaterThan(0) expect(rocky.stats.proceduralWallCells).toBeGreaterThan(0) const oasis = generateWilderness({ levelId: 43, levelName: 'Far Oasis', levelTypeName: 'Act 2 - Desert', sizeX: 64, sizeY: 64, subType: 0, subTheme: 0, seed: 43001, pieces: makeMockBorderPieces('Act 2'), substitutions: [], }) expect(oasis.stats.proceduralRoadCells).toBeGreaterThan(0) expect(oasis.stats.proceduralWaterCells).toBeGreaterThan(0) }) test('Act 3 Kurast Causeway tracks causeway, canal, and balustrade cells', () => { const result = generateWilderness({ levelId: 82, levelName: 'Kurast Causeway', levelTypeName: 'Act 3 - Kurast', sizeX: 32, sizeY: 64, subType: 0, subTheme: 0, seed: 82001, pieces: [{ name: 'Act 3 - Bridge', border: false, levels: [makeMockDs1(16, 16)] }], substitutions: [], }) expect(result.stats.proceduralRoadCells).toBeGreaterThan(0) expect(result.stats.proceduralWaterCells).toBeGreaterThan(0) expect(result.stats.proceduralWallCells).toBeGreaterThan(0) }) test('Act 5 Siege levels track barricade walls, trenches, and frozen lakes', () => { const result = generateWilderness({ levelId: 112, levelName: 'Arreat Plateau', levelTypeName: 'Act 5 - Barricade', sizeX: 80, sizeY: 80, subType: 0, subTheme: 0, seed: 112001, pieces: makeMockBorderPieces('Act 5'), substitutions: [], }) expect(result.stats.proceduralWallCells).toBeGreaterThan(0) expect(result.stats.proceduralRoadCells).toBeGreaterThan(0) expect(result.stats.proceduralWaterCells).toBeGreaterThan(0) }) })