Special Level Finalization Pipeline

See also: DESIGN.md (architecture) | LORE.md (porting lessons) | C_PARITY_WORKLIST.md (implementation plan)

Overview

Special levels (Oracle, Mines, Castle, etc.) require a complete finalization pipeline to match C NetHack’s RNG behavior. This document describes the required steps discovered through test regression debugging (2026-02-10).

The Problem

After commit 065cdb0 added makelevel(depth, dnum, dlevel) support for special levels, tests calling makelevel(1) without branch coordinates fell through to procedural generation, causing themerooms RNG explosion (1.4M+ calls vs 2.9K expected).

The Solution

1. Proper Level Selection

Tests must specify branch coordinates:

// WRONG - generates procedural dungeon
const map = makelevel(1);

// CORRECT - generates Oracle special level
const { DUNGEONS_OF_DOOM } = await import('../../js/special_levels.js');
const map = makelevel(5, DUNGEONS_OF_DOOM, 5);

2. Complete Finalization Pipeline

Special levels in finalize_level() must execute all steps:

export function finalize_level() {
    // 1. Execute deferred placements (objects/monsters/traps)
    executeDeferredObjects();
    executeDeferredMonsters();
    executeDeferredTraps();

    // 2. Fill ordinary rooms with random content
    // This was MISSING - caused -93 fill_ordinary_room calls
    for (let i = 0; i < map.nroom; i++) {
        const croom = map.rooms[i];
        if (croom.rtype === OROOM && croom.needfill === FILL_NORMAL) {
            fill_ordinary_room(map, croom, depth, bonusItems);
        }
    }

    // 3. Wallification
    wallification(levelState.map);

    // 4. Topology finalization (bound_digging + mineralize)
    // This was MISSING - caused -922 mineralize calls
    bound_digging(levelState.map);
    mineralize(levelState.map, depth);

    return levelState.map;
}

3. Room needfill Initialization

Rooms created via des.room() need needfill=FILL_NORMAL:

// In create_room_splev() after calling dungeon.create_room:
const room = levelState.map.rooms[levelState.map.rooms.length - 1];
if (rtype === OROOM || rtype === THEMEROOM) {
    room.needfill = FILL_NORMAL;  // Required for fill_ordinary_room
}

RNG Alignment Results

Metric Before After Delta
Total RNG calls 2213 3416 +1203
Gap from C (2924) -711 +492 Improved
mineralize 0 1009 +1009 (C: 922)
fill_ordinary_room 0 75 +75 (C: 93)

Remaining Issues

  1. Room Count: Oracle creates 7 rooms, C expects 9 (missing 2 rooms/niches)
  2. Corridor Generation: Excessive finddpos/dig_corridor calls (JS=672/636 vs C=36/186)
  3. C-Specific Functions: Missing nhl_rn2, start_corpse_timeout, makeniche implementations

Key Learnings

Files Modified

2026-02-12 Regression Note