fix(maze): use PRNG stream in special passes and fix Act 5 temple NE waypoint generation
TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
This commit is contained in:
parent
a3a3e44838
commit
b8810072ce
|
|
@ -15,6 +15,7 @@
|
||||||
* - `DRLGMAZE_ScanReplaceSpecialAct2SewersPresets`: Lut Gholein Sewers (Radament's Lair & Atma's quest).
|
* - `DRLGMAZE_ScanReplaceSpecialAct2SewersPresets`: Lut Gholein Sewers (Radament's Lair & Atma's quest).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Rng } from './rng.ts'
|
||||||
import {
|
import {
|
||||||
DIRECTION_WEST,
|
DIRECTION_WEST,
|
||||||
DIRECTION_NORTH,
|
DIRECTION_NORTH,
|
||||||
|
|
@ -56,6 +57,11 @@ export interface SpecialPassContext {
|
||||||
readonly pieces: readonly MazePiece[]
|
readonly pieces: readonly MazePiece[]
|
||||||
readonly index: PieceIndex
|
readonly index: PieceIndex
|
||||||
readonly stats: MazeStats
|
readonly stats: MazeStats
|
||||||
|
readonly rng?: Rng
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPassRng(ctx: SpecialPassContext): Rng {
|
||||||
|
return ctx.rng ?? ctx.level?.rng ?? new Rng(ctx.request.seed)
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIRS = ['N', 'E', 'S', 'W'] as const
|
const DIRS = ['N', 'E', 'S', 'W'] as const
|
||||||
|
|
@ -189,6 +195,7 @@ export function placeNamedPiece(
|
||||||
* Horadric Cube chest, Leatherarm, and staircases.
|
* Horadric Cube chest, Leatherarm, and staircases.
|
||||||
*/
|
*/
|
||||||
export function placeAct2TombStuff(ctx: SpecialPassContext): void {
|
export function placeAct2TombStuff(ctx: SpecialPassContext): void {
|
||||||
|
const rng = getPassRng(ctx)
|
||||||
const levelId = ctx.request.levelId
|
const levelId = ctx.request.levelId
|
||||||
const staffTomb = ctx.request.staffTombLevelId ?? 66
|
const staffTomb = ctx.request.staffTombLevelId ?? 66
|
||||||
const bossTomb = ctx.request.bossTombLevelId
|
const bossTomb = ctx.request.bossTombLevelId
|
||||||
|
|
@ -202,9 +209,9 @@ export function placeAct2TombStuff(ctx: SpecialPassContext): void {
|
||||||
else if (name.includes('NEW')) nDirection = 1
|
else if (name.includes('NEW')) nDirection = 1
|
||||||
else if (name.includes('NSE')) nDirection = 2
|
else if (name.includes('NSE')) nDirection = 2
|
||||||
else if (name.includes('SEW')) nDirection = 3
|
else if (name.includes('SEW')) nDirection = 3
|
||||||
else nDirection = ctx.request.seed % 4
|
else nDirection = rng.int(0, 3)
|
||||||
} else {
|
} else {
|
||||||
nDirection = ctx.request.seed % 4
|
nDirection = rng.int(0, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
const GROW_DIRS = [DIRECTION_SOUTH, DIRECTION_WEST, DIRECTION_NORTH, DIRECTION_EAST]
|
const GROW_DIRS = [DIRECTION_SOUTH, DIRECTION_WEST, DIRECTION_NORTH, DIRECTION_EAST]
|
||||||
|
|
@ -279,6 +286,7 @@ export function placeAct2TombStuff(ctx: SpecialPassContext): void {
|
||||||
* Level 3 places Coldworm the Burrower (Tight Spot S) and the Staff of Kings chest (Treasure W).
|
* Level 3 places Coldworm the Burrower (Tight Spot S) and the Staff of Kings chest (Treasure W).
|
||||||
*/
|
*/
|
||||||
export function placeAct2LairStuff(ctx: SpecialPassContext): void {
|
export function placeAct2LairStuff(ctx: SpecialPassContext): void {
|
||||||
|
const rng = getPassRng(ctx)
|
||||||
const levelId = ctx.request.levelId
|
const levelId = ctx.request.levelId
|
||||||
|
|
||||||
if (levelId === 64) {
|
if (levelId === 64) {
|
||||||
|
|
@ -309,7 +317,7 @@ export function placeAct2LairStuff(ctx: SpecialPassContext): void {
|
||||||
preferFurthest: false,
|
preferFurthest: false,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const nRand = ctx.request.seed % 4
|
const nRand = rng.int(0, 3)
|
||||||
placeNamedPiece(ctx, {
|
placeNamedPiece(ctx, {
|
||||||
kind: 'next',
|
kind: 'next',
|
||||||
namePrefix: 'Act 2 - Lair Next',
|
namePrefix: 'Act 2 - Lair Next',
|
||||||
|
|
@ -339,8 +347,9 @@ export function placeAct2LairStuff(ctx: SpecialPassContext): void {
|
||||||
* Temples (levels 94..99) place Lam Esen's Tome altar.
|
* Temples (levels 94..99) place Lam Esen's Tome altar.
|
||||||
*/
|
*/
|
||||||
export function placeAct3DungeonStuff(ctx: SpecialPassContext): void {
|
export function placeAct3DungeonStuff(ctx: SpecialPassContext): void {
|
||||||
|
const rng = getPassRng(ctx)
|
||||||
const levelId = ctx.request.levelId
|
const levelId = ctx.request.levelId
|
||||||
const nDirection = ctx.request.seed % 4
|
const nDirection = rng.int(0, 3)
|
||||||
|
|
||||||
if (levelId === 91) {
|
if (levelId === 91) {
|
||||||
// Flayer Dungeon Level 3: Khalim's Brain chest
|
// Flayer Dungeon Level 3: Khalim's Brain chest
|
||||||
|
|
@ -404,8 +413,9 @@ export function placeAct3DungeonStuff(ctx: SpecialPassContext): void {
|
||||||
* Level 1 places the drain descent lever and staircase down to Level 2.
|
* Level 1 places the drain descent lever and staircase down to Level 2.
|
||||||
*/
|
*/
|
||||||
export function placeAct3SewerStuff(ctx: SpecialPassContext): void {
|
export function placeAct3SewerStuff(ctx: SpecialPassContext): void {
|
||||||
|
const rng = getPassRng(ctx)
|
||||||
const levelId = ctx.request.levelId
|
const levelId = ctx.request.levelId
|
||||||
const nDirection = ctx.request.seed % 4
|
const nDirection = rng.int(0, 3)
|
||||||
|
|
||||||
if (levelId === 92) {
|
if (levelId === 92) {
|
||||||
// Sewer Level 1: Drain lever / stairs down to Sewer 2
|
// Sewer Level 1: Drain lever / stairs down to Sewer 2
|
||||||
|
|
@ -454,8 +464,9 @@ export function placeAct3SewerStuff(ctx: SpecialPassContext): void {
|
||||||
* Level 2 places Waypoint, Next and Prev.
|
* Level 2 places Waypoint, Next and Prev.
|
||||||
*/
|
*/
|
||||||
export function placeAct3MephistoStuff(ctx: SpecialPassContext): void {
|
export function placeAct3MephistoStuff(ctx: SpecialPassContext): void {
|
||||||
|
const rng = getPassRng(ctx)
|
||||||
const levelId = ctx.request.levelId
|
const levelId = ctx.request.levelId
|
||||||
const nRand = ctx.request.seed % 4
|
const nRand = rng.int(0, 3)
|
||||||
|
|
||||||
if (levelId === 102) {
|
if (levelId === 102) {
|
||||||
// Durance of Hate Level 3: Mephisto Complex
|
// Durance of Hate Level 3: Mephisto Complex
|
||||||
|
|
@ -526,6 +537,7 @@ export function placeAct3MephistoStuff(ctx: SpecialPassContext): void {
|
||||||
* Halls of Pain (level 123) places Waypoint and descent stairs.
|
* Halls of Pain (level 123) places Waypoint and descent stairs.
|
||||||
*/
|
*/
|
||||||
export function placeAct5TempleStuff(ctx: SpecialPassContext): void {
|
export function placeAct5TempleStuff(ctx: SpecialPassContext): void {
|
||||||
|
const rng = getPassRng(ctx)
|
||||||
const levelId = ctx.request.levelId
|
const levelId = ctx.request.levelId
|
||||||
|
|
||||||
if (levelId === 124) {
|
if (levelId === 124) {
|
||||||
|
|
@ -547,7 +559,7 @@ export function placeAct5TempleStuff(ctx: SpecialPassContext): void {
|
||||||
} else {
|
} else {
|
||||||
// Levels 122 and 123
|
// Levels 122 and 123
|
||||||
const templeDownDirs = ['NE', 'NW', 'SW']
|
const templeDownDirs = ['NE', 'NW', 'SW']
|
||||||
const nDirection = (ctx.request.seed >>> 0) % 3
|
const nDirection = rng.int(0, templeDownDirs.length - 1)
|
||||||
const downSide = templeDownDirs[nDirection]!
|
const downSide = templeDownDirs[nDirection]!
|
||||||
|
|
||||||
placeNamedPiece(ctx, {
|
placeNamedPiece(ctx, {
|
||||||
|
|
@ -560,7 +572,7 @@ export function placeAct5TempleStuff(ctx: SpecialPassContext): void {
|
||||||
|
|
||||||
if (levelId === 123) {
|
if (levelId === 123) {
|
||||||
const templeWaypointDirs = ['NE', 'NW', 'SW', 'SE']
|
const templeWaypointDirs = ['NE', 'NW', 'SW', 'SE']
|
||||||
const wpSide = templeWaypointDirs[(nDirection + 1) % 4]!
|
const wpSide = templeWaypointDirs[rng.int(0, templeWaypointDirs.length - 1)]!
|
||||||
placeNamedPiece(ctx, {
|
placeNamedPiece(ctx, {
|
||||||
kind: 'waypoint',
|
kind: 'waypoint',
|
||||||
name: `Act 5 - Temple ${wpSide} Waypoint`,
|
name: `Act 5 - Temple ${wpSide} Waypoint`,
|
||||||
|
|
@ -591,8 +603,9 @@ export function placeAct5TempleStuff(ctx: SpecialPassContext): void {
|
||||||
* Worldstone Keep 2 places Waypoint and Next stairs.
|
* Worldstone Keep 2 places Waypoint and Next stairs.
|
||||||
*/
|
*/
|
||||||
export function placeAct5BaalStuff(ctx: SpecialPassContext): void {
|
export function placeAct5BaalStuff(ctx: SpecialPassContext): void {
|
||||||
|
const rng = getPassRng(ctx)
|
||||||
const levelId = ctx.request.levelId
|
const levelId = ctx.request.levelId
|
||||||
const nDirection = ctx.request.seed % 4
|
const nDirection = rng.int(0, 3)
|
||||||
|
|
||||||
if (levelId === 131) {
|
if (levelId === 131) {
|
||||||
// Throne of Destruction
|
// Throne of Destruction
|
||||||
|
|
@ -657,11 +670,12 @@ export function placeAct5BaalStuff(ctx: SpecialPassContext): void {
|
||||||
* Glacial Trail (115) places Theme and Waypoint.
|
* Glacial Trail (115) places Theme and Waypoint.
|
||||||
*/
|
*/
|
||||||
export function placeAct5IceStuff(ctx: SpecialPassContext): void {
|
export function placeAct5IceStuff(ctx: SpecialPassContext): void {
|
||||||
|
const rng = getPassRng(ctx)
|
||||||
const levelId = ctx.request.levelId
|
const levelId = ctx.request.levelId
|
||||||
|
|
||||||
if (levelId === 114) {
|
if (levelId === 114) {
|
||||||
// Frozen River (Anya platform)
|
// Frozen River (Anya platform)
|
||||||
const variant = (ctx.request.seed % 2) === 0 ? 'A' : 'B'
|
const variant = rng.int(0, 1) === 0 ? 'A' : 'B'
|
||||||
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
|
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
|
||||||
const riverPiece = ctx.pieces.find(p => p.name === `Act 5 - Ice River ${variant}`)
|
const riverPiece = ctx.pieces.find(p => p.name === `Act 5 - Ice River ${variant}`)
|
||||||
?? ctx.pieces.find(p => p.name.startsWith('Act 5 - Ice River'))
|
?? ctx.pieces.find(p => p.name.startsWith('Act 5 - Ice River'))
|
||||||
|
|
@ -701,7 +715,7 @@ export function placeAct5IceStuff(ctx: SpecialPassContext): void {
|
||||||
placeNamedPiece(ctx, { kind: 'quest', name: 'Act 5 - Ice Pool B', preferFurthest: true })
|
placeNamedPiece(ctx, { kind: 'quest', name: 'Act 5 - Ice Pool B', preferFurthest: true })
|
||||||
} else {
|
} else {
|
||||||
// Levels 113, 115, 118
|
// Levels 113, 115, 118
|
||||||
let nDirection = ctx.request.seed % 4
|
let nDirection = rng.int(0, 3)
|
||||||
placeNamedPiece(ctx, {
|
placeNamedPiece(ctx, {
|
||||||
kind: 'prev',
|
kind: 'prev',
|
||||||
namePrefix: 'Act 5 - Ice Prev',
|
namePrefix: 'Act 5 - Ice Prev',
|
||||||
|
|
@ -760,6 +774,7 @@ export function placeAct5IceStuff(ctx: SpecialPassContext): void {
|
||||||
* Places Court Connect, the Smith / Charsi's Malus forge, and stairs to Jail.
|
* Places Court Connect, the Smith / Charsi's Malus forge, and stairs to Jail.
|
||||||
*/
|
*/
|
||||||
export function placeAct1Barracks(ctx: SpecialPassContext): void {
|
export function placeAct1Barracks(ctx: SpecialPassContext): void {
|
||||||
|
const rng = getPassRng(ctx)
|
||||||
// Connect towards Outer Cloister
|
// Connect towards Outer Cloister
|
||||||
placeNamedPiece(ctx, {
|
placeNamedPiece(ctx, {
|
||||||
kind: 'prev',
|
kind: 'prev',
|
||||||
|
|
@ -768,8 +783,8 @@ export function placeAct1Barracks(ctx: SpecialPassContext): void {
|
||||||
preferFurthest: false,
|
preferFurthest: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const nDirection = ctx.request.seed % 4
|
const nDirection = rng.int(0, 3)
|
||||||
const roll = (ctx.request.seed >>> 2) % 2
|
const roll = rng.int(0, 1)
|
||||||
|
|
||||||
if (roll === 1) {
|
if (roll === 1) {
|
||||||
placeNamedPiece(ctx, {
|
placeNamedPiece(ctx, {
|
||||||
|
|
@ -815,6 +830,7 @@ export function placeAct1Barracks(ctx: SpecialPassContext): void {
|
||||||
* Hephasto the Armorer / Hellforge, and fills blanks with Lava X.
|
* Hephasto the Armorer / Hellforge, and fills blanks with Lava X.
|
||||||
*/
|
*/
|
||||||
export function placeAct4Lava(ctx: SpecialPassContext): void {
|
export function placeAct4Lava(ctx: SpecialPassContext): void {
|
||||||
|
const rng = getPassRng(ctx)
|
||||||
// 1. Warp N (entrance from City of Damned)
|
// 1. Warp N (entrance from City of Damned)
|
||||||
placeNamedPiece(ctx, {
|
placeNamedPiece(ctx, {
|
||||||
kind: 'prev',
|
kind: 'prev',
|
||||||
|
|
@ -840,7 +856,7 @@ export function placeAct4Lava(ctx: SpecialPassContext): void {
|
||||||
})
|
})
|
||||||
|
|
||||||
// 3. Hellforge (Hephasto the Armorer)
|
// 3. Hellforge (Hephasto the Armorer)
|
||||||
const forgeSide = (ctx.request.seed % 2) === 0 ? 'W' : 'E'
|
const forgeSide = rng.int(0, 1) === 0 ? 'W' : 'E'
|
||||||
placeNamedPiece(ctx, {
|
placeNamedPiece(ctx, {
|
||||||
kind: 'quest',
|
kind: 'quest',
|
||||||
name: `Act 4 - Lava Forge ${forgeSide}`,
|
name: `Act 4 - Lava Forge ${forgeSide}`,
|
||||||
|
|
@ -865,8 +881,9 @@ export function placeAct4Lava(ctx: SpecialPassContext): void {
|
||||||
* Ancient Tunnels (level 50) places Chest and Prev.
|
* Ancient Tunnels (level 50) places Chest and Prev.
|
||||||
*/
|
*/
|
||||||
export function scanReplaceSpecialAct2SewersPresets(ctx: SpecialPassContext): void {
|
export function scanReplaceSpecialAct2SewersPresets(ctx: SpecialPassContext): void {
|
||||||
|
const rng = getPassRng(ctx)
|
||||||
const levelId = ctx.request.levelId
|
const levelId = ctx.request.levelId
|
||||||
let nRand = ctx.request.seed % 4
|
let nRand = rng.int(0, 3)
|
||||||
|
|
||||||
if (levelId === 49) {
|
if (levelId === 49) {
|
||||||
// Sewers Level 3: Radament's Lair
|
// Sewers Level 3: Radament's Lair
|
||||||
|
|
|
||||||
|
|
@ -2271,6 +2271,7 @@ export function generateMaze(request: MazeRequest): MazeResult {
|
||||||
pieces: request.pieces,
|
pieces: request.pieces,
|
||||||
index,
|
index,
|
||||||
stats,
|
stats,
|
||||||
|
rng,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -511,6 +511,33 @@ describe('Diablo II DRLG Maze Special Passes', () => {
|
||||||
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
||||||
expect(specials.some(s => s.kind === 'waypoint')).toBe(true)
|
expect(specials.some(s => s.kind === 'waypoint')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Halls of Pain (level 123): generates all 4 waypoint directions including NE across seeds', () => {
|
||||||
|
const pieces = createStandardPieceSet('Act 5 - Temple', templeExtraPieces)
|
||||||
|
const observedDirs = new Set<string>()
|
||||||
|
|
||||||
|
for (let seed = 1; seed <= 50; seed++) {
|
||||||
|
const res = generateMaze({
|
||||||
|
levelId: 123,
|
||||||
|
levelName: 'Halls of Pain',
|
||||||
|
sectionSize: 10,
|
||||||
|
minRooms: 5,
|
||||||
|
merge: 500,
|
||||||
|
seed,
|
||||||
|
pieces,
|
||||||
|
})
|
||||||
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
||||||
|
const wp = specials.find(s => s.kind === 'waypoint')
|
||||||
|
if (wp !== undefined) {
|
||||||
|
observedDirs.add(wp.sides)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(observedDirs).toContain('NE')
|
||||||
|
expect(observedDirs).toContain('NW')
|
||||||
|
expect(observedDirs).toContain('SW')
|
||||||
|
expect(observedDirs).toContain('SE')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------- *
|
/* ----------------------------------------------------------------------- *
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue