/** * mapMetrics.ts — measurable properties of a map, as opposed to opinions. * * critique.ts answers "is this good?" with designer heuristics. This module * answers "what is actually true about this layout?" with numbers that come * from the procedural-generation literature and can be recomputed, compared * across maps, and regression-tested: * * - reachability flood fill from the player's entry point; anything the * player cannot walk to is a softlock waiting to happen * - dead space how much of the rectangle is not accessible playable area * - shape the walkable area thinned to a one-cell skeleton, then read * as a graph: endpoints, junctions, cycles, critical path. * Linearity near 1 is a corridor; near 0 is a wide open blob * - variety Shannon entropy over 5x5 tile windows — low entropy across * the board is the monotonous-floor problem, measured * - tension for maps with random encounters, how far the player is from * somewhere safe * * Everything is pure and synchronous: give it a map, passage flags and the * event list, and it hands back numbers. No I/O, no dependencies. */ import { type PlaceableMap } from '../utils/placement.js'; export interface MetricEvent { id: number; name: string; x: number; y: number; /** True when the event is a shop, inn, save point or healing spot. */ safe?: boolean; } export interface MapMetricsOptions { /** Where the player enters. Defaults to the largest walkable region. */ entry?: { x: number; y: number; } | null; /** Random encounters defined on the map, used for the tension metric. */ encounterCount?: number; /** What kind of space this is meant to be, which sets the dead-space band. */ expected?: 'interior' | 'dungeon' | 'exterior'; } export interface MetricVerdict { metric: string; value: number; band: 'ok' | 'low' | 'high' | 'critical'; message: string; } export interface MapMetrics { mapId?: number; passability: 'flags' | 'none'; size: { width: number; height: number; totalTiles: number; }; space: { walkableTiles: number; accessibleTiles: number; deadSpaceRatio: number; regions: number; largestRegionShare: number; }; reachability: { entry: { x: number; y: number; } | null; strandedTiles: number; unreachableEvents: { id: number; name: string; x: number; y: number; }[]; }; shape: { skeletonCells: number; endpoints: number; junctions: number; cycles: number; criticalPathLength: number; linearity: number; }; variety: { distinctTiles: number; meanEntropy: number; minEntropy: number; monotonousWindowPct: number; }; tension: { hasEncounters: boolean; safePoints: number; meanStepsToSafety: number; maxStepsToSafety: number; } | null; verdicts: MetricVerdict[]; } /** Shannon entropy in bits of a multiset of values. */ export declare function shannonEntropy(values: number[]): number; /** * Zhang-Suen thinning: erode a binary mask to a one-cell-wide skeleton while * preserving connectivity. This is what turns "a big walkable blob" into * something a graph metric can be read off. * * The mask is row-major, `true` meaning foreground. The result is a new mask. */ export declare function thinZhangSuen(mask: boolean[], width: number, height: number): boolean[]; /** * Measure one map. `flags` is the tileset's passage array; without it the space * and reachability metrics cannot be computed and are reported as zero with * `passability: "none"`. */ export declare function computeMapMetrics(map: PlaceableMap, flags: number[] | null, events: MetricEvent[], opts?: MapMetricsOptions, mapId?: number): MapMetrics;