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).
|
||||
*/
|
||||
|
||||
import { Rng } from './rng.ts'
|
||||
import {
|
||||
DIRECTION_WEST,
|
||||
DIRECTION_NORTH,
|
||||
|
|
@ -56,6 +57,11 @@ export interface SpecialPassContext {
|
|||
readonly pieces: readonly MazePiece[]
|
||||
readonly index: PieceIndex
|
||||
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
|
||||
|
|
@ -189,6 +195,7 @@ export function placeNamedPiece(
|
|||
* Horadric Cube chest, Leatherarm, and staircases.
|
||||
*/
|
||||
export function placeAct2TombStuff(ctx: SpecialPassContext): void {
|
||||
const rng = getPassRng(ctx)
|
||||
const levelId = ctx.request.levelId
|
||||
const staffTomb = ctx.request.staffTombLevelId ?? 66
|
||||
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('NSE')) nDirection = 2
|
||||
else if (name.includes('SEW')) nDirection = 3
|
||||
else nDirection = ctx.request.seed % 4
|
||||
else nDirection = rng.int(0, 3)
|
||||
} else {
|
||||
nDirection = ctx.request.seed % 4
|
||||
nDirection = rng.int(0, 3)
|
||||
}
|
||||
|
||||
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).
|
||||
*/
|
||||
export function placeAct2LairStuff(ctx: SpecialPassContext): void {
|
||||
const rng = getPassRng(ctx)
|
||||
const levelId = ctx.request.levelId
|
||||
|
||||
if (levelId === 64) {
|
||||
|
|
@ -309,7 +317,7 @@ export function placeAct2LairStuff(ctx: SpecialPassContext): void {
|
|||
preferFurthest: false,
|
||||
})
|
||||
} else {
|
||||
const nRand = ctx.request.seed % 4
|
||||
const nRand = rng.int(0, 3)
|
||||
placeNamedPiece(ctx, {
|
||||
kind: '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.
|
||||
*/
|
||||
export function placeAct3DungeonStuff(ctx: SpecialPassContext): void {
|
||||
const rng = getPassRng(ctx)
|
||||
const levelId = ctx.request.levelId
|
||||
const nDirection = ctx.request.seed % 4
|
||||
const nDirection = rng.int(0, 3)
|
||||
|
||||
if (levelId === 91) {
|
||||
// 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.
|
||||
*/
|
||||
export function placeAct3SewerStuff(ctx: SpecialPassContext): void {
|
||||
const rng = getPassRng(ctx)
|
||||
const levelId = ctx.request.levelId
|
||||
const nDirection = ctx.request.seed % 4
|
||||
const nDirection = rng.int(0, 3)
|
||||
|
||||
if (levelId === 92) {
|
||||
// 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.
|
||||
*/
|
||||
export function placeAct3MephistoStuff(ctx: SpecialPassContext): void {
|
||||
const rng = getPassRng(ctx)
|
||||
const levelId = ctx.request.levelId
|
||||
const nRand = ctx.request.seed % 4
|
||||
const nRand = rng.int(0, 3)
|
||||
|
||||
if (levelId === 102) {
|
||||
// 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.
|
||||
*/
|
||||
export function placeAct5TempleStuff(ctx: SpecialPassContext): void {
|
||||
const rng = getPassRng(ctx)
|
||||
const levelId = ctx.request.levelId
|
||||
|
||||
if (levelId === 124) {
|
||||
|
|
@ -547,7 +559,7 @@ export function placeAct5TempleStuff(ctx: SpecialPassContext): void {
|
|||
} else {
|
||||
// Levels 122 and 123
|
||||
const templeDownDirs = ['NE', 'NW', 'SW']
|
||||
const nDirection = (ctx.request.seed >>> 0) % 3
|
||||
const nDirection = rng.int(0, templeDownDirs.length - 1)
|
||||
const downSide = templeDownDirs[nDirection]!
|
||||
|
||||
placeNamedPiece(ctx, {
|
||||
|
|
@ -560,7 +572,7 @@ export function placeAct5TempleStuff(ctx: SpecialPassContext): void {
|
|||
|
||||
if (levelId === 123) {
|
||||
const templeWaypointDirs = ['NE', 'NW', 'SW', 'SE']
|
||||
const wpSide = templeWaypointDirs[(nDirection + 1) % 4]!
|
||||
const wpSide = templeWaypointDirs[rng.int(0, templeWaypointDirs.length - 1)]!
|
||||
placeNamedPiece(ctx, {
|
||||
kind: '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.
|
||||
*/
|
||||
export function placeAct5BaalStuff(ctx: SpecialPassContext): void {
|
||||
const rng = getPassRng(ctx)
|
||||
const levelId = ctx.request.levelId
|
||||
const nDirection = ctx.request.seed % 4
|
||||
const nDirection = rng.int(0, 3)
|
||||
|
||||
if (levelId === 131) {
|
||||
// Throne of Destruction
|
||||
|
|
@ -657,11 +670,12 @@ export function placeAct5BaalStuff(ctx: SpecialPassContext): void {
|
|||
* Glacial Trail (115) places Theme and Waypoint.
|
||||
*/
|
||||
export function placeAct5IceStuff(ctx: SpecialPassContext): void {
|
||||
const rng = getPassRng(ctx)
|
||||
const levelId = ctx.request.levelId
|
||||
|
||||
if (levelId === 114) {
|
||||
// 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) {
|
||||
const riverPiece = ctx.pieces.find(p => p.name === `Act 5 - Ice River ${variant}`)
|
||||
?? 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 })
|
||||
} else {
|
||||
// Levels 113, 115, 118
|
||||
let nDirection = ctx.request.seed % 4
|
||||
let nDirection = rng.int(0, 3)
|
||||
placeNamedPiece(ctx, {
|
||||
kind: '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.
|
||||
*/
|
||||
export function placeAct1Barracks(ctx: SpecialPassContext): void {
|
||||
const rng = getPassRng(ctx)
|
||||
// Connect towards Outer Cloister
|
||||
placeNamedPiece(ctx, {
|
||||
kind: 'prev',
|
||||
|
|
@ -768,8 +783,8 @@ export function placeAct1Barracks(ctx: SpecialPassContext): void {
|
|||
preferFurthest: false,
|
||||
})
|
||||
|
||||
const nDirection = ctx.request.seed % 4
|
||||
const roll = (ctx.request.seed >>> 2) % 2
|
||||
const nDirection = rng.int(0, 3)
|
||||
const roll = rng.int(0, 1)
|
||||
|
||||
if (roll === 1) {
|
||||
placeNamedPiece(ctx, {
|
||||
|
|
@ -815,6 +830,7 @@ export function placeAct1Barracks(ctx: SpecialPassContext): void {
|
|||
* Hephasto the Armorer / Hellforge, and fills blanks with Lava X.
|
||||
*/
|
||||
export function placeAct4Lava(ctx: SpecialPassContext): void {
|
||||
const rng = getPassRng(ctx)
|
||||
// 1. Warp N (entrance from City of Damned)
|
||||
placeNamedPiece(ctx, {
|
||||
kind: 'prev',
|
||||
|
|
@ -840,7 +856,7 @@ export function placeAct4Lava(ctx: SpecialPassContext): void {
|
|||
})
|
||||
|
||||
// 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, {
|
||||
kind: 'quest',
|
||||
name: `Act 4 - Lava Forge ${forgeSide}`,
|
||||
|
|
@ -865,8 +881,9 @@ export function placeAct4Lava(ctx: SpecialPassContext): void {
|
|||
* Ancient Tunnels (level 50) places Chest and Prev.
|
||||
*/
|
||||
export function scanReplaceSpecialAct2SewersPresets(ctx: SpecialPassContext): void {
|
||||
const rng = getPassRng(ctx)
|
||||
const levelId = ctx.request.levelId
|
||||
let nRand = ctx.request.seed % 4
|
||||
let nRand = rng.int(0, 3)
|
||||
|
||||
if (levelId === 49) {
|
||||
// Sewers Level 3: Radament's Lair
|
||||
|
|
|
|||
|
|
@ -2271,6 +2271,7 @@ export function generateMaze(request: MazeRequest): MazeResult {
|
|||
pieces: request.pieces,
|
||||
index,
|
||||
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 }[]
|
||||
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