hayao 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +113 -24
  2. package/bin/create-hayao.mjs +380 -0
  3. package/bin/hayao-mcp-cli.mjs +11 -0
  4. package/dist/app/browser.d.ts +33 -2
  5. package/dist/app/game.d.ts +47 -2
  6. package/dist/app/tuning.d.ts +68 -0
  7. package/dist/art/palette.d.ts +41 -0
  8. package/dist/audio/adaptive.d.ts +58 -0
  9. package/dist/audio/album.d.ts +16 -0
  10. package/dist/audio/analysis.d.ts +59 -0
  11. package/dist/audio/audio.d.ts +21 -0
  12. package/dist/audio/chord.d.ts +17 -0
  13. package/dist/audio/genres.d.ts +11 -0
  14. package/dist/audio/lint.d.ts +29 -0
  15. package/dist/audio/match.d.ts +38 -0
  16. package/dist/audio/music.d.ts +88 -0
  17. package/dist/audio/pcm.d.ts +54 -0
  18. package/dist/audio/quality.d.ts +28 -0
  19. package/dist/audio/reverb.d.ts +15 -0
  20. package/dist/audio/synth.d.ts +56 -0
  21. package/dist/audio/theory.d.ts +64 -0
  22. package/dist/content/campaign.d.ts +69 -0
  23. package/dist/content/generate.d.ts +78 -0
  24. package/dist/content/level.d.ts +93 -0
  25. package/dist/content/worldgraph.d.ts +73 -0
  26. package/dist/core/dmath.d.ts +2 -0
  27. package/dist/hayao.global.js +15 -0
  28. package/dist/index.d.ts +30 -1
  29. package/dist/index.js +4293 -434
  30. package/dist/index.js.map +4 -4
  31. package/dist/index.min.js +15 -0
  32. package/dist/input/source.d.ts +52 -1
  33. package/dist/mcp.js +31225 -0
  34. package/dist/rasterize-worker-lite.mjs +13 -0
  35. package/dist/render/canvas.d.ts +6 -1
  36. package/dist/render/commands.d.ts +46 -0
  37. package/dist/render/paint.d.ts +33 -0
  38. package/dist/render/renderer.d.ts +22 -0
  39. package/dist/render/svg.d.ts +4 -1
  40. package/dist/render/svgString.d.ts +6 -2
  41. package/dist/scene/cameraController.d.ts +42 -0
  42. package/dist/scene/node.d.ts +17 -4
  43. package/dist/scene/nodes.d.ts +14 -0
  44. package/dist/scene/parallax.d.ts +15 -0
  45. package/dist/studio/mcpMain.d.ts +1 -0
  46. package/dist/studio/mcpServer.d.ts +2 -0
  47. package/dist/studio/record.d.ts +54 -0
  48. package/dist/studio/run.d.ts +78 -0
  49. package/dist/studio/session.d.ts +80 -0
  50. package/dist/studio/timeline.d.ts +35 -0
  51. package/dist/studio/vitePlugin.d.ts +6 -0
  52. package/dist/studio-plugin.js +228 -0
  53. package/dist/ui/overlay.d.ts +6 -0
  54. package/dist/verify/audioFilmstrip.d.ts +39 -0
  55. package/dist/verify/ethnography.d.ts +67 -0
  56. package/dist/verify/gates.d.ts +160 -0
  57. package/dist/verify/layout.d.ts +30 -3
  58. package/dist/verify/ramp.d.ts +40 -0
  59. package/dist/world.d.ts +38 -2
  60. package/dist-studio/assets/index-C7tty_Wo.js +109 -0
  61. package/dist-studio/assets/index-CM3tjRQo.css +1 -0
  62. package/dist-studio/index.html +18 -0
  63. package/docs/API.md +233 -9
  64. package/docs/CONVENTIONS.md +223 -0
  65. package/docs/EMBED.md +85 -0
  66. package/docs/QUICKSTART.md +129 -0
  67. package/docs/STUDIO.md +97 -0
  68. package/docs/VERIFICATION.md +226 -0
  69. package/package.json +39 -6
@@ -0,0 +1,56 @@
1
+ import { Rng } from '../core/rng';
2
+ import { type Samples } from './pcm';
3
+ export type Wave = 'sine' | 'triangle' | 'saw' | 'square' | 'noise';
4
+ /**
5
+ * A one-shot sound, fully described by data. Every field is optional; omitted
6
+ * fields take musical defaults. Times are seconds, pitch offsets are semitones,
7
+ * depths are 0..1 unless noted.
8
+ */
9
+ export interface SoundSpec {
10
+ /** Base pitch in Hz. */
11
+ freq?: number;
12
+ /** Oscillator shape. */
13
+ wave?: Wave;
14
+ /** Square/pulse duty cycle 0..1 (square only). */
15
+ duty?: number;
16
+ attack?: number;
17
+ decay?: number;
18
+ sustain?: number;
19
+ release?: number;
20
+ sustainLevel?: number;
21
+ /** Percussive volume spike at onset (jsfxr "punch"), 0..1. */
22
+ punch?: number;
23
+ /** Overall output gain. */
24
+ volume?: number;
25
+ slide?: number;
26
+ slideAccel?: number;
27
+ pitchJump?: number;
28
+ pitchJumpTime?: number;
29
+ vibrato?: number;
30
+ vibratoFreq?: number;
31
+ /** Second detuned oscillator, cents (0 = off). Adds unison warmth/chorus. */
32
+ detune?: number;
33
+ /** Sub-oscillator: a sine one octave down, mixed at this level 0..1. Adds weight. */
34
+ sub?: number;
35
+ noise?: number;
36
+ shapeCurve?: number;
37
+ fm?: number;
38
+ fmFreq?: number;
39
+ tremolo?: number;
40
+ tremoloFreq?: number;
41
+ bitCrush?: number;
42
+ lowpass?: number;
43
+ highpass?: number;
44
+ delay?: number;
45
+ delayFeedback?: number;
46
+ }
47
+ /**
48
+ * Render a SoundSpec to a mono signal. Pure and deterministic: pass a seeded
49
+ * `rng` for reproducible noise (defaults to a fixed seed so bare specs are
50
+ * still bit-stable). The returned buffer is normalized only by `volume`, so a
51
+ * caller can mix many at chosen gains.
52
+ */
53
+ export declare function renderSound(spec: SoundSpec, opts?: {
54
+ rng?: Rng;
55
+ sampleRate?: number;
56
+ }): Samples;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Parse scientific pitch notation ("C4", "F#3", "Bb5", "A4") to a MIDI number.
3
+ * Middle C (C4) = 60, A4 = 69. Throws on malformed input so bad authoring is
4
+ * caught, not silently mis-sounded.
5
+ */
6
+ export declare function noteToMidi(name: string): number;
7
+ /** MIDI number → name in sharps, e.g. 60 → "C4". */
8
+ export declare function midiToName(midi: number): string;
9
+ /** MIDI number → frequency in Hz (A4 = a4, default 440). Deterministic. */
10
+ export declare function midiToFreq(midi: number, a4?: number): number;
11
+ /** Convenience: pitch name or MIDI number → Hz. */
12
+ export declare function pitchToFreq(pitch: string | number, a4?: number): number;
13
+ /** Scale (mode) interval tables in semitones from the tonic. */
14
+ export declare const SCALES: {
15
+ readonly major: readonly [0, 2, 4, 5, 7, 9, 11];
16
+ readonly minor: readonly [0, 2, 3, 5, 7, 8, 10];
17
+ readonly harmonicMinor: readonly [0, 2, 3, 5, 7, 8, 11];
18
+ readonly melodicMinor: readonly [0, 2, 3, 5, 7, 9, 11];
19
+ readonly dorian: readonly [0, 2, 3, 5, 7, 9, 10];
20
+ readonly phrygian: readonly [0, 1, 3, 5, 7, 8, 10];
21
+ readonly lydian: readonly [0, 2, 4, 6, 7, 9, 11];
22
+ readonly mixolydian: readonly [0, 2, 4, 5, 7, 9, 10];
23
+ readonly locrian: readonly [0, 1, 3, 5, 6, 8, 10];
24
+ readonly majorPentatonic: readonly [0, 2, 4, 7, 9];
25
+ readonly minorPentatonic: readonly [0, 3, 5, 7, 10];
26
+ readonly blues: readonly [0, 3, 5, 6, 7, 10];
27
+ readonly chromatic: readonly [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
28
+ };
29
+ export type ScaleName = keyof typeof SCALES;
30
+ /**
31
+ * Scale degrees as MIDI numbers spanning `octaves` starting at `tonic`.
32
+ * e.g. scaleMidis(noteToMidi('C4'), 'major', 2) → C4 D4 E4 … up two octaves.
33
+ */
34
+ export declare function scaleMidis(tonic: number, mode?: ScaleName, octaves?: number): number[];
35
+ /** Pitch classes (0–11) of a scale — for in-key checks. */
36
+ export declare function scalePitchClasses(tonic: number, mode?: ScaleName): Set<number>;
37
+ /**
38
+ * Build a diatonic chord by stacking thirds on a scale degree. `degree` is
39
+ * 0-based (0 = I/i). Returns MIDI numbers. `size` 3 = triad, 4 = seventh.
40
+ * Because it stacks the actual scale, chord quality (maj/min/dim) falls out of
41
+ * the mode automatically — no quality table to get wrong.
42
+ */
43
+ export declare function diatonicChord(tonic: number, mode: ScaleName, degree: number, size?: number): number[];
44
+ /**
45
+ * Realize a roman-numeral progression in a key to concrete chords (arrays of
46
+ * MIDI notes). Case-insensitive on the numeral; a trailing "7" adds a seventh.
47
+ * e.g. progression(noteToMidi('C4'),'major',['I','V','vi','IV']).
48
+ */
49
+ export declare function progression(tonic: number, mode: ScaleName, romans: string[]): number[][];
50
+ /** Transpose a set of MIDI notes by semitones. */
51
+ export declare function transpose(notes: number[], semitones: number): number[];
52
+ /**
53
+ * Open a close-position chord: drop the root an octave so the voicing spreads
54
+ * out and the root/7th no longer clash in the same octave. Turns muddy stacked
55
+ * sevenths into something that breathes.
56
+ */
57
+ export declare function openVoicing(chord: number[]): number[];
58
+ /**
59
+ * Smooth a progression's voice-leading. Each chord after the first is octave-
60
+ * shifted so its notes sit near the previous chord's register — voices move by
61
+ * small steps instead of leaping in parallel root position. The single biggest
62
+ * cure for the "MIDI demo" sound. Returns new chords (MIDI, sorted low→high).
63
+ */
64
+ export declare function voiceLead(chords: number[][]): number[][];
@@ -0,0 +1,69 @@
1
+ import type { Rng } from '../core/rng';
2
+ import type { Puzzle } from '../verify/solver';
3
+ import type { SolveOptions } from '../verify/solver';
4
+ import { type DifficultyBand, type GeneratedLevel } from './generate';
5
+ /** One named stretch of the campaign, generated inside its own difficulty band. */
6
+ export interface ActSpec<State = unknown, Move = unknown> extends DifficultyBand {
7
+ /** Display name ("The Shallows", "Deep Roots", …). */
8
+ name: string;
9
+ /** How many verified levels this act contributes. */
10
+ count: number;
11
+ /**
12
+ * Optional per-act candidate factory — overrides the campaign default. This is
13
+ * how an act introduces a NEW mechanic or board size (a bigger grid, a new tile
14
+ * kind) while the earlier acts keep the simpler one: mechanic-gating as data.
15
+ */
16
+ factory?: (rng: Rng) => Puzzle<State, Move>;
17
+ /** Optional per-act solver budget override. */
18
+ solve?: SolveOptions;
19
+ /** Optional per-act attempt cap override. */
20
+ maxAttempts?: number;
21
+ }
22
+ export interface CampaignSpec<State, Move> {
23
+ /** Default candidate factory — used by any act that doesn't bring its own. */
24
+ factory?: (rng: Rng) => Puzzle<State, Move>;
25
+ /** Ordered acts; earlier acts should sit in lower difficulty bands. */
26
+ acts: ActSpec<State, Move>[];
27
+ /** Base seed for the whole campaign (default 1). */
28
+ seed?: number;
29
+ /** Default solver budget for acts that don't override it. */
30
+ solve?: SolveOptions;
31
+ /** Rough authoring estimate: minutes a median player spends per level (default 1.5). */
32
+ minutesPerLevel?: number;
33
+ }
34
+ /** A generated level tagged with the act it belongs to and its campaign index. */
35
+ export interface CampaignLevel<State, Move> extends GeneratedLevel<State, Move> {
36
+ /** Zero-based act index. */
37
+ act: number;
38
+ /** The act's display name. */
39
+ actName: string;
40
+ /** Position within its act. */
41
+ actIndex: number;
42
+ }
43
+ export interface ActBounds {
44
+ name: string;
45
+ /** First campaign index (inclusive). */
46
+ from: number;
47
+ /** Last campaign index (inclusive). */
48
+ to: number;
49
+ }
50
+ export interface Campaign<State, Move> {
51
+ levels: CampaignLevel<State, Move>[];
52
+ acts: ActBounds[];
53
+ /** Per-level difficulty (solver depth), campaign order — feed to `rampIssues`. */
54
+ difficulty: number[];
55
+ /** Reproduce the campaign: every level's sub-seed, in order. */
56
+ seeds: number[];
57
+ /** Honest length estimate in minutes (levels × minutesPerLevel). */
58
+ estMinutes: number;
59
+ }
60
+ /**
61
+ * Compose a full campaign by generating each act inside its band and concatenating
62
+ * them in order. Deterministic: same spec → same campaign everywhere. Acts are
63
+ * seeded independently (base seed mixed with the act index) so re-tuning one act's
64
+ * band or count never reshuffles the others.
65
+ *
66
+ * Throws (via `generateLevels`) if any act's band is too tight to fill — a loud,
67
+ * early failure beats silently shipping a short act.
68
+ */
69
+ export declare function composeCampaign<State, Move>(spec: CampaignSpec<State, Move>): Campaign<State, Move>;
@@ -0,0 +1,78 @@
1
+ import { Rng } from '../core/rng';
2
+ import { type Puzzle, type SolveOptions } from '../verify/solver';
3
+ /** A difficulty window expressed on the solver's own proof metrics. */
4
+ export interface DifficultyBand {
5
+ /** Minimum solution length (shortest winning move count). Below this = trivial. */
6
+ minDepth?: number;
7
+ /** Maximum solution length. Above this = a slog / likely unfair. */
8
+ maxDepth?: number;
9
+ /** Minimum nodes the solver expanded (a branchiness / "how much thinking" proxy). */
10
+ minNodes?: number;
11
+ /** Maximum nodes expanded — caps combinatorial monsters. */
12
+ maxNodes?: number;
13
+ }
14
+ export interface GenerateOptions<State, Move> extends DifficultyBand {
15
+ /** How many verified levels to return. */
16
+ count: number;
17
+ /** Base seed; the whole run is a pure function of it (default 1). */
18
+ seed?: number;
19
+ /** Cap on candidate attempts before giving up (default count * 60). */
20
+ maxAttempts?: number;
21
+ /** Search budget handed to the solver per candidate. */
22
+ solve?: SolveOptions;
23
+ /**
24
+ * Extra rejection hook — return true to discard an otherwise in-band candidate
25
+ * (e.g. a genre-specific "too few boxes" or "spawn adjacent to goal" rule).
26
+ */
27
+ reject?: (puzzle: Puzzle<State, Move>, result: AcceptedProof<Move>, subSeed: number) => boolean;
28
+ /**
29
+ * Canonical de-dup key for a candidate. Defaults to the puzzle's own initial
30
+ * state key, so two seeds that happen to build the same layout collapse to one.
31
+ */
32
+ dedupeKey?: (puzzle: Puzzle<State, Move>, result: AcceptedProof<Move>) => string;
33
+ }
34
+ /** The solver proof carried by an accepted candidate. */
35
+ export interface AcceptedProof<Move> {
36
+ /** Shortest winning move sequence (the existence proof). */
37
+ path: Move[];
38
+ /** Its length — the primary difficulty metric. */
39
+ depth: number;
40
+ /** Nodes the solver expanded to find it — a complexity proxy. */
41
+ nodes: number;
42
+ }
43
+ /** A generated, solver-verified level: reproducible from `seed`, ramped by `depth`. */
44
+ export interface GeneratedLevel<State, Move> extends AcceptedProof<Move> {
45
+ /** Position in the returned (difficulty-sorted) sequence. */
46
+ index: number;
47
+ /** The sub-seed that rebuilds this exact puzzle via `factory(new Rng(seed))`. */
48
+ seed: number;
49
+ /** The built puzzle instance (single-level: `initial()` returns this candidate). */
50
+ puzzle: Puzzle<State, Move>;
51
+ }
52
+ export interface GenerateReport<State, Move> {
53
+ levels: GeneratedLevel<State, Move>[];
54
+ /** Candidates tried. */
55
+ attempts: number;
56
+ /** How many were solvable at all (in-band or not). */
57
+ solvable: number;
58
+ /** True if `count` levels were found before `maxAttempts` ran out. */
59
+ complete: boolean;
60
+ /** Reproduce the whole run: the sub-seeds, in ramp order. */
61
+ seeds: number[];
62
+ }
63
+ /** Derive an independent, reproducible sub-seed for attempt `i` of a base run. */
64
+ export declare function subSeed(seed: number, i: number): number;
65
+ /**
66
+ * Generate `count` solver-verified levels inside a difficulty band. Builds
67
+ * candidates from `factory`, proves each with the BFS solver, and keeps the
68
+ * in-band, non-duplicate ones — sorted by solution depth so the returned
69
+ * sequence already ramps. Pure: same options → same levels, everywhere.
70
+ *
71
+ * A capped solver search (`exhausted`) is treated as "unknown, reject" — a level
72
+ * only ships if it was actually PROVEN winnable within budget.
73
+ */
74
+ export declare function generateLevelsReport<State, Move>(factory: (rng: Rng) => Puzzle<State, Move>, opts: GenerateOptions<State, Move>): GenerateReport<State, Move>;
75
+ /** Convenience: just the ramped levels. Throws if the band was too tight to fill. */
76
+ export declare function generateLevels<State, Move>(factory: (rng: Rng) => Puzzle<State, Move>, opts: GenerateOptions<State, Move>): GeneratedLevel<State, Move>[];
77
+ /** Rebuild the exact puzzle for a stored sub-seed — the campaign ships seeds, not maps. */
78
+ export declare function levelFromSeed<State, Move>(factory: (rng: Rng) => Puzzle<State, Move>, seed: number): Puzzle<State, Move>;
@@ -0,0 +1,93 @@
1
+ import { type TilemapData } from '../physics/tilemap';
2
+ import { type Cell, type Passable } from '../logic/graph';
3
+ export interface LevelEntity {
4
+ /** Entity kind, resolved from the legend (e.g. 'crystal', 'gust'). */
5
+ kind: string;
6
+ /** Tile coordinates. */
7
+ tx: number;
8
+ ty: number;
9
+ /** Center of the tile in design-space px. */
10
+ x: number;
11
+ y: number;
12
+ }
13
+ /** The authored input: rows + a legend. Everything else is derived. */
14
+ export interface LevelSpec {
15
+ name: string;
16
+ /** Tile edge in px (default 32). */
17
+ tileSize?: number;
18
+ /** ASCII rows, top to bottom. Ragged rows are padded with empty on the right. */
19
+ rows: string[];
20
+ /** Non-terrain marker char → entity kind. 'S'/'G' are reserved and need no legend. */
21
+ legend?: Record<string, string>;
22
+ }
23
+ /** The resolved, validated level — plain JSON, safe to hash/snapshot/diff. */
24
+ export interface LevelData extends Required<Omit<LevelSpec, 'legend'>> {
25
+ legend: Record<string, string>;
26
+ cols: number;
27
+ rowCount: number;
28
+ spawn: Cell;
29
+ goal: Cell;
30
+ entities: LevelEntity[];
31
+ }
32
+ /** Center of a tile in design-space px. */
33
+ export declare const tileCenter: (tx: number, ty: number, tileSize: number) => {
34
+ x: number;
35
+ y: number;
36
+ };
37
+ /**
38
+ * Resolve a LevelSpec into a full LevelData: find the spawn/goal markers, extract
39
+ * legend entities, and record dimensions. Throws only on the two structural sins
40
+ * that make a level meaningless (no spawn, no goal); everything else is surfaced
41
+ * as a soft issue by `levelIssues` so an agent can see ALL problems at once.
42
+ */
43
+ export declare function defineLevel(spec: LevelSpec): LevelData;
44
+ /**
45
+ * Validate a level's data. Returns human-readable issues (empty = clean), in the
46
+ * layoutIssues idiom. Catches unknown glyphs, spawn/goal buried in solid, and
47
+ * markers off the map — the authoring mistakes that produce a broken stage.
48
+ */
49
+ export declare function levelIssues(data: LevelData): string[];
50
+ /** Build the collision tilemap from the level's terrain (markers → empty). */
51
+ export declare function levelToTilemap(data: LevelData): TilemapData;
52
+ export interface ReachReport {
53
+ ok: boolean;
54
+ /** Labels of things the spawn cannot reach ('goal', or 'entity@tx,ty'). */
55
+ unreachable: string[];
56
+ /** Count of reachable cells/footholds (search size). */
57
+ reached: number;
58
+ }
59
+ /**
60
+ * Generic connectivity proof: flood-fill non-solid cells from the spawn and check
61
+ * the goal (and every entity) is reachable. Right for top-down / roguelike stages
62
+ * where movement is grid-walking. `passable` defaults to "not a solid tile".
63
+ */
64
+ export declare function levelReachable(data: LevelData, opts?: {
65
+ passable?: Passable;
66
+ diagonal?: boolean;
67
+ }): ReachReport;
68
+ export interface PlatformerReachOptions {
69
+ /** Max tiles the body can rise in one jump (derive from jumpHeight / tileSize). */
70
+ jumpTiles: number;
71
+ /** Max horizontal tiles cleared in a jump (derive from jumpDistance / tileSize). */
72
+ runTiles: number;
73
+ }
74
+ /**
75
+ * Platformer reachability: build the graph of FOOTHOLDS (a non-solid, non-hazard
76
+ * cell with solid/one-way ground directly below) and connect two footholds when a
77
+ * jump arc can carry the body between them — up to `jumpTiles` of rise, any drop,
78
+ * within `runTiles` horizontally. Then BFS from the spawn's foothold and confirm
79
+ * the goal's foothold is reachable. This is the honest "can you actually climb it?"
80
+ * proof a flat flood-fill can't give — it respects gravity and jump limits.
81
+ */
82
+ export declare function platformerReachable(data: LevelData, opts: PlatformerReachOptions): ReachReport;
83
+ export interface LevelChange {
84
+ kind: 'tile' | 'entity+' | 'entity-' | 'spawn' | 'goal';
85
+ detail: string;
86
+ }
87
+ /**
88
+ * Structural diff between two levels — the primitive that makes data-authored
89
+ * stages iterable: change three tiles, see exactly three changes, not a whole
90
+ * rewritten build() function. Compares terrain cell-by-cell, entities by position,
91
+ * and the spawn/goal markers.
92
+ */
93
+ export declare function diffLevels(a: LevelData, b: LevelData): LevelChange[];
@@ -0,0 +1,73 @@
1
+ import { type Puzzle, type SolveResult } from '../verify/solver';
2
+ /** A place in the world (a room or a named area). */
3
+ export interface WorldRegion {
4
+ id: string;
5
+ name?: string;
6
+ biome?: string;
7
+ }
8
+ /** A traversal between two regions, gated behind abilities. Bidirectional unless `oneWay`. */
9
+ export interface WorldEdge {
10
+ from: string;
11
+ to: string;
12
+ /** Ability ids ALL required to pass. Empty/undefined = open. */
13
+ requires?: string[];
14
+ /** If true, only `from → to` (a drop you can't climb back up). */
15
+ oneWay?: boolean;
16
+ }
17
+ /** A collectible that grants an ability, located in a region. */
18
+ export interface WorldPickup {
19
+ id: string;
20
+ region: string;
21
+ /** Ability id this pickup grants. */
22
+ grants: string;
23
+ }
24
+ /** The whole world as pure data. */
25
+ export interface WorldGraphDef {
26
+ regions: WorldRegion[];
27
+ edges: WorldEdge[];
28
+ pickups: WorldPickup[];
29
+ /** Starting region id. */
30
+ start: string;
31
+ /** Goal region id (the "heart" — where a minimal completion ends). */
32
+ goal: string;
33
+ }
34
+ /** A progression state: where you are + which pickups you've taken (sorted). */
35
+ export interface ProgressState {
36
+ region: string;
37
+ taken: string[];
38
+ }
39
+ export type ProgressMove = {
40
+ kind: 'move';
41
+ to: string;
42
+ } | {
43
+ kind: 'take';
44
+ pickup: string;
45
+ };
46
+ /** 'complete' = reach the goal; 'full' = reach the goal with 100% pickups. */
47
+ export type CompletionMode = 'complete' | 'full';
48
+ /** Ability ids granted by a set of taken pickup ids. */
49
+ export declare function abilitiesOf(world: WorldGraphDef, taken: readonly string[]): Set<string>;
50
+ /** Build the progression Puzzle for `solve()` to prove. */
51
+ export declare function progressionPuzzle(world: WorldGraphDef, mode?: CompletionMode): Puzzle<ProgressState, ProgressMove>;
52
+ /** Prove a minimal completion (reach the goal) exists. */
53
+ export declare function proveCompletable(world: WorldGraphDef): SolveResult<ProgressMove>;
54
+ /** Prove a 100% completion (all pickups + reach the goal) exists. */
55
+ export declare function proveFullCompletion(world: WorldGraphDef): SolveResult<ProgressMove>;
56
+ /** Regions reachable from the start with a FIXED ability set (no collecting). */
57
+ export declare function reachableRegions(world: WorldGraphDef, abilities: Iterable<string>): string[];
58
+ export interface SoftlockReport {
59
+ ok: boolean;
60
+ /** Reachable states (as keys) from which the goal can never be reached. */
61
+ deadEnds: string[];
62
+ /** How many progression states are reachable from the start (a size proxy). */
63
+ statesExplored: number;
64
+ }
65
+ /**
66
+ * Prove no softlock: enumerate every reachable progression state and confirm the
67
+ * goal stays reachable from each. Only one-way edges can create a dead end;
68
+ * with all-bidirectional edges a completable world is softlock-free by
69
+ * construction, but this checks it directly regardless.
70
+ */
71
+ export declare function findSoftlocks(world: WorldGraphDef, mode?: CompletionMode): SoftlockReport;
72
+ /** Structural problems a solver can't express (dangling ids, dupes, isolation). */
73
+ export declare function validateWorld(world: WorldGraphDef): string[];
@@ -18,3 +18,5 @@ export declare function dlog(x: number): number;
18
18
  export declare const dlog10: (x: number) => number;
19
19
  /** Deterministic base-2 logarithm. */
20
20
  export declare const dlog2: (x: number) => number;
21
+ /** Deterministic base^exp (bit-identical across JS engines). */
22
+ export declare function dpow(base: number, exp: number): number;