koota-kit 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +17 -8
- 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/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/docs/API.md +66 -0
- package/docs/ARCHITECTURE.md +15 -3
- package/package.json +14 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/jbcom/koota-kit/compare/v0.2.1...v0.3.0) (2026-10-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add named RNG substreams, seed derivation, and master seeds ([a120cb7](https://github.com/jbcom/koota-kit/commit/a120cb7209eac1d16b964c6f3af2d97061cfcfc3))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* type EntropySource so Web Crypto implementations satisfy it ([7b8d3cb](https://github.com/jbcom/koota-kit/commit/7b8d3cb3ee8a5a994b1578f3000270d40b5295f9))
|
|
14
|
+
|
|
3
15
|
## [0.2.1](https://github.com/jbcom/koota-kit/compare/v0.2.0...v0.2.1) (2026-10-06)
|
|
4
16
|
|
|
5
17
|
|
package/README.md
CHANGED
|
@@ -106,15 +106,20 @@ stream in query iteration order. Entity IDs contain process-global world and
|
|
|
106
106
|
generation bits. Derive a local stream from a stable domain key instead:
|
|
107
107
|
|
|
108
108
|
```ts
|
|
109
|
-
import {
|
|
109
|
+
import { nextU32, substream } from "koota-kit/rng";
|
|
110
|
+
import { deriveSeed } from "koota-kit/seed";
|
|
110
111
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
events: 0,
|
|
114
|
-
});
|
|
115
|
-
const terrainVariant = nextU32(tileRng.gen);
|
|
112
|
+
const tileSeed = deriveSeed(sim.seeds.gen, "tile", 12, 8);
|
|
113
|
+
const terrainVariant = nextU32(substream(tileSeed, "terrain"));
|
|
116
114
|
```
|
|
117
115
|
|
|
116
|
+
`deriveSeed` hashes length-prefixed parts into a 128-bit hex seed, so seed
|
|
117
|
+
chains such as master seed → run → level never collide by accident.
|
|
118
|
+
`substream(seed, name)` gives each generator its own stream; adding a new
|
|
119
|
+
named substream never shifts an existing one. `createMasterSeed()` makes a
|
|
120
|
+
128-bit root seed from `crypto.getRandomValues`, and accepts an injected
|
|
121
|
+
entropy source for tests.
|
|
122
|
+
|
|
118
123
|
### Object-valued traits need factories
|
|
119
124
|
|
|
120
125
|
Koota's structure-of-arrays layout can alias a bare object literal across
|
|
@@ -154,7 +159,8 @@ The root package exports the full API. Focused entry points are also available:
|
|
|
154
159
|
| Entry point | Purpose |
|
|
155
160
|
| --- | --- |
|
|
156
161
|
| `koota-kit/world` | World lifecycle, clock, Koota facade, world snapshots |
|
|
157
|
-
| `koota-kit/rng` | Seeded streams, bounded draws, snapshots |
|
|
162
|
+
| `koota-kit/rng` | Seeded streams, named substreams, bounded draws, snapshots |
|
|
163
|
+
| `koota-kit/seed` | Seed derivation and 128-bit master seeds |
|
|
158
164
|
| `koota-kit/traits` | Safe trait declaration |
|
|
159
165
|
| `koota-kit/eventLog` | World-scoped publish/drain logs |
|
|
160
166
|
|
|
@@ -169,7 +175,10 @@ For task-oriented usage, read the Sourcey guides on
|
|
|
169
175
|
|
|
170
176
|
## Errors and edge cases
|
|
171
177
|
|
|
172
|
-
- Seeds must be strings or finite numbers.
|
|
178
|
+
- Seeds must be strings or finite numbers. Substream names must be non-empty
|
|
179
|
+
strings.
|
|
180
|
+
- `createMasterSeed` throws when no entropy source exists or the source leaves
|
|
181
|
+
the buffer all zero.
|
|
173
182
|
- `advanceClock` accepts finite, non-negative `dt`; `0` is a valid paused tick.
|
|
174
183
|
- `nextInt` requires safe-integer bounds with `maxExclusive > minInclusive` and
|
|
175
184
|
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"}
|
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
|
+
}
|
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.0",
|
|
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
|
],
|