@fundamental-engine/core 0.8.0 → 0.9.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 (53) hide show
  1. package/dist/agents/event-agent.d.ts +4 -2
  2. package/dist/agents/event-agent.d.ts.map +1 -1
  3. package/dist/agents/event-agent.js +15 -6
  4. package/dist/agents/event-agent.js.map +1 -1
  5. package/dist/core/currents.d.ts +12 -0
  6. package/dist/core/currents.d.ts.map +1 -1
  7. package/dist/core/currents.js +23 -0
  8. package/dist/core/currents.js.map +1 -1
  9. package/dist/core/events.d.ts +22 -0
  10. package/dist/core/events.d.ts.map +1 -1
  11. package/dist/core/events.js +52 -0
  12. package/dist/core/events.js.map +1 -1
  13. package/dist/core/field.d.ts.map +1 -1
  14. package/dist/core/field.js +537 -78
  15. package/dist/core/field.js.map +1 -1
  16. package/dist/core/frame-harness.d.ts +109 -0
  17. package/dist/core/frame-harness.d.ts.map +1 -0
  18. package/dist/core/frame-harness.js +300 -0
  19. package/dist/core/frame-harness.js.map +1 -0
  20. package/dist/core/host-headless.d.ts +35 -0
  21. package/dist/core/host-headless.d.ts.map +1 -0
  22. package/dist/core/host-headless.js +47 -0
  23. package/dist/core/host-headless.js.map +1 -0
  24. package/dist/core/integrator.d.ts +6 -0
  25. package/dist/core/integrator.d.ts.map +1 -1
  26. package/dist/core/integrator.js +62 -12
  27. package/dist/core/integrator.js.map +1 -1
  28. package/dist/core/streamlines.d.ts.map +1 -1
  29. package/dist/core/streamlines.js +18 -2
  30. package/dist/core/streamlines.js.map +1 -1
  31. package/dist/core/types.d.ts +88 -0
  32. package/dist/core/types.d.ts.map +1 -1
  33. package/dist/core/types.js +10 -1
  34. package/dist/core/types.js.map +1 -1
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +5 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/record/index.d.ts +13 -0
  40. package/dist/record/index.d.ts.map +1 -0
  41. package/dist/record/index.js +13 -0
  42. package/dist/record/index.js.map +1 -0
  43. package/dist/record/record.d.ts +87 -0
  44. package/dist/record/record.d.ts.map +1 -0
  45. package/dist/record/record.js +172 -0
  46. package/dist/record/record.js.map +1 -0
  47. package/dist/record/rng.d.ts +15 -0
  48. package/dist/record/rng.d.ts.map +1 -0
  49. package/dist/record/rng.js +25 -0
  50. package/dist/record/rng.js.map +1 -0
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. package/package.json +1 -1
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Record / replay (BACKLOG tooling, #692) — capture a field run frame-by-frame and reproduce it
3
+ * deterministically for debugging and regression tests.
4
+ *
5
+ * The foundation rests on two existing seams:
6
+ * - the injectable randomness (`FieldOptions.rng` / #371) + wall clock (`now`), so a *seeded* run
7
+ * is fully reproducible — same seed, same stream, same trajectory;
8
+ * - the render-agnostic swarm read-out (`FieldHandle.readParticles`, stride {@link PARTICLE_STRIDE},
9
+ * wire version {@link PARTICLE_WIRE_VERSION}), the compact `[x, y, z, heat, size]` packing a
10
+ * renderer already consumes — so a recording is just that wire buffer captured every frame.
11
+ *
12
+ * A run is driven on a {@link headlessHost} (no DOM, manual `tick()`), so this is pure and headless —
13
+ * it runs in a test or a Node service with no browser. The recorder captures per-frame particle state
14
+ * into ONE flat `Float32Array` (frames laid end to end), plus the metadata needed to reproduce the run.
15
+ * The replayer takes that metadata, re-runs the same config + seed, and yields identical frames;
16
+ * {@link verifyReplay} proves it.
17
+ *
18
+ * Scope of this foundational slice (and what is deferred):
19
+ * - CAPTURED: per-frame particle positions, heat, and size via the readParticles wire format, plus
20
+ * the env seed (rng) and the deterministic field config. (Velocities are NOT stored — the wire
21
+ * format is position-based; positions are the verified invariant, and velocity is recoverable as a
22
+ * per-frame position delta.)
23
+ * - DEFERRED: an input timeline (burst/flowTo/setFormation calls interleaved with frames),
24
+ * programmatic bodies (`addBody`) in the recorded config, and an on-disk serialization format.
25
+ * The metadata + buffer here are the substrate those build on. See the module's `RecordedRun`.
26
+ */
27
+ import { createField } from "../core/field.js";
28
+ import { headlessHost } from "../core/host-headless.js";
29
+ import { PARTICLE_STRIDE, PARTICLE_WIRE_VERSION } from "../core/types.js";
30
+ import { seededRng } from "./rng.js";
31
+ /** Build the deterministic field for a config on a fresh headless host + seeded rng. Shared by record
32
+ * and replay so both drive byte-identical inputs. Returns the handle, the host, and a particle buffer
33
+ * big enough for the pool. */
34
+ function openRun(config) {
35
+ const host = headlessHost({ width: config.width, height: config.height });
36
+ const rng = seededRng(config.seed ?? 0);
37
+ // a deterministic wall clock that advances in lockstep with the headless tick (~1/60 s a frame), so
38
+ // wall-time-gated behaviour (the input-idle → ambient switch) is reproducible too — without it,
39
+ // `performance.now` would make two replays of the same config diverge once they cross an idle
40
+ // threshold at different real times. Starts at 0; the host's tick() drives the simulation clock,
41
+ // this drives the wall clock, and the seeded rng drives randomness — the three inputs a run needs.
42
+ let wall = 0;
43
+ const FRAME_MS = 1000 / 60;
44
+ const now = () => wall;
45
+ const field = createField(undefined, {
46
+ ...config.options,
47
+ host,
48
+ rng,
49
+ now,
50
+ render: 'none',
51
+ });
52
+ // expose the clock advance through the host's tick so callers drive both clocks with one call.
53
+ const baseTick = host.tick.bind(host);
54
+ host.tick = (t) => {
55
+ wall += FRAME_MS;
56
+ baseTick(t);
57
+ };
58
+ // size the scratch buffer to the live pool (it never grows mid-run beyond the seeded ceiling; a
59
+ // generous headroom covers source-spawned matter). readParticles writes min(count, capacity).
60
+ const cap = Math.max(field.particleCount() * 4, 1024);
61
+ return { field, host, scratch: new Float32Array(cap * PARTICLE_STRIDE) };
62
+ }
63
+ /**
64
+ * Record a deterministic field run: build the field, tick it `frames` times on a headless host, and
65
+ * capture each frame's particle state via `readParticles`. Pure given the config (seeded rng + manual
66
+ * clock). Returns a compact {@link RecordedRun}.
67
+ */
68
+ export function recordRun(config) {
69
+ const { field, host, scratch } = openRun(config);
70
+ try {
71
+ const counts = [];
72
+ const perFrame = [];
73
+ let maxCount = 0;
74
+ for (let f = 0; f < config.frames; f++) {
75
+ host.tick();
76
+ const n = field.readParticles(scratch);
77
+ counts.push(n);
78
+ if (n > maxCount)
79
+ maxCount = n;
80
+ perFrame.push(scratch.slice(0, n * PARTICLE_STRIDE));
81
+ }
82
+ // pack the per-frame slices into one contiguous buffer with a uniform per-frame slot width.
83
+ const slot = maxCount * PARTICLE_STRIDE;
84
+ const particles = new Float32Array(config.frames * slot);
85
+ for (let f = 0; f < config.frames; f++) {
86
+ particles.set(perFrame[f], f * slot);
87
+ }
88
+ return {
89
+ wireVersion: PARTICLE_WIRE_VERSION,
90
+ stride: PARTICLE_STRIDE,
91
+ config,
92
+ frames: config.frames,
93
+ counts,
94
+ maxCount,
95
+ particles,
96
+ };
97
+ }
98
+ finally {
99
+ field.destroy();
100
+ }
101
+ }
102
+ /**
103
+ * Replay a recorded run: re-run the SAME config + seed and reproduce the per-frame particle state. The
104
+ * result is a fresh {@link RecordedRun} — deterministically identical to the original on the same build.
105
+ * Pass the original's `config` (or the whole `RecordedRun`); only the config drives the simulation.
106
+ */
107
+ export function replayRun(run) {
108
+ const config = 'particles' in run ? run.config : run;
109
+ return recordRun(config);
110
+ }
111
+ /**
112
+ * Verify a replay reproduces a recording: compare every captured value frame by frame. Deterministic
113
+ * replay on the same build should be bit-identical (default `tol` 0); a small `tol` absorbs
114
+ * cross-platform float drift if a consumer compares runs from different machines.
115
+ */
116
+ export function verifyReplay(expected, actual, tol = 0) {
117
+ if (expected.wireVersion !== actual.wireVersion) {
118
+ return {
119
+ ok: false,
120
+ maxDelta: Infinity,
121
+ mismatch: { frame: 0, particle: -1, reason: `wire version ${expected.wireVersion} → ${actual.wireVersion}` },
122
+ };
123
+ }
124
+ if (expected.frames !== actual.frames) {
125
+ return {
126
+ ok: false,
127
+ maxDelta: Infinity,
128
+ mismatch: { frame: Math.min(expected.frames, actual.frames), particle: -1, reason: `frame count ${expected.frames} → ${actual.frames}` },
129
+ };
130
+ }
131
+ const stride = expected.stride;
132
+ let maxDelta = 0;
133
+ for (let f = 0; f < expected.frames; f++) {
134
+ const ec = expected.counts[f] ?? 0;
135
+ const ac = actual.counts[f] ?? 0;
136
+ if (ec !== ac) {
137
+ return { ok: false, maxDelta, mismatch: { frame: f, particle: -1, reason: `particle count ${ec} → ${ac}` } };
138
+ }
139
+ const eSlot = f * expected.maxCount * stride;
140
+ const aSlot = f * actual.maxCount * stride;
141
+ for (let i = 0; i < ec; i++) {
142
+ for (let k = 0; k < stride; k++) {
143
+ const ev = expected.particles[eSlot + i * stride + k];
144
+ const av = actual.particles[aSlot + i * stride + k];
145
+ const d = Math.abs(ev - av);
146
+ if (d > maxDelta)
147
+ maxDelta = d;
148
+ if (d > tol) {
149
+ return {
150
+ ok: false,
151
+ maxDelta,
152
+ mismatch: { frame: f, particle: i, reason: `value gap ${d} at channel ${k} (${ev} vs ${av})` },
153
+ };
154
+ }
155
+ }
156
+ }
157
+ }
158
+ return { ok: true, maxDelta, mismatch: null };
159
+ }
160
+ /**
161
+ * Read one frame's particle state back out of a {@link RecordedRun} as a view onto the packed buffer —
162
+ * a `subarray` of length `counts[frame] * stride`, laid out `[x, y, z, heat, size]`. Zero-copy; do not
163
+ * mutate. Out-of-range frames return an empty view.
164
+ */
165
+ export function frameAt(run, frame) {
166
+ if (frame < 0 || frame >= run.frames)
167
+ return new Float32Array(0);
168
+ const slot = run.maxCount * run.stride;
169
+ const start = frame * slot;
170
+ return run.particles.subarray(start, start + (run.counts[frame] ?? 0) * run.stride);
171
+ }
172
+ //# sourceMappingURL=record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.js","sourceRoot":"","sources":["../../src/record/record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAE1E,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAoDrC;;+BAE+B;AAC/B,SAAS,OAAO,CAAC,MAAoB;IAKnC,MAAM,IAAI,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1E,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC;IACxC,oGAAoG;IACpG,gGAAgG;IAChG,8FAA8F;IAC9F,iGAAiG;IACjG,mGAAmG;IACnG,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAE,CAAC;IAC3B,MAAM,GAAG,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC;IAC/B,MAAM,KAAK,GAAG,WAAW,CAAC,SAAyC,EAAE;QACnE,GAAG,MAAM,CAAC,OAAO;QACjB,IAAI;QACJ,GAAG;QACH,GAAG;QACH,MAAM,EAAE,MAAM;KACf,CAAC,CAAC;IACH,+FAA+F;IAC/F,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAU,EAAQ,EAAE;QAC/B,IAAI,IAAI,QAAQ,CAAC;QACjB,QAAQ,CAAC,CAAC,CAAC,CAAC;IACd,CAAC,CAAC;IACF,gGAAgG;IAChG,8FAA8F;IAC9F,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,aAAa,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;IACtD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,YAAY,CAAC,GAAG,GAAG,eAAe,CAAC,EAAE,CAAC;AAC3E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,MAAoB;IAC5C,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,IAAI,CAAC;QACH,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,MAAM,QAAQ,GAAmB,EAAE,CAAC;QACpC,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,MAAM,CAAC,GAAG,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YACvC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACf,IAAI,CAAC,GAAG,QAAQ;gBAAE,QAAQ,GAAG,CAAC,CAAC;YAC/B,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC;QACvD,CAAC;QACD,4FAA4F;QAC5F,MAAM,IAAI,GAAG,QAAQ,GAAG,eAAe,CAAC;QACxC,MAAM,SAAS,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;QACzD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAE,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;QACxC,CAAC;QACD,OAAO;YACL,WAAW,EAAE,qBAAqB;YAClC,MAAM,EAAE,eAAe;YACvB,MAAM;YACN,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,MAAM;YACN,QAAQ;YACR,SAAS;SACV,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,KAAK,CAAC,OAAO,EAAE,CAAC;IAClB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,GAA+B;IACvD,MAAM,MAAM,GAAG,WAAW,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC;IACrD,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC;AAC3B,CAAC;AAqBD;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,QAAqB,EAAE,MAAmB,EAAE,GAAG,GAAG,CAAC;IAC9E,IAAI,QAAQ,CAAC,WAAW,KAAK,MAAM,CAAC,WAAW,EAAE,CAAC;QAChD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,gBAAgB,QAAQ,CAAC,WAAW,MAAM,MAAM,CAAC,WAAW,EAAE,EAAE;SAC7G,CAAC;IACJ,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC;QACtC,OAAO;YACL,EAAE,EAAE,KAAK;YACT,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,eAAe,QAAQ,CAAC,MAAM,MAAM,MAAM,CAAC,MAAM,EAAE,EAAE;SACzI,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IAC/B,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACd,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC;QAC/G,CAAC;QACD,MAAM,KAAK,GAAG,CAAC,GAAG,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAAC;QAC7C,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC;QAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAChC,MAAM,EAAE,GAAG,QAAQ,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,CAAE,CAAC;gBACvD,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,CAAE,CAAC;gBACrD,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;gBAC5B,IAAI,CAAC,GAAG,QAAQ;oBAAE,QAAQ,GAAG,CAAC,CAAC;gBAC/B,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC;oBACZ,OAAO;wBACL,EAAE,EAAE,KAAK;wBACT,QAAQ;wBACR,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,aAAa,CAAC,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE;qBAC/F,CAAC;gBACJ,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAChD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,GAAgB,EAAE,KAAa;IACrD,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,IAAI,GAAG,CAAC,MAAM;QAAE,OAAO,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IACjE,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC;IACvC,MAAM,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC;IAC3B,OAAO,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;AACtF,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A small, dependency-free seeded PRNG for the record/replay seam.
3
+ *
4
+ * The engine takes its randomness through an injectable `rng: () => number` (FieldOptions.rng / #371),
5
+ * so a deterministic generator is the single thing a recorder needs to make a run reproducible. This is
6
+ * `mulberry32` — a well-known 32-bit generator: tiny, fast, and bit-identical across JS engines (pure
7
+ * integer math, no `Math.random`), so a recording seeded here on one machine replays identically on
8
+ * another. The same seed always produces the same stream.
9
+ *
10
+ * This is NOT a cryptographic generator; it exists purely for reproducible simulation, never for
11
+ * security. Pure: a given `seed` fully determines the sequence.
12
+ */
13
+ /** Create a seeded `() => number` (uniform in [0, 1)) from a 32-bit integer seed. */
14
+ export declare function seededRng(seed: number): () => number;
15
+ //# sourceMappingURL=rng.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../../src/record/rng.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,qFAAqF;AACrF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,MAAM,CAUpD"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A small, dependency-free seeded PRNG for the record/replay seam.
3
+ *
4
+ * The engine takes its randomness through an injectable `rng: () => number` (FieldOptions.rng / #371),
5
+ * so a deterministic generator is the single thing a recorder needs to make a run reproducible. This is
6
+ * `mulberry32` — a well-known 32-bit generator: tiny, fast, and bit-identical across JS engines (pure
7
+ * integer math, no `Math.random`), so a recording seeded here on one machine replays identically on
8
+ * another. The same seed always produces the same stream.
9
+ *
10
+ * This is NOT a cryptographic generator; it exists purely for reproducible simulation, never for
11
+ * security. Pure: a given `seed` fully determines the sequence.
12
+ */
13
+ /** Create a seeded `() => number` (uniform in [0, 1)) from a 32-bit integer seed. */
14
+ export function seededRng(seed) {
15
+ // coerce to a 32-bit unsigned integer so any number (or 0) gives a valid, stable starting state.
16
+ let a = seed >>> 0;
17
+ return function next() {
18
+ a |= 0;
19
+ a = (a + 0x6d2b79f5) | 0;
20
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
21
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
22
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
23
+ };
24
+ }
25
+ //# sourceMappingURL=rng.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rng.js","sourceRoot":"","sources":["../../src/record/rng.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,qFAAqF;AACrF,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,iGAAiG;IACjG,IAAI,CAAC,GAAG,IAAI,KAAK,CAAC,CAAC;IACnB,OAAO,SAAS,IAAI;QAClB,CAAC,IAAI,CAAC,CAAC;QACP,CAAC,GAAG,CAAC,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACzC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC/C,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,UAAU,CAAC;IAC/C,CAAC,CAAC;AACJ,CAAC"}
package/dist/version.d.ts CHANGED
@@ -4,5 +4,5 @@
4
4
  * with `packages/core/package.json` by `version.test.ts` (the build fails if they drift), so the
5
5
  * release bump is the single source of truth.
6
6
  */
7
- export declare const FIELD_VERSION = "0.8.0";
7
+ export declare const FIELD_VERSION = "0.9.0";
8
8
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -4,5 +4,5 @@
4
4
  * with `packages/core/package.json` by `version.test.ts` (the build fails if they drift), so the
5
5
  * release bump is the single source of truth.
6
6
  */
7
- export const FIELD_VERSION = '0.8.0';
7
+ export const FIELD_VERSION = '0.9.0';
8
8
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fundamental-engine/core",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "A reciprocal DOM-physics field — elements bend the field; the field's density bends them back.",
5
5
  "type": "module",
6
6
  "license": "MIT",