koota-kit 0.2.1 → 0.3.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1](https://github.com/jbcom/koota-kit/compare/v0.3.0...v0.3.1) (2026-10-07)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * declare maintained Node support and align OSS protection gates ([20f6aa3](https://github.com/jbcom/koota-kit/commit/20f6aa37b971138d3d7fbca54152f44b99d1f2d8))
9
+ * declare the supported Node lines and test each in CI ([94ed880](https://github.com/jbcom/koota-kit/commit/94ed8803242a43c055a9ae9333327b0856109bb5))
10
+
11
+ ## [0.3.0](https://github.com/jbcom/koota-kit/compare/v0.2.1...v0.3.0) (2026-10-06)
12
+
13
+
14
+ ### Features
15
+
16
+ * add named RNG substreams, seed derivation, and master seeds ([a120cb7](https://github.com/jbcom/koota-kit/commit/a120cb7209eac1d16b964c6f3af2d97061cfcfc3))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * type EntropySource so Web Crypto implementations satisfy it ([7b8d3cb](https://github.com/jbcom/koota-kit/commit/7b8d3cb3ee8a5a994b1578f3000270d40b5295f9))
22
+
3
23
  ## [0.2.1](https://github.com/jbcom/koota-kit/compare/v0.2.0...v0.2.1) (2026-10-06)
4
24
 
5
25
 
package/CONTRIBUTING.md CHANGED
@@ -5,7 +5,7 @@ Thanks for taking the time to contribute.
5
5
  ## Getting set up
6
6
 
7
7
  With [mise](https://mise.jdx.dev) (recommended — installs the Node and pnpm
8
- versions pinned in `mise.toml`):
8
+ toolchain selected in `mise.toml`; Node selects a major, not an exact patch):
9
9
 
10
10
  ```sh
11
11
  mise install
package/README.md CHANGED
@@ -34,7 +34,9 @@ pnpm add koota-kit koota
34
34
 
35
35
  Requirements:
36
36
 
37
- - Node.js 22 or newer (CI covers Node 22/24/26 on Linux and Node 24 on Windows)
37
+ - Node.js 22, 24 and 26 (CI covers each on Linux and Node 24 on Windows).
38
+ The package declares `>=22`; support follows maintained Node lines rather
39
+ than requiring one exact patch release.
38
40
  - Koota `^0.6.6`, installed by the application as a peer dependency
39
41
 
40
42
  The package ships native ESM and CommonJS entry points with format-correct
@@ -106,15 +108,20 @@ stream in query iteration order. Entity IDs contain process-global world and
106
108
  generation bits. Derive a local stream from a stable domain key instead:
107
109
 
108
110
  ```ts
109
- import { createRng, nextU32 } from "koota-kit/rng";
111
+ import { nextU32, substream } from "koota-kit/rng";
112
+ import { deriveSeed } from "koota-kit/seed";
110
113
 
111
- const tileRng = createRng({
112
- gen: `${String(sim.seeds.gen)}/tile:12,8`,
113
- events: 0,
114
- });
115
- const terrainVariant = nextU32(tileRng.gen);
114
+ const tileSeed = deriveSeed(sim.seeds.gen, "tile", 12, 8);
115
+ const terrainVariant = nextU32(substream(tileSeed, "terrain"));
116
116
  ```
117
117
 
118
+ `deriveSeed` hashes length-prefixed parts into a 128-bit hex seed, so seed
119
+ chains such as master seed → run → level never collide by accident.
120
+ `substream(seed, name)` gives each generator its own stream; adding a new
121
+ named substream never shifts an existing one. `createMasterSeed()` makes a
122
+ 128-bit root seed from `crypto.getRandomValues`, and accepts an injected
123
+ entropy source for tests.
124
+
118
125
  ### Object-valued traits need factories
119
126
 
120
127
  Koota's structure-of-arrays layout can alias a bare object literal across
@@ -154,7 +161,8 @@ The root package exports the full API. Focused entry points are also available:
154
161
  | Entry point | Purpose |
155
162
  | --- | --- |
156
163
  | `koota-kit/world` | World lifecycle, clock, Koota facade, world snapshots |
157
- | `koota-kit/rng` | Seeded streams, bounded draws, snapshots |
164
+ | `koota-kit/rng` | Seeded streams, named substreams, bounded draws, snapshots |
165
+ | `koota-kit/seed` | Seed derivation and 128-bit master seeds |
158
166
  | `koota-kit/traits` | Safe trait declaration |
159
167
  | `koota-kit/eventLog` | World-scoped publish/drain logs |
160
168
 
@@ -169,7 +177,10 @@ For task-oriented usage, read the Sourcey guides on
169
177
 
170
178
  ## Errors and edge cases
171
179
 
172
- - Seeds must be strings or finite numbers.
180
+ - Seeds must be strings or finite numbers. Substream names must be non-empty
181
+ strings.
182
+ - `createMasterSeed` throws when no entropy source exists or the source leaves
183
+ the buffer all zero.
173
184
  - `advanceClock` accepts finite, non-negative `dt`; `0` is a valid paused tick.
174
185
  - `nextInt` requires safe-integer bounds with `maxExclusive > minInclusive` and
175
186
  a range no larger than `2**32`.
@@ -1,16 +1,18 @@
1
1
  "use strict";
2
2
  // koota-kit — thin koota ECS conventions layer.
3
3
  //
4
- // Four modules, also importable as subpaths:
4
+ // Five modules, also importable as subpaths:
5
5
  // ./world — WorldHandle lifecycle (world + dual-stream rng + clock + scratch)
6
- // ./rng — dual-layer deterministic seedrandom PRNG with byte-exact
7
- // snapshot/restore
6
+ // ./rng — dual-layer deterministic seedrandom PRNG and named substreams
7
+ // with byte-exact snapshot/restore
8
+ // ./seed — length-prefixed SHA-256 seed derivation and 128-bit master
9
+ // seeds (leaf module, zero runtime dependencies)
8
10
  // ./traits — defineTrait AoS-aliasing guard (leaf module, zero sibling
9
11
  // imports)
10
12
  // ./eventLog — scratch-backed publish/drain event logs with a peek seam for
11
13
  // observers (harnesses, tests, HUDs)
12
14
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.defineTrait = exports.defineEventLog = exports.snapshotStream = exports.snapshotLayers = exports.restoreStream = exports.restoreLayers = exports.nextU32 = exports.nextInt = exports.nextFloat = exports.createRng = exports.chance = exports.trait = exports.snapshotWorld = exports.restoreWorldHeader = exports.relation = exports.destroySimWorld = exports.createSimWorld = exports.createActions = exports.advanceClock = void 0;
15
+ exports.defineTrait = exports.defineEventLog = exports.MASTER_SEED_BYTES = exports.isMasterSeed = exports.deriveSeed = exports.createMasterSeed = exports.substream = exports.snapshotSubstreams = exports.snapshotStream = exports.snapshotLayers = exports.restoreSubstreams = exports.restoreStream = exports.restoreLayers = exports.nextU32 = exports.nextInt = exports.nextFloat = exports.getSubstream = exports.createSubstreams = exports.createRng = exports.chance = exports.trait = exports.snapshotWorld = exports.restoreWorldHeader = exports.relation = exports.destroySimWorld = exports.createSimWorld = exports.createActions = exports.advanceClock = void 0;
14
16
  var world_js_1 = require("./world.cjs");
15
17
  Object.defineProperty(exports, "advanceClock", { enumerable: true, get: function () { return world_js_1.advanceClock; } });
16
18
  Object.defineProperty(exports, "createActions", { enumerable: true, get: function () { return world_js_1.createActions; } });
@@ -23,13 +25,23 @@ Object.defineProperty(exports, "trait", { enumerable: true, get: function () { r
23
25
  var rng_js_1 = require("./rng.cjs");
24
26
  Object.defineProperty(exports, "chance", { enumerable: true, get: function () { return rng_js_1.chance; } });
25
27
  Object.defineProperty(exports, "createRng", { enumerable: true, get: function () { return rng_js_1.createRng; } });
28
+ Object.defineProperty(exports, "createSubstreams", { enumerable: true, get: function () { return rng_js_1.createSubstreams; } });
29
+ Object.defineProperty(exports, "getSubstream", { enumerable: true, get: function () { return rng_js_1.getSubstream; } });
26
30
  Object.defineProperty(exports, "nextFloat", { enumerable: true, get: function () { return rng_js_1.nextFloat; } });
27
31
  Object.defineProperty(exports, "nextInt", { enumerable: true, get: function () { return rng_js_1.nextInt; } });
28
32
  Object.defineProperty(exports, "nextU32", { enumerable: true, get: function () { return rng_js_1.nextU32; } });
29
33
  Object.defineProperty(exports, "restoreLayers", { enumerable: true, get: function () { return rng_js_1.restoreLayers; } });
30
34
  Object.defineProperty(exports, "restoreStream", { enumerable: true, get: function () { return rng_js_1.restoreStream; } });
35
+ Object.defineProperty(exports, "restoreSubstreams", { enumerable: true, get: function () { return rng_js_1.restoreSubstreams; } });
31
36
  Object.defineProperty(exports, "snapshotLayers", { enumerable: true, get: function () { return rng_js_1.snapshotLayers; } });
32
37
  Object.defineProperty(exports, "snapshotStream", { enumerable: true, get: function () { return rng_js_1.snapshotStream; } });
38
+ Object.defineProperty(exports, "snapshotSubstreams", { enumerable: true, get: function () { return rng_js_1.snapshotSubstreams; } });
39
+ Object.defineProperty(exports, "substream", { enumerable: true, get: function () { return rng_js_1.substream; } });
40
+ var seed_js_1 = require("./seed.cjs");
41
+ Object.defineProperty(exports, "createMasterSeed", { enumerable: true, get: function () { return seed_js_1.createMasterSeed; } });
42
+ Object.defineProperty(exports, "deriveSeed", { enumerable: true, get: function () { return seed_js_1.deriveSeed; } });
43
+ Object.defineProperty(exports, "isMasterSeed", { enumerable: true, get: function () { return seed_js_1.isMasterSeed; } });
44
+ Object.defineProperty(exports, "MASTER_SEED_BYTES", { enumerable: true, get: function () { return seed_js_1.MASTER_SEED_BYTES; } });
33
45
  var eventLog_js_1 = require("./eventLog.cjs");
34
46
  Object.defineProperty(exports, "defineEventLog", { enumerable: true, get: function () { return eventLog_js_1.defineEventLog; } });
35
47
  var index_js_1 = require("./traits/index.cjs");
@@ -1,7 +1,9 @@
1
1
  export { advanceClock, createActions, createSimWorld, destroySimWorld, relation, restoreWorldHeader, snapshotWorld, trait, } from "./world.cjs";
2
2
  export type { Entity, World, WorldHandle, WorldSnapshot } from "./world.cjs";
3
- export { chance, createRng, nextFloat, nextInt, nextU32, restoreLayers, restoreStream, snapshotLayers, snapshotStream, } from "./rng.cjs";
4
- export type { RngLayers, RngLayersSnapshot, RngSeeds, RngStream, RngStreamSnapshot, } from "./rng.cjs";
3
+ export { chance, createRng, createSubstreams, getSubstream, nextFloat, nextInt, nextU32, restoreLayers, restoreStream, restoreSubstreams, snapshotLayers, snapshotStream, snapshotSubstreams, substream, } from "./rng.cjs";
4
+ export type { RngLayers, RngLayersSnapshot, RngSeeds, RngStream, RngStreamSnapshot, Substreams, SubstreamsSnapshot, } from "./rng.cjs";
5
+ export { createMasterSeed, deriveSeed, isMasterSeed, MASTER_SEED_BYTES } from "./seed.cjs";
6
+ export type { EntropySource, SeedPart } from "./seed.cjs";
5
7
  export { defineEventLog } from "./eventLog.cjs";
6
8
  export type { EventLog } from "./eventLog.cjs";
7
9
  export { defineTrait } from "./traits/index.cjs";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,SAAS,EACT,OAAO,EACP,OAAO,EACP,aAAa,EACb,aAAa,EACb,cAAc,EACd,cAAc,GACf,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,GAClB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAO,EACP,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/cjs/rng.cjs CHANGED
@@ -32,7 +32,13 @@ exports.snapshotStream = snapshotStream;
32
32
  exports.snapshotLayers = snapshotLayers;
33
33
  exports.restoreStream = restoreStream;
34
34
  exports.restoreLayers = restoreLayers;
35
+ exports.substream = substream;
36
+ exports.createSubstreams = createSubstreams;
37
+ exports.getSubstream = getSubstream;
38
+ exports.snapshotSubstreams = snapshotSubstreams;
39
+ exports.restoreSubstreams = restoreSubstreams;
35
40
  const seedrandom_1 = __importDefault(require("seedrandom"));
41
+ const seed_js_1 = require("./seed.cjs");
36
42
  function assertSeed(seed, name) {
37
43
  if (typeof seed !== "string" && typeof seed !== "number") {
38
44
  throw new TypeError(`createRng: seeds.${name} must be a string or finite number.`);
@@ -166,3 +172,77 @@ function restoreLayers(snap) {
166
172
  }
167
173
  return { gen: restoreStream(snap.gen), events: restoreStream(snap.events) };
168
174
  }
175
+ function assertSubstreamName(name, api) {
176
+ if (typeof name !== "string" || name.length === 0) {
177
+ throw new TypeError(`${api}: name must be a non-empty string.`);
178
+ }
179
+ }
180
+ function assertSubstreamSeed(seed, api) {
181
+ if (typeof seed !== "string" && !(typeof seed === "number" && Number.isFinite(seed))) {
182
+ throw new TypeError(`${api}: seed must be a string or finite number.`);
183
+ }
184
+ }
185
+ /**
186
+ * Create the stream named `name` under `seed`: a fresh stream seeded by
187
+ * `deriveSeed(seed, name)`. Creating or consuming any other substream never
188
+ * shifts this one.
189
+ */
190
+ function substream(seed, name) {
191
+ assertSubstreamName(name, "substream");
192
+ return makeStream((0, seed_js_1.deriveSeed)(seed, name));
193
+ }
194
+ /** Create an empty, lazily populated set of named substreams under `seed`. */
195
+ function createSubstreams(seed) {
196
+ assertSubstreamSeed(seed, "createSubstreams");
197
+ return { seed, streams: new Map() };
198
+ }
199
+ /**
200
+ * Return the set's stream for `name`, creating it from the set's seed on first
201
+ * use. The same name always returns the same live stream instance.
202
+ */
203
+ function getSubstream(substreams, name) {
204
+ assertSubstreamName(name, "getSubstream");
205
+ const existing = substreams.streams.get(name);
206
+ if (existing)
207
+ return existing;
208
+ const stream = substream(substreams.seed, name);
209
+ substreams.streams.set(name, stream);
210
+ return stream;
211
+ }
212
+ /**
213
+ * Capture the set's seed plus every stream created so far, with names in
214
+ * sorted order. JSON-serializable and detached from later draws.
215
+ */
216
+ function snapshotSubstreams(substreams) {
217
+ // Object.fromEntries defines own data properties, so a stream named
218
+ // "__proto__" is captured instead of hitting the prototype setter.
219
+ const streams = Object.fromEntries([...substreams.streams.keys()]
220
+ // UTF-16 code-unit order, never localeCompare: the order must not vary
221
+ // by runtime locale. Branch-free because names in a Map are unique.
222
+ .sort((left, right) => Number(left > right) - Number(left < right))
223
+ .map((name) => [name, snapshotStream(substreams.streams.get(name))]));
224
+ return { seed: substreams.seed, streams };
225
+ }
226
+ /**
227
+ * Rebuild a substream set from `snapshotSubstreams` output. Validates the
228
+ * complete snapshot before building anything; streams that were never created
229
+ * restore lazily from the seed on first `getSubstream`.
230
+ */
231
+ function restoreSubstreams(snapshot) {
232
+ if (typeof snapshot !== "object" || snapshot === null) {
233
+ throw new TypeError("restoreSubstreams: snapshot must contain a seed and streams.");
234
+ }
235
+ assertSubstreamSeed(snapshot.seed, "restoreSubstreams");
236
+ const entries = snapshot.streams;
237
+ if (typeof entries !== "object" || entries === null || Array.isArray(entries)) {
238
+ throw new TypeError("restoreSubstreams: streams must be an object keyed by stream name.");
239
+ }
240
+ const restored = new Map();
241
+ for (const [name, stream] of Object.entries(entries)) {
242
+ if (name.length === 0) {
243
+ throw new TypeError("restoreSubstreams: stream names must be non-empty strings.");
244
+ }
245
+ restored.set(name, restoreStream(stream));
246
+ }
247
+ return { seed: snapshot.seed, streams: restored };
248
+ }
@@ -82,5 +82,39 @@ export declare function restoreStream(snap: RngStreamSnapshot): RngStream;
82
82
  * `restoreStream` — applied independently to `gen` and `events`.
83
83
  */
84
84
  export declare function restoreLayers(snap: RngLayersSnapshot): RngLayers;
85
+ export type Substreams = {
86
+ /** The seed every member stream is derived from. */
87
+ readonly seed: string | number;
88
+ /** Streams created so far, keyed by name. Create members with `getSubstream`. */
89
+ readonly streams: ReadonlyMap<string, RngStream>;
90
+ };
91
+ export type SubstreamsSnapshot = {
92
+ seed: string | number;
93
+ streams: Record<string, RngStreamSnapshot>;
94
+ };
95
+ /**
96
+ * Create the stream named `name` under `seed`: a fresh stream seeded by
97
+ * `deriveSeed(seed, name)`. Creating or consuming any other substream never
98
+ * shifts this one.
99
+ */
100
+ export declare function substream(seed: string | number, name: string): RngStream;
101
+ /** Create an empty, lazily populated set of named substreams under `seed`. */
102
+ export declare function createSubstreams(seed: string | number): Substreams;
103
+ /**
104
+ * Return the set's stream for `name`, creating it from the set's seed on first
105
+ * use. The same name always returns the same live stream instance.
106
+ */
107
+ export declare function getSubstream(substreams: Substreams, name: string): RngStream;
108
+ /**
109
+ * Capture the set's seed plus every stream created so far, with names in
110
+ * sorted order. JSON-serializable and detached from later draws.
111
+ */
112
+ export declare function snapshotSubstreams(substreams: Substreams): SubstreamsSnapshot;
113
+ /**
114
+ * Rebuild a substream set from `snapshotSubstreams` output. Validates the
115
+ * complete snapshot before building anything; streams that were never created
116
+ * restore lazily from the seed on first `getSubstream`.
117
+ */
118
+ export declare function restoreSubstreams(snapshot: SubstreamsSnapshot): Substreams;
85
119
  export {};
86
120
  //# sourceMappingURL=rng.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../../src/rng.ts"],"names":[],"mappings":"AAqBA,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,KAAK,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC;AACnC,KAAK,QAAQ,GAAG,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;AAE/C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,SAAS,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC,CAAC;AAgBF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAUpD;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAc7F;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AACjD,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAEtF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,iBAAiB,CAGnE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,iBAAiB,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAqBhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAKhE"}
1
+ {"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../../src/rng.ts"],"names":[],"mappings":"AAqBA,OAAO,UAAU,MAAM,YAAY,CAAC;AAGpC,KAAK,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC;AACnC,KAAK,QAAQ,GAAG,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;AAE/C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,SAAS,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC,CAAC;AAgBF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAUpD;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAc7F;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AACjD,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAEtF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,iBAAiB,CAGnE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,iBAAiB,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAqBhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAKhE;AAUD,MAAM,MAAM,UAAU,GAAG;IACvB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC5C,CAAC;AAcF;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAGxE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAGlE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAO5E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,UAAU,GAAG,kBAAkB,CAW7E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,UAAU,CAiB1E"}
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ // Seed derivation and master seeds.
3
+ //
4
+ // A seeded game usually has a chain of seeds: a buried master seed for the
5
+ // profile, a run seed derived from it, a level seed derived from that, and
6
+ // one stream per generator. Building those keys by string concatenation is
7
+ // ambiguous ("1" + "23" === "12" + "3"), so `deriveSeed` hashes length-prefixed
8
+ // parts instead. Leaf module: no runtime dependency, so it is identical in
9
+ // browsers and Node and in both the ESM and CommonJS builds.
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.MASTER_SEED_BYTES = void 0;
12
+ exports.deriveSeed = deriveSeed;
13
+ exports.createMasterSeed = createMasterSeed;
14
+ exports.isMasterSeed = isMasterSeed;
15
+ /** Byte length of a master seed (128 bits). */
16
+ exports.MASTER_SEED_BYTES = 16;
17
+ // Changing the domain tag or the encoding changes every derived seed in every
18
+ // consumer's saves. Treat both as a frozen public contract.
19
+ const DERIVE_DOMAIN = "koota-kit:deriveSeed:v1";
20
+ const HEX_SEED = /^[0-9a-f]{32}$/;
21
+ function canonicalPart(value, label, api) {
22
+ if (typeof value === "string")
23
+ return value;
24
+ if (typeof value === "number" && Number.isFinite(value))
25
+ return String(value);
26
+ throw new TypeError(`${api}: ${label} must be a string or finite number.`);
27
+ }
28
+ /** UTF-8 encode, mapping lone surrogates to U+FFFD exactly like TextEncoder. */
29
+ function utf8(text) {
30
+ const bytes = [];
31
+ // String iteration yields whole code points; an unpaired surrogate arrives
32
+ // alone as its own code unit, so it is the only surrogate value seen here.
33
+ for (const char of text) {
34
+ let code = char.codePointAt(0);
35
+ if (code >= 0xd800 && code <= 0xdfff)
36
+ code = 0xfffd;
37
+ if (code < 0x80) {
38
+ bytes.push(code);
39
+ }
40
+ else if (code < 0x800) {
41
+ bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
42
+ }
43
+ else if (code < 0x10000) {
44
+ bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
45
+ }
46
+ else {
47
+ bytes.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 0x3f), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
48
+ }
49
+ }
50
+ return bytes;
51
+ }
52
+ const K = [
53
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
54
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
55
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
56
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
57
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
58
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
59
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
60
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
61
+ ];
62
+ const rotr = (value, bits) => (value >>> bits) | (value << (32 - bits));
63
+ /** FIPS 180-4 SHA-256 of `message`, as eight big-endian 32-bit words. */
64
+ function sha256(message) {
65
+ const bitLength = message.length * 8;
66
+ const zeroPadding = (55 - (message.length % 64) + 64) % 64;
67
+ // Seed inputs are far below 2**32 bits, so the high length word is zero.
68
+ const padded = [
69
+ ...message,
70
+ 0x80,
71
+ ...new Array(zeroPadding).fill(0),
72
+ 0,
73
+ 0,
74
+ 0,
75
+ 0,
76
+ bitLength >>> 24,
77
+ (bitLength >>> 16) & 0xff,
78
+ (bitLength >>> 8) & 0xff,
79
+ bitLength & 0xff,
80
+ ];
81
+ const hash = new Uint32Array([
82
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
83
+ ]);
84
+ const w = new Uint32Array(64);
85
+ for (let offset = 0; offset < padded.length; offset += 64) {
86
+ for (let t = 0; t < 16; t += 1) {
87
+ const i = offset + t * 4;
88
+ w[t] =
89
+ (padded[i] << 24) |
90
+ (padded[i + 1] << 16) |
91
+ (padded[i + 2] << 8) |
92
+ padded[i + 3];
93
+ }
94
+ for (let t = 16; t < 64; t += 1) {
95
+ const w15 = w[t - 15];
96
+ const w2 = w[t - 2];
97
+ const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ (w15 >>> 3);
98
+ const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ (w2 >>> 10);
99
+ w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0;
100
+ }
101
+ let a = hash[0];
102
+ let b = hash[1];
103
+ let c = hash[2];
104
+ let d = hash[3];
105
+ let e = hash[4];
106
+ let f = hash[5];
107
+ let g = hash[6];
108
+ let h = hash[7];
109
+ for (let t = 0; t < 64; t += 1) {
110
+ const t1 = (h +
111
+ (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) +
112
+ ((e & f) ^ (~e & g)) +
113
+ K[t] +
114
+ w[t]) >>>
115
+ 0;
116
+ const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
117
+ h = g;
118
+ g = f;
119
+ f = e;
120
+ e = (d + t1) >>> 0;
121
+ d = c;
122
+ c = b;
123
+ b = a;
124
+ a = (t1 + t2) >>> 0;
125
+ }
126
+ hash[0] = (hash[0] + a) >>> 0;
127
+ hash[1] = (hash[1] + b) >>> 0;
128
+ hash[2] = (hash[2] + c) >>> 0;
129
+ hash[3] = (hash[3] + d) >>> 0;
130
+ hash[4] = (hash[4] + e) >>> 0;
131
+ hash[5] = (hash[5] + f) >>> 0;
132
+ hash[6] = (hash[6] + g) >>> 0;
133
+ hash[7] = (hash[7] + h) >>> 0;
134
+ }
135
+ return hash;
136
+ }
137
+ const toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
138
+ /**
139
+ * Derive a child seed from `parent` and ordered `parts`. Returns the first 128
140
+ * bits of SHA-256 over a domain tag plus each value as a 4-byte big-endian
141
+ * UTF-8 length and its UTF-8 text, as 32 lowercase hex characters. Numbers
142
+ * canonicalize to their string form, matching `createRng`.
143
+ */
144
+ function deriveSeed(parent, ...parts) {
145
+ const values = [canonicalPart(parent, "parent", "deriveSeed")];
146
+ parts.forEach((part, index) => {
147
+ values.push(canonicalPart(part, `parts[${index}]`, "deriveSeed"));
148
+ });
149
+ const chunks = [utf8(DERIVE_DOMAIN)];
150
+ for (const value of values) {
151
+ const encoded = utf8(value);
152
+ const length = encoded.length;
153
+ const prefix = [
154
+ (length >>> 24) & 0xff,
155
+ (length >>> 16) & 0xff,
156
+ (length >>> 8) & 0xff,
157
+ length & 0xff,
158
+ ];
159
+ chunks.push(prefix, encoded);
160
+ }
161
+ const digest = sha256(chunks.flat());
162
+ return Array.from(digest.subarray(0, 4), (word) => word.toString(16).padStart(8, "0")).join("");
163
+ }
164
+ /**
165
+ * Create a 128-bit master seed from `entropy` (default `globalThis.crypto`)
166
+ * as 32 lowercase hex characters. Fails closed when no source exists, when it
167
+ * returns anything but the requested 16-byte buffer, or when the buffer is
168
+ * still all zero (an unfilled stub, not entropy).
169
+ */
170
+ function createMasterSeed(entropy) {
171
+ const source = entropy ?? globalThis.crypto;
172
+ if (!source || typeof source.getRandomValues !== "function") {
173
+ throw new TypeError("createMasterSeed: no entropy source with getRandomValues is available.");
174
+ }
175
+ const requested = new Uint8Array(exports.MASTER_SEED_BYTES);
176
+ const bytes = source.getRandomValues(requested);
177
+ if (bytes !== requested) {
178
+ throw new TypeError("createMasterSeed: the entropy source must fill the requested 16-byte array.");
179
+ }
180
+ if (bytes.every((byte) => byte === 0)) {
181
+ throw new RangeError("createMasterSeed: the entropy source returned all zero bytes.");
182
+ }
183
+ return toHex(bytes);
184
+ }
185
+ /** True for exactly 32 lowercase hex characters: the shape of a master or derived seed. */
186
+ function isMasterSeed(value) {
187
+ return typeof value === "string" && HEX_SEED.test(value);
188
+ }
@@ -0,0 +1,25 @@
1
+ /** A value that can seed a stream or be mixed into a derived seed. */
2
+ export type SeedPart = string | number;
3
+ /** The subset of Web Crypto that `createMasterSeed` needs. Inject one in tests. */
4
+ export type EntropySource = {
5
+ getRandomValues(array: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;
6
+ };
7
+ /** Byte length of a master seed (128 bits). */
8
+ export declare const MASTER_SEED_BYTES = 16;
9
+ /**
10
+ * Derive a child seed from `parent` and ordered `parts`. Returns the first 128
11
+ * bits of SHA-256 over a domain tag plus each value as a 4-byte big-endian
12
+ * UTF-8 length and its UTF-8 text, as 32 lowercase hex characters. Numbers
13
+ * canonicalize to their string form, matching `createRng`.
14
+ */
15
+ export declare function deriveSeed(parent: SeedPart, ...parts: SeedPart[]): string;
16
+ /**
17
+ * Create a 128-bit master seed from `entropy` (default `globalThis.crypto`)
18
+ * as 32 lowercase hex characters. Fails closed when no source exists, when it
19
+ * returns anything but the requested 16-byte buffer, or when the buffer is
20
+ * still all zero (an unfilled stub, not entropy).
21
+ */
22
+ export declare function createMasterSeed(entropy?: EntropySource): string;
23
+ /** True for exactly 32 lowercase hex characters: the shape of a master or derived seed. */
24
+ export declare function isMasterSeed(value: unknown): value is string;
25
+ //# sourceMappingURL=seed.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["../../src/seed.ts"],"names":[],"mappings":"AASA,sEAAsE;AACtE,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC,mFAAmF;AAGnF,MAAM,MAAM,aAAa,GAAG;IAC1B,eAAe,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;CAC1E,CAAC;AAEF,+CAA+C;AAC/C,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAoIpC;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,CAmBzE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAgBhE;AAED,2FAA2F;AAC3F,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE5D"}
@@ -2,8 +2,7 @@
2
2
  /**
3
3
  * `src/traits/` — trait-authoring conventions, kept in a LEAF sub-package.
4
4
  *
5
- * Packaging pattern (from medieval-hexagon-gameboard): trait declarations
6
- * live in a leaf sub-package with ZERO sibling imports — this file imports
5
+ * Trait declarations live in a leaf sub-package with ZERO sibling imports — this file imports
7
6
  * only `koota` (the npm dep) and pure types, never runtime values from
8
7
  * sibling modules like ./world or ./rng. That is what prevents the
9
8
  * koota ↔ world-facade ↔ game-scenario top-level evaluation cycle that a
@@ -12,8 +11,7 @@
12
11
  * from exactly one location so trait reference-identity holds regardless of
13
12
  * which subpath a consumer imports from.
14
13
  *
15
- * The AoS-aliasing footgun this module guards against (timber-town's
16
- * documented convention): object-valued fields in a SoA schema must use the
14
+ * Object-valued fields in a SoA schema must use the
17
15
  * AoS-factory pattern (`() => ({...})`) so each entity gets its own object
18
16
  * instance — koota's SoA pattern would otherwise alias state across
19
17
  * entities (one shared object literal mutated by every entity that has the
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * `src/traits/` — trait-authoring conventions, kept in a LEAF sub-package.
3
3
  *
4
- * Packaging pattern (from medieval-hexagon-gameboard): trait declarations
5
- * live in a leaf sub-package with ZERO sibling imports — this file imports
4
+ * Trait declarations live in a leaf sub-package with ZERO sibling imports — this file imports
6
5
  * only `koota` (the npm dep) and pure types, never runtime values from
7
6
  * sibling modules like ./world or ./rng. That is what prevents the
8
7
  * koota ↔ world-facade ↔ game-scenario top-level evaluation cycle that a
@@ -11,8 +10,7 @@
11
10
  * from exactly one location so trait reference-identity holds regardless of
12
11
  * which subpath a consumer imports from.
13
12
  *
14
- * The AoS-aliasing footgun this module guards against (timber-town's
15
- * documented convention): object-valued fields in a SoA schema must use the
13
+ * Object-valued fields in a SoA schema must use the
16
14
  * AoS-factory pattern (`() => ({...})`) so each entity gets its own object
17
15
  * instance — koota's SoA pattern would otherwise alias state across
18
16
  * entities (one shared object literal mutated by every entity that has the
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/traits/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAGjD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GAC/D,CAAC,GACD;KACG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GACtD,CAAC,CAAC,CAAC,CAAC,GACJ,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GACjB,KAAK,GACL,CAAC,CAAC,CAAC,CAAC;CACX,CAAC;AAEN;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAgBvF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/traits/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAGjD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GAC/D,CAAC,GACD;KACG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GACtD,CAAC,CAAC,CAAC,CAAC,GACJ,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GACjB,KAAK,GACL,CAAC,CAAC,CAAC,CAAC;CACX,CAAC;AAEN;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAgBvF"}
@@ -1,7 +1,9 @@
1
1
  export { advanceClock, createActions, createSimWorld, destroySimWorld, relation, restoreWorldHeader, snapshotWorld, trait, } from "./world.js";
2
2
  export type { Entity, World, WorldHandle, WorldSnapshot } from "./world.js";
3
- export { chance, createRng, nextFloat, nextInt, nextU32, restoreLayers, restoreStream, snapshotLayers, snapshotStream, } from "./rng.js";
4
- export type { RngLayers, RngLayersSnapshot, RngSeeds, RngStream, RngStreamSnapshot, } from "./rng.js";
3
+ export { chance, createRng, createSubstreams, getSubstream, nextFloat, nextInt, nextU32, restoreLayers, restoreStream, restoreSubstreams, snapshotLayers, snapshotStream, snapshotSubstreams, substream, } from "./rng.js";
4
+ export type { RngLayers, RngLayersSnapshot, RngSeeds, RngStream, RngStreamSnapshot, Substreams, SubstreamsSnapshot, } from "./rng.js";
5
+ export { createMasterSeed, deriveSeed, isMasterSeed, MASTER_SEED_BYTES } from "./seed.js";
6
+ export type { EntropySource, SeedPart } from "./seed.js";
5
7
  export { defineEventLog } from "./eventLog.js";
6
8
  export type { EventLog } from "./eventLog.js";
7
9
  export { defineTrait } from "./traits/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,SAAS,EACT,OAAO,EACP,OAAO,EACP,aAAa,EACb,aAAa,EACb,cAAc,EACd,cAAc,GACf,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,GAClB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAO,EACP,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/esm/index.js CHANGED
@@ -1,14 +1,17 @@
1
1
  // koota-kit — thin koota ECS conventions layer.
2
2
  //
3
- // Four modules, also importable as subpaths:
3
+ // Five modules, also importable as subpaths:
4
4
  // ./world — WorldHandle lifecycle (world + dual-stream rng + clock + scratch)
5
- // ./rng — dual-layer deterministic seedrandom PRNG with byte-exact
6
- // snapshot/restore
5
+ // ./rng — dual-layer deterministic seedrandom PRNG and named substreams
6
+ // with byte-exact snapshot/restore
7
+ // ./seed — length-prefixed SHA-256 seed derivation and 128-bit master
8
+ // seeds (leaf module, zero runtime dependencies)
7
9
  // ./traits — defineTrait AoS-aliasing guard (leaf module, zero sibling
8
10
  // imports)
9
11
  // ./eventLog — scratch-backed publish/drain event logs with a peek seam for
10
12
  // observers (harnesses, tests, HUDs)
11
13
  export { advanceClock, createActions, createSimWorld, destroySimWorld, relation, restoreWorldHeader, snapshotWorld, trait, } from "./world.js";
12
- export { chance, createRng, nextFloat, nextInt, nextU32, restoreLayers, restoreStream, snapshotLayers, snapshotStream, } from "./rng.js";
14
+ export { chance, createRng, createSubstreams, getSubstream, nextFloat, nextInt, nextU32, restoreLayers, restoreStream, restoreSubstreams, snapshotLayers, snapshotStream, snapshotSubstreams, substream, } from "./rng.js";
15
+ export { createMasterSeed, deriveSeed, isMasterSeed, MASTER_SEED_BYTES } from "./seed.js";
13
16
  export { defineEventLog } from "./eventLog.js";
14
17
  export { defineTrait } from "./traits/index.js";
package/dist/esm/rng.d.ts CHANGED
@@ -82,5 +82,39 @@ export declare function restoreStream(snap: RngStreamSnapshot): RngStream;
82
82
  * `restoreStream` — applied independently to `gen` and `events`.
83
83
  */
84
84
  export declare function restoreLayers(snap: RngLayersSnapshot): RngLayers;
85
+ export type Substreams = {
86
+ /** The seed every member stream is derived from. */
87
+ readonly seed: string | number;
88
+ /** Streams created so far, keyed by name. Create members with `getSubstream`. */
89
+ readonly streams: ReadonlyMap<string, RngStream>;
90
+ };
91
+ export type SubstreamsSnapshot = {
92
+ seed: string | number;
93
+ streams: Record<string, RngStreamSnapshot>;
94
+ };
95
+ /**
96
+ * Create the stream named `name` under `seed`: a fresh stream seeded by
97
+ * `deriveSeed(seed, name)`. Creating or consuming any other substream never
98
+ * shifts this one.
99
+ */
100
+ export declare function substream(seed: string | number, name: string): RngStream;
101
+ /** Create an empty, lazily populated set of named substreams under `seed`. */
102
+ export declare function createSubstreams(seed: string | number): Substreams;
103
+ /**
104
+ * Return the set's stream for `name`, creating it from the set's seed on first
105
+ * use. The same name always returns the same live stream instance.
106
+ */
107
+ export declare function getSubstream(substreams: Substreams, name: string): RngStream;
108
+ /**
109
+ * Capture the set's seed plus every stream created so far, with names in
110
+ * sorted order. JSON-serializable and detached from later draws.
111
+ */
112
+ export declare function snapshotSubstreams(substreams: Substreams): SubstreamsSnapshot;
113
+ /**
114
+ * Rebuild a substream set from `snapshotSubstreams` output. Validates the
115
+ * complete snapshot before building anything; streams that were never created
116
+ * restore lazily from the seed on first `getSubstream`.
117
+ */
118
+ export declare function restoreSubstreams(snapshot: SubstreamsSnapshot): Substreams;
85
119
  export {};
86
120
  //# sourceMappingURL=rng.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../../src/rng.ts"],"names":[],"mappings":"AAqBA,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,KAAK,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC;AACnC,KAAK,QAAQ,GAAG,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;AAE/C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,SAAS,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC,CAAC;AAgBF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAUpD;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAc7F;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AACjD,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAEtF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,iBAAiB,CAGnE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,iBAAiB,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAqBhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAKhE"}
1
+ {"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../../src/rng.ts"],"names":[],"mappings":"AAqBA,OAAO,UAAU,MAAM,YAAY,CAAC;AAGpC,KAAK,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC;AACnC,KAAK,QAAQ,GAAG,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;AAE/C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,SAAS,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC,CAAC;AAgBF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAUpD;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAc7F;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AACjD,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAEtF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,iBAAiB,CAGnE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,iBAAiB,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAqBhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAKhE;AAUD,MAAM,MAAM,UAAU,GAAG;IACvB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC5C,CAAC;AAcF;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAGxE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAGlE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAO5E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,UAAU,GAAG,kBAAkB,CAW7E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,UAAU,CAiB1E"}
package/dist/esm/rng.js CHANGED
@@ -19,6 +19,7 @@
19
19
  // Forbidden in sim code: `Math.random()`. Use `nextFloat(rng.gen)` or
20
20
  // `nextFloat(rng.events)` depending on the layer.
21
21
  import seedrandom from "seedrandom";
22
+ import { deriveSeed } from "./seed.js";
22
23
  function assertSeed(seed, name) {
23
24
  if (typeof seed !== "string" && typeof seed !== "number") {
24
25
  throw new TypeError(`createRng: seeds.${name} must be a string or finite number.`);
@@ -152,3 +153,77 @@ export function restoreLayers(snap) {
152
153
  }
153
154
  return { gen: restoreStream(snap.gen), events: restoreStream(snap.events) };
154
155
  }
156
+ function assertSubstreamName(name, api) {
157
+ if (typeof name !== "string" || name.length === 0) {
158
+ throw new TypeError(`${api}: name must be a non-empty string.`);
159
+ }
160
+ }
161
+ function assertSubstreamSeed(seed, api) {
162
+ if (typeof seed !== "string" && !(typeof seed === "number" && Number.isFinite(seed))) {
163
+ throw new TypeError(`${api}: seed must be a string or finite number.`);
164
+ }
165
+ }
166
+ /**
167
+ * Create the stream named `name` under `seed`: a fresh stream seeded by
168
+ * `deriveSeed(seed, name)`. Creating or consuming any other substream never
169
+ * shifts this one.
170
+ */
171
+ export function substream(seed, name) {
172
+ assertSubstreamName(name, "substream");
173
+ return makeStream(deriveSeed(seed, name));
174
+ }
175
+ /** Create an empty, lazily populated set of named substreams under `seed`. */
176
+ export function createSubstreams(seed) {
177
+ assertSubstreamSeed(seed, "createSubstreams");
178
+ return { seed, streams: new Map() };
179
+ }
180
+ /**
181
+ * Return the set's stream for `name`, creating it from the set's seed on first
182
+ * use. The same name always returns the same live stream instance.
183
+ */
184
+ export function getSubstream(substreams, name) {
185
+ assertSubstreamName(name, "getSubstream");
186
+ const existing = substreams.streams.get(name);
187
+ if (existing)
188
+ return existing;
189
+ const stream = substream(substreams.seed, name);
190
+ substreams.streams.set(name, stream);
191
+ return stream;
192
+ }
193
+ /**
194
+ * Capture the set's seed plus every stream created so far, with names in
195
+ * sorted order. JSON-serializable and detached from later draws.
196
+ */
197
+ export function snapshotSubstreams(substreams) {
198
+ // Object.fromEntries defines own data properties, so a stream named
199
+ // "__proto__" is captured instead of hitting the prototype setter.
200
+ const streams = Object.fromEntries([...substreams.streams.keys()]
201
+ // UTF-16 code-unit order, never localeCompare: the order must not vary
202
+ // by runtime locale. Branch-free because names in a Map are unique.
203
+ .sort((left, right) => Number(left > right) - Number(left < right))
204
+ .map((name) => [name, snapshotStream(substreams.streams.get(name))]));
205
+ return { seed: substreams.seed, streams };
206
+ }
207
+ /**
208
+ * Rebuild a substream set from `snapshotSubstreams` output. Validates the
209
+ * complete snapshot before building anything; streams that were never created
210
+ * restore lazily from the seed on first `getSubstream`.
211
+ */
212
+ export function restoreSubstreams(snapshot) {
213
+ if (typeof snapshot !== "object" || snapshot === null) {
214
+ throw new TypeError("restoreSubstreams: snapshot must contain a seed and streams.");
215
+ }
216
+ assertSubstreamSeed(snapshot.seed, "restoreSubstreams");
217
+ const entries = snapshot.streams;
218
+ if (typeof entries !== "object" || entries === null || Array.isArray(entries)) {
219
+ throw new TypeError("restoreSubstreams: streams must be an object keyed by stream name.");
220
+ }
221
+ const restored = new Map();
222
+ for (const [name, stream] of Object.entries(entries)) {
223
+ if (name.length === 0) {
224
+ throw new TypeError("restoreSubstreams: stream names must be non-empty strings.");
225
+ }
226
+ restored.set(name, restoreStream(stream));
227
+ }
228
+ return { seed: snapshot.seed, streams: restored };
229
+ }
@@ -0,0 +1,25 @@
1
+ /** A value that can seed a stream or be mixed into a derived seed. */
2
+ export type SeedPart = string | number;
3
+ /** The subset of Web Crypto that `createMasterSeed` needs. Inject one in tests. */
4
+ export type EntropySource = {
5
+ getRandomValues(array: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;
6
+ };
7
+ /** Byte length of a master seed (128 bits). */
8
+ export declare const MASTER_SEED_BYTES = 16;
9
+ /**
10
+ * Derive a child seed from `parent` and ordered `parts`. Returns the first 128
11
+ * bits of SHA-256 over a domain tag plus each value as a 4-byte big-endian
12
+ * UTF-8 length and its UTF-8 text, as 32 lowercase hex characters. Numbers
13
+ * canonicalize to their string form, matching `createRng`.
14
+ */
15
+ export declare function deriveSeed(parent: SeedPart, ...parts: SeedPart[]): string;
16
+ /**
17
+ * Create a 128-bit master seed from `entropy` (default `globalThis.crypto`)
18
+ * as 32 lowercase hex characters. Fails closed when no source exists, when it
19
+ * returns anything but the requested 16-byte buffer, or when the buffer is
20
+ * still all zero (an unfilled stub, not entropy).
21
+ */
22
+ export declare function createMasterSeed(entropy?: EntropySource): string;
23
+ /** True for exactly 32 lowercase hex characters: the shape of a master or derived seed. */
24
+ export declare function isMasterSeed(value: unknown): value is string;
25
+ //# sourceMappingURL=seed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["../../src/seed.ts"],"names":[],"mappings":"AASA,sEAAsE;AACtE,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC,mFAAmF;AAGnF,MAAM,MAAM,aAAa,GAAG;IAC1B,eAAe,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;CAC1E,CAAC;AAEF,+CAA+C;AAC/C,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAoIpC;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,CAmBzE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAgBhE;AAED,2FAA2F;AAC3F,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE5D"}
@@ -0,0 +1,182 @@
1
+ // Seed derivation and master seeds.
2
+ //
3
+ // A seeded game usually has a chain of seeds: a buried master seed for the
4
+ // profile, a run seed derived from it, a level seed derived from that, and
5
+ // one stream per generator. Building those keys by string concatenation is
6
+ // ambiguous ("1" + "23" === "12" + "3"), so `deriveSeed` hashes length-prefixed
7
+ // parts instead. Leaf module: no runtime dependency, so it is identical in
8
+ // browsers and Node and in both the ESM and CommonJS builds.
9
+ /** Byte length of a master seed (128 bits). */
10
+ export const MASTER_SEED_BYTES = 16;
11
+ // Changing the domain tag or the encoding changes every derived seed in every
12
+ // consumer's saves. Treat both as a frozen public contract.
13
+ const DERIVE_DOMAIN = "koota-kit:deriveSeed:v1";
14
+ const HEX_SEED = /^[0-9a-f]{32}$/;
15
+ function canonicalPart(value, label, api) {
16
+ if (typeof value === "string")
17
+ return value;
18
+ if (typeof value === "number" && Number.isFinite(value))
19
+ return String(value);
20
+ throw new TypeError(`${api}: ${label} must be a string or finite number.`);
21
+ }
22
+ /** UTF-8 encode, mapping lone surrogates to U+FFFD exactly like TextEncoder. */
23
+ function utf8(text) {
24
+ const bytes = [];
25
+ // String iteration yields whole code points; an unpaired surrogate arrives
26
+ // alone as its own code unit, so it is the only surrogate value seen here.
27
+ for (const char of text) {
28
+ let code = char.codePointAt(0);
29
+ if (code >= 0xd800 && code <= 0xdfff)
30
+ code = 0xfffd;
31
+ if (code < 0x80) {
32
+ bytes.push(code);
33
+ }
34
+ else if (code < 0x800) {
35
+ bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
36
+ }
37
+ else if (code < 0x10000) {
38
+ bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
39
+ }
40
+ else {
41
+ bytes.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 0x3f), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
42
+ }
43
+ }
44
+ return bytes;
45
+ }
46
+ const K = [
47
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
48
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
49
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
50
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
51
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
52
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
53
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
54
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
55
+ ];
56
+ const rotr = (value, bits) => (value >>> bits) | (value << (32 - bits));
57
+ /** FIPS 180-4 SHA-256 of `message`, as eight big-endian 32-bit words. */
58
+ function sha256(message) {
59
+ const bitLength = message.length * 8;
60
+ const zeroPadding = (55 - (message.length % 64) + 64) % 64;
61
+ // Seed inputs are far below 2**32 bits, so the high length word is zero.
62
+ const padded = [
63
+ ...message,
64
+ 0x80,
65
+ ...new Array(zeroPadding).fill(0),
66
+ 0,
67
+ 0,
68
+ 0,
69
+ 0,
70
+ bitLength >>> 24,
71
+ (bitLength >>> 16) & 0xff,
72
+ (bitLength >>> 8) & 0xff,
73
+ bitLength & 0xff,
74
+ ];
75
+ const hash = new Uint32Array([
76
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
77
+ ]);
78
+ const w = new Uint32Array(64);
79
+ for (let offset = 0; offset < padded.length; offset += 64) {
80
+ for (let t = 0; t < 16; t += 1) {
81
+ const i = offset + t * 4;
82
+ w[t] =
83
+ (padded[i] << 24) |
84
+ (padded[i + 1] << 16) |
85
+ (padded[i + 2] << 8) |
86
+ padded[i + 3];
87
+ }
88
+ for (let t = 16; t < 64; t += 1) {
89
+ const w15 = w[t - 15];
90
+ const w2 = w[t - 2];
91
+ const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ (w15 >>> 3);
92
+ const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ (w2 >>> 10);
93
+ w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0;
94
+ }
95
+ let a = hash[0];
96
+ let b = hash[1];
97
+ let c = hash[2];
98
+ let d = hash[3];
99
+ let e = hash[4];
100
+ let f = hash[5];
101
+ let g = hash[6];
102
+ let h = hash[7];
103
+ for (let t = 0; t < 64; t += 1) {
104
+ const t1 = (h +
105
+ (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) +
106
+ ((e & f) ^ (~e & g)) +
107
+ K[t] +
108
+ w[t]) >>>
109
+ 0;
110
+ const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
111
+ h = g;
112
+ g = f;
113
+ f = e;
114
+ e = (d + t1) >>> 0;
115
+ d = c;
116
+ c = b;
117
+ b = a;
118
+ a = (t1 + t2) >>> 0;
119
+ }
120
+ hash[0] = (hash[0] + a) >>> 0;
121
+ hash[1] = (hash[1] + b) >>> 0;
122
+ hash[2] = (hash[2] + c) >>> 0;
123
+ hash[3] = (hash[3] + d) >>> 0;
124
+ hash[4] = (hash[4] + e) >>> 0;
125
+ hash[5] = (hash[5] + f) >>> 0;
126
+ hash[6] = (hash[6] + g) >>> 0;
127
+ hash[7] = (hash[7] + h) >>> 0;
128
+ }
129
+ return hash;
130
+ }
131
+ const toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
132
+ /**
133
+ * Derive a child seed from `parent` and ordered `parts`. Returns the first 128
134
+ * bits of SHA-256 over a domain tag plus each value as a 4-byte big-endian
135
+ * UTF-8 length and its UTF-8 text, as 32 lowercase hex characters. Numbers
136
+ * canonicalize to their string form, matching `createRng`.
137
+ */
138
+ export function deriveSeed(parent, ...parts) {
139
+ const values = [canonicalPart(parent, "parent", "deriveSeed")];
140
+ parts.forEach((part, index) => {
141
+ values.push(canonicalPart(part, `parts[${index}]`, "deriveSeed"));
142
+ });
143
+ const chunks = [utf8(DERIVE_DOMAIN)];
144
+ for (const value of values) {
145
+ const encoded = utf8(value);
146
+ const length = encoded.length;
147
+ const prefix = [
148
+ (length >>> 24) & 0xff,
149
+ (length >>> 16) & 0xff,
150
+ (length >>> 8) & 0xff,
151
+ length & 0xff,
152
+ ];
153
+ chunks.push(prefix, encoded);
154
+ }
155
+ const digest = sha256(chunks.flat());
156
+ return Array.from(digest.subarray(0, 4), (word) => word.toString(16).padStart(8, "0")).join("");
157
+ }
158
+ /**
159
+ * Create a 128-bit master seed from `entropy` (default `globalThis.crypto`)
160
+ * as 32 lowercase hex characters. Fails closed when no source exists, when it
161
+ * returns anything but the requested 16-byte buffer, or when the buffer is
162
+ * still all zero (an unfilled stub, not entropy).
163
+ */
164
+ export function createMasterSeed(entropy) {
165
+ const source = entropy ?? globalThis.crypto;
166
+ if (!source || typeof source.getRandomValues !== "function") {
167
+ throw new TypeError("createMasterSeed: no entropy source with getRandomValues is available.");
168
+ }
169
+ const requested = new Uint8Array(MASTER_SEED_BYTES);
170
+ const bytes = source.getRandomValues(requested);
171
+ if (bytes !== requested) {
172
+ throw new TypeError("createMasterSeed: the entropy source must fill the requested 16-byte array.");
173
+ }
174
+ if (bytes.every((byte) => byte === 0)) {
175
+ throw new RangeError("createMasterSeed: the entropy source returned all zero bytes.");
176
+ }
177
+ return toHex(bytes);
178
+ }
179
+ /** True for exactly 32 lowercase hex characters: the shape of a master or derived seed. */
180
+ export function isMasterSeed(value) {
181
+ return typeof value === "string" && HEX_SEED.test(value);
182
+ }
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * `src/traits/` — trait-authoring conventions, kept in a LEAF sub-package.
3
3
  *
4
- * Packaging pattern (from medieval-hexagon-gameboard): trait declarations
5
- * live in a leaf sub-package with ZERO sibling imports — this file imports
4
+ * Trait declarations live in a leaf sub-package with ZERO sibling imports — this file imports
6
5
  * only `koota` (the npm dep) and pure types, never runtime values from
7
6
  * sibling modules like ./world or ./rng. That is what prevents the
8
7
  * koota ↔ world-facade ↔ game-scenario top-level evaluation cycle that a
@@ -11,8 +10,7 @@
11
10
  * from exactly one location so trait reference-identity holds regardless of
12
11
  * which subpath a consumer imports from.
13
12
  *
14
- * The AoS-aliasing footgun this module guards against (timber-town's
15
- * documented convention): object-valued fields in a SoA schema must use the
13
+ * Object-valued fields in a SoA schema must use the
16
14
  * AoS-factory pattern (`() => ({...})`) so each entity gets its own object
17
15
  * instance — koota's SoA pattern would otherwise alias state across
18
16
  * entities (one shared object literal mutated by every entity that has the
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/traits/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAGjD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GAC/D,CAAC,GACD;KACG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GACtD,CAAC,CAAC,CAAC,CAAC,GACJ,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GACjB,KAAK,GACL,CAAC,CAAC,CAAC,CAAC;CACX,CAAC;AAEN;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAgBvF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/traits/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAGjD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GAC/D,CAAC,GACD;KACG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GACtD,CAAC,CAAC,CAAC,CAAC,GACJ,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GACjB,KAAK,GACL,CAAC,CAAC,CAAC,CAAC;CACX,CAAC;AAEN;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAgBvF"}
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * `src/traits/` — trait-authoring conventions, kept in a LEAF sub-package.
3
3
  *
4
- * Packaging pattern (from medieval-hexagon-gameboard): trait declarations
5
- * live in a leaf sub-package with ZERO sibling imports — this file imports
4
+ * Trait declarations live in a leaf sub-package with ZERO sibling imports — this file imports
6
5
  * only `koota` (the npm dep) and pure types, never runtime values from
7
6
  * sibling modules like ./world or ./rng. That is what prevents the
8
7
  * koota ↔ world-facade ↔ game-scenario top-level evaluation cycle that a
@@ -11,8 +10,7 @@
11
10
  * from exactly one location so trait reference-identity holds regardless of
12
11
  * which subpath a consumer imports from.
13
12
  *
14
- * The AoS-aliasing footgun this module guards against (timber-town's
15
- * documented convention): object-valued fields in a SoA schema must use the
13
+ * Object-valued fields in a SoA schema must use the
16
14
  * AoS-factory pattern (`() => ({...})`) so each entity gets its own object
17
15
  * instance — koota's SoA pattern would otherwise alias state across
18
16
  * entities (one shared object literal mutated by every entity that has the
package/docs/API.md CHANGED
@@ -82,6 +82,72 @@ function restoreLayers(snapshot: RngLayersSnapshot): RngLayers;
82
82
  - Restore functions reject malformed ARC4 state and clone accepted state so a
83
83
  snapshot can seed multiple independent replays.
84
84
 
85
+ ### Named substreams
86
+
87
+ ```ts
88
+ type Substreams = {
89
+ readonly seed: string | number;
90
+ readonly streams: ReadonlyMap<string, RngStream>;
91
+ };
92
+ type SubstreamsSnapshot = {
93
+ seed: string | number;
94
+ streams: Record<string, RngStreamSnapshot>;
95
+ };
96
+
97
+ function substream(seed: string | number, name: string): RngStream;
98
+ function createSubstreams(seed: string | number): Substreams;
99
+ function getSubstream(substreams: Substreams, name: string): RngStream;
100
+ function snapshotSubstreams(substreams: Substreams): SubstreamsSnapshot;
101
+ function restoreSubstreams(snapshot: SubstreamsSnapshot): Substreams;
102
+ ```
103
+
104
+ - `substream(seed, name)` returns a fresh stream seeded by
105
+ `deriveSeed(seed, name)`. Its sequence depends only on `seed` and `name`, so
106
+ adding, removing, reordering, or consuming another substream never shifts it.
107
+ - `name` must be a non-empty string. Invalid input throws before anything is
108
+ allocated.
109
+ - `createSubstreams` holds one lazily created stream per name, so generators
110
+ that share a set keep drawing where they left off. `getSubstream` returns the
111
+ same stream instance for the same name.
112
+ - `snapshotSubstreams` captures the seed plus every stream created so far, with
113
+ names in sorted order. `restoreSubstreams` validates the whole snapshot
114
+ before building anything and reproduces every later draw byte-exact. Streams
115
+ that were never created restore lazily from the seed.
116
+
117
+ ## `seed`
118
+
119
+ ```ts
120
+ type SeedPart = string | number;
121
+ type EntropySource = {
122
+ getRandomValues(array: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;
123
+ }; // `globalThis.crypto` satisfies it, with or without DOM typings
124
+
125
+ const MASTER_SEED_BYTES = 16;
126
+
127
+ function deriveSeed(parent: SeedPart, ...parts: SeedPart[]): string;
128
+ function createMasterSeed(entropy?: EntropySource): string;
129
+ function isMasterSeed(value: unknown): value is string;
130
+ ```
131
+
132
+ - `deriveSeed` hashes the parent seed and every part with SHA-256 and returns
133
+ the first 128 bits as 32 lowercase hex characters. The result is a valid
134
+ seed for `createRng`, `createSimWorld`, `substream`, and further
135
+ `deriveSeed` calls.
136
+ - Every value is length-prefixed before hashing, so `("a", "bc")` and
137
+ `("ab", "c")` derive different seeds. Order matters.
138
+ - Numbers canonicalize to their string form, matching `createRng`, so
139
+ `deriveSeed(7, "x") === deriveSeed("7", "x")`. Non-finite numbers and
140
+ non-string, non-number values throw.
141
+ - The output is stable across platforms and package versions. A change to the
142
+ derivation would be a breaking change.
143
+ - `createMasterSeed` fills 16 bytes from `entropy` (default
144
+ `globalThis.crypto`) and returns them as 32 lowercase hex characters. It
145
+ throws when no entropy source exists, when the source returns something
146
+ other than the requested 16-byte array, or when it returns all zero bytes,
147
+ which signals an unfilled buffer rather than real entropy.
148
+ - `isMasterSeed` accepts exactly 32 lowercase hex characters. Use it to
149
+ validate a persisted seed before deriving from it.
150
+
85
151
  ## `traits`
86
152
 
87
153
  ```ts
@@ -8,7 +8,8 @@ queries, and actions directly.
8
8
 
9
9
  ```text
10
10
  traits ───────────────> koota
11
- rng ──────────────────> seedrandom
11
+ seed ─────────────────> (leaf: built-in SHA-256)
12
+ rng ──────────────────> seedrandom + seed
12
13
  world ────────────────> koota + rng
13
14
  eventLog ─────────────> WorldHandle scratch
14
15
  index ────────────────> public re-exports only
@@ -19,7 +20,15 @@ keeps trait declarations out of world/scenario evaluation cycles and preserves
19
20
  the reference identity Koota uses to identify traits.
20
21
 
21
22
  `rng` has no Koota dependency. Its serialized contract is the seedrandom ARC4
22
- state plus a two-stream container.
23
+ state plus a two-stream container, or a seed plus named-substream container.
24
+
25
+ `seed` is a leaf module with no runtime dependency at all. `deriveSeed` uses a
26
+ small built-in synchronous SHA-256 so it behaves identically in browsers and
27
+ Node, in both the ESM and the CommonJS build (Web Crypto's digest is async, and
28
+ the common hash packages are ESM-only). The test suite checks it against
29
+ `node:crypto` and a frozen golden vector. Its byte encoding (a
30
+ domain tag, then each value as a 4-byte big-endian UTF-8 length plus its UTF-8
31
+ text) is a stable contract: changing it would change every derived world.
23
32
 
24
33
  `world` owns allocation, teardown, time, and facade-owned persistence. It does
25
34
  not own application entity serialization.
@@ -36,6 +45,8 @@ lifetime. It does not use a process-global queue.
36
45
  5. Object-valued structure-of-arrays fields use factories.
37
46
  6. Event logs have one consuming owner; observers use `peek`.
38
47
  7. World teardown clears facade-owned scratch state and releases the Koota ID.
48
+ 8. A named substream's sequence depends only on its seed and name; creating or
49
+ consuming another substream never shifts it.
39
50
 
40
51
  ## Performance choices
41
52
 
@@ -52,6 +63,7 @@ lifetime. It does not use a process-global queue.
52
63
  - No entity/trait serializer: applications need versioned domain schemas.
53
64
  - No scheduler or system runner: Koota's queries/actions remain the execution
54
65
  model.
55
- - No automatic derived-key hashing: the application owns stable domain keys.
66
+ - No implicit derived keys: `deriveSeed` hashes only the parts the application
67
+ passes, so the application still owns stable domain keys.
56
68
  - No multi-consumer queue semantics: publish separate logs when two systems
57
69
  must independently consume the same fact.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koota-kit",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Deterministic Koota simulation conventions: world lifecycle, dual RNG streams, safe object traits, and world-scoped event logs.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,6 +43,16 @@
43
43
  "default": "./dist/cjs/rng.cjs"
44
44
  }
45
45
  },
46
+ "./seed": {
47
+ "import": {
48
+ "types": "./dist/esm/seed.d.ts",
49
+ "default": "./dist/esm/seed.js"
50
+ },
51
+ "require": {
52
+ "types": "./dist/cjs/seed.d.cts",
53
+ "default": "./dist/cjs/seed.cjs"
54
+ }
55
+ },
46
56
  "./traits": {
47
57
  "import": {
48
58
  "types": "./dist/esm/traits/index.d.ts",
@@ -147,6 +157,9 @@
147
157
  "rng": [
148
158
  "./dist/esm/rng.d.ts"
149
159
  ],
160
+ "seed": [
161
+ "./dist/esm/seed.d.ts"
162
+ ],
150
163
  "traits": [
151
164
  "./dist/esm/traits/index.d.ts"
152
165
  ],