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 +20 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +20 -9
- package/dist/cjs/index.cjs +16 -4
- package/dist/cjs/index.d.cts +4 -2
- package/dist/cjs/index.d.cts.map +1 -1
- package/dist/cjs/rng.cjs +80 -0
- package/dist/cjs/rng.d.cts +34 -0
- package/dist/cjs/rng.d.cts.map +1 -1
- package/dist/cjs/seed.cjs +188 -0
- package/dist/cjs/seed.d.cts +25 -0
- package/dist/cjs/seed.d.cts.map +1 -0
- package/dist/cjs/traits/index.cjs +2 -4
- package/dist/cjs/traits/index.d.cts +2 -4
- package/dist/cjs/traits/index.d.cts.map +1 -1
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +7 -4
- package/dist/esm/rng.d.ts +34 -0
- package/dist/esm/rng.d.ts.map +1 -1
- package/dist/esm/rng.js +75 -0
- package/dist/esm/seed.d.ts +25 -0
- package/dist/esm/seed.d.ts.map +1 -0
- package/dist/esm/seed.js +182 -0
- package/dist/esm/traits/index.d.ts +2 -4
- package/dist/esm/traits/index.d.ts.map +1 -1
- package/dist/esm/traits/index.js +2 -4
- package/docs/API.md +66 -0
- package/docs/ARCHITECTURE.md +15 -3
- package/package.json +14 -1
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
|
-
|
|
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
|
|
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 {
|
|
111
|
+
import { nextU32, substream } from "koota-kit/rng";
|
|
112
|
+
import { deriveSeed } from "koota-kit/seed";
|
|
110
113
|
|
|
111
|
-
const
|
|
112
|
-
|
|
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`.
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// koota-kit — thin koota ECS conventions layer.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
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
|
|
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");
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -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";
|
package/dist/cjs/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"
|
|
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
|
+
}
|
package/dist/cjs/rng.d.cts
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.cts.map
|
package/dist/cjs/rng.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../../src/rng.ts"],"names":[],"mappings":"AAqBA,OAAO,UAAU,MAAM,YAAY,CAAC;
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"
|
|
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
|
-
//
|
|
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
|
|
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
|
package/dist/esm/rng.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../../src/rng.ts"],"names":[],"mappings":"AAqBA,OAAO,UAAU,MAAM,YAAY,CAAC;
|
|
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"}
|
package/dist/esm/seed.js
ADDED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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"}
|
package/dist/esm/traits/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `src/traits/` — trait-authoring conventions, kept in a LEAF sub-package.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
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
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -8,7 +8,8 @@ queries, and actions directly.
|
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
10
|
traits ───────────────> koota
|
|
11
|
-
|
|
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
|
|
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.
|
|
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
|
],
|