koota-kit 0.1.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/CONTRIBUTING.md +51 -0
  3. package/LICENSE +21 -0
  4. package/README.md +215 -0
  5. package/SECURITY.md +25 -0
  6. package/dist/cjs/eventLog.cjs +81 -0
  7. package/dist/cjs/eventLog.d.cts +49 -0
  8. package/dist/cjs/eventLog.d.cts.map +1 -0
  9. package/dist/cjs/index.cjs +36 -0
  10. package/dist/cjs/index.d.cts +9 -0
  11. package/dist/cjs/index.d.cts.map +1 -0
  12. package/dist/cjs/package.json +3 -0
  13. package/dist/cjs/rng.cjs +168 -0
  14. package/dist/cjs/rng.d.cts +86 -0
  15. package/dist/cjs/rng.d.cts.map +1 -0
  16. package/dist/cjs/traits/index.cjs +54 -0
  17. package/dist/cjs/traits/index.d.cts +48 -0
  18. package/dist/cjs/traits/index.d.cts.map +1 -0
  19. package/dist/cjs/world.cjs +111 -0
  20. package/dist/cjs/world.d.cts +83 -0
  21. package/dist/cjs/world.d.cts.map +1 -0
  22. package/dist/esm/eventLog.d.ts +49 -0
  23. package/dist/esm/eventLog.d.ts.map +1 -0
  24. package/dist/esm/eventLog.js +78 -0
  25. package/dist/esm/index.d.ts +9 -0
  26. package/dist/esm/index.d.ts.map +1 -0
  27. package/dist/esm/index.js +14 -0
  28. package/dist/esm/rng.d.ts +86 -0
  29. package/dist/esm/rng.d.ts.map +1 -0
  30. package/dist/esm/rng.js +154 -0
  31. package/dist/esm/traits/index.d.ts +48 -0
  32. package/dist/esm/traits/index.d.ts.map +1 -0
  33. package/dist/esm/traits/index.js +51 -0
  34. package/dist/esm/world.d.ts +83 -0
  35. package/dist/esm/world.d.ts.map +1 -0
  36. package/dist/esm/world.js +101 -0
  37. package/docs/API.md +119 -0
  38. package/docs/ARCHITECTURE.md +57 -0
  39. package/docs/assets/favicon.svg +1 -0
  40. package/docs/assets/koota-kit-hero.webp +0 -0
  41. package/docs/changelog.md +13 -0
  42. package/docs/contributing.md +28 -0
  43. package/examples/basic.mjs +27 -0
  44. package/examples/commonjs.cjs +8 -0
  45. package/package.json +173 -0
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ // Dual-layer deterministic PRNG (seedrandom-backed).
3
+ //
4
+ // `gen` — worldgen / layouts / spawn locations / map composition / one-time
5
+ // seeded decisions. Replays a world reliably from the gen seed alone, even
6
+ // when play diverges. Snapshot it once at world creation and you can
7
+ // re-derive the same world forever.
8
+ //
9
+ // `events` — per-tick runtime decisions (weather rolls, event triggers, mood
10
+ // drift, combat dice). Diverges based on play history. Snapshot/restore
11
+ // independently so a worldgen replay doesn't carry play state with it.
12
+ //
13
+ // Why two streams: a single stream couples worldgen to play, so seeding a
14
+ // "same map, fresh playthrough" scenario requires you to either re-derive
15
+ // worldgen mid-restore (fragile) or snapshot the stream at the exact tick
16
+ // worldgen finished (brittle to system ordering). Two streams: snapshot gen
17
+ // once, never touch it again; snapshot events on demand. Each layer's
18
+ // determinism contract is independent.
19
+ //
20
+ // Forbidden in sim code: `Math.random()`. Use `nextFloat(rng.gen)` or
21
+ // `nextFloat(rng.events)` depending on the layer.
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.createRng = createRng;
27
+ exports.nextU32 = nextU32;
28
+ exports.nextFloat = nextFloat;
29
+ exports.nextInt = nextInt;
30
+ exports.chance = chance;
31
+ exports.snapshotStream = snapshotStream;
32
+ exports.snapshotLayers = snapshotLayers;
33
+ exports.restoreStream = restoreStream;
34
+ exports.restoreLayers = restoreLayers;
35
+ const seedrandom_1 = __importDefault(require("seedrandom"));
36
+ function assertSeed(seed, name) {
37
+ if (typeof seed !== "string" && typeof seed !== "number") {
38
+ throw new TypeError(`createRng: seeds.${name} must be a string or finite number.`);
39
+ }
40
+ if (typeof seed === "number" && !Number.isFinite(seed)) {
41
+ throw new RangeError(`createRng: seeds.${name} must be finite; received ${String(seed)}.`);
42
+ }
43
+ }
44
+ function makeStream(seed) {
45
+ const prng = (0, seedrandom_1.default)(String(seed), { state: true });
46
+ return { prng };
47
+ }
48
+ /**
49
+ * Create both RNG layers from their seeds. Called once at world creation
50
+ * (see `createSimWorld` in `./world`) — `gen` and `events` are independent
51
+ * seedrandom instances from this point on, so consuming one never perturbs
52
+ * the other's sequence.
53
+ */
54
+ function createRng(seeds) {
55
+ if (typeof seeds !== "object" || seeds === null) {
56
+ throw new TypeError("createRng: seeds must be an object with gen and events values.");
57
+ }
58
+ assertSeed(seeds.gen, "gen");
59
+ assertSeed(seeds.events, "events");
60
+ return {
61
+ gen: makeStream(seeds.gen),
62
+ events: makeStream(seeds.events),
63
+ };
64
+ }
65
+ /**
66
+ * Draw the next raw unsigned 32-bit integer from `stream` (range
67
+ * `[0, 2**32)`). The lowest-level draw primitive — `nextFloat`, `nextInt`,
68
+ * and `chance` are all expressed in terms of `quick()`, not this function;
69
+ * reach for `nextU32` directly only when you need the full 32 bits of
70
+ * entropy (e.g. seeding another PRNG, hashing).
71
+ */
72
+ function nextU32(stream) {
73
+ return stream.prng.int32() >>> 0;
74
+ }
75
+ /**
76
+ * Draw the next float from `stream` in the half-open range `[0, 1)`. The
77
+ * primitive every other draw helper in this module (`nextInt`, `chance`)
78
+ * is built on.
79
+ */
80
+ function nextFloat(stream) {
81
+ return stream.prng.quick();
82
+ }
83
+ /**
84
+ * Draw a random integer from `stream` in the half-open range
85
+ * `[minInclusive, maxExclusive)`. Bounds must be safe integers, the range must
86
+ * be non-empty and no larger than `2**32`, and valid calls consume exactly one
87
+ * `nextFloat` draw. Invalid calls do not consume the stream.
88
+ */
89
+ function nextInt(stream, minInclusive, maxExclusive) {
90
+ if (!Number.isSafeInteger(minInclusive) || !Number.isSafeInteger(maxExclusive)) {
91
+ throw new TypeError("nextInt: bounds must be safe integers.");
92
+ }
93
+ if (maxExclusive <= minInclusive) {
94
+ throw new RangeError(`nextInt: maxExclusive (${maxExclusive}) must be greater than minInclusive (${minInclusive}).`);
95
+ }
96
+ if (maxExclusive - minInclusive > 2 ** 32) {
97
+ throw new RangeError("nextInt: the requested range cannot exceed 2**32 values.");
98
+ }
99
+ const span = maxExclusive - minInclusive;
100
+ return minInclusive + Math.floor(nextFloat(stream) * span);
101
+ }
102
+ /**
103
+ * Roll a boolean weighted by finite probability `p` in `[0, 1]` (`0` never
104
+ * true, `1` always true). Valid calls consume exactly one `nextFloat` draw
105
+ * from `stream`, so calling `chance` where a system might otherwise skip a
106
+ * draw entirely will shift every later draw on that stream — keep draw counts
107
+ * deterministic across codepaths that must replay identically.
108
+ */
109
+ function chance(stream, p) {
110
+ if (!Number.isFinite(p) || p < 0 || p > 1) {
111
+ throw new RangeError(`chance: probability must be a finite number in [0, 1]; received ${p}.`);
112
+ }
113
+ return nextFloat(stream) < p;
114
+ }
115
+ /**
116
+ * Capture `stream`'s internal seedrandom state as a plain, JSON-serializable
117
+ * object (deep-cloned, so later draws on `stream` cannot mutate the
118
+ * snapshot). Restoring it with `restoreStream` reproduces every subsequent
119
+ * draw byte-exact.
120
+ */
121
+ function snapshotStream(stream) {
122
+ const state = stream.prng.state();
123
+ return { state: { i: state.i, j: state.j, S: [...state.S] } };
124
+ }
125
+ /**
126
+ * Snapshot both RNG layers at once. See `snapshotStream` — this just applies
127
+ * it to `gen` and `events` independently and returns both.
128
+ */
129
+ function snapshotLayers(rng) {
130
+ return { gen: snapshotStream(rng.gen), events: snapshotStream(rng.events) };
131
+ }
132
+ /**
133
+ * Rebuild an `RngStream` from a snapshot taken by `snapshotStream`. The next
134
+ * draw on the returned stream reproduces the exact next draw the snapshotted
135
+ * stream would have produced — the seed string passed to `seedrandom` here
136
+ * is irrelevant because `{ state: snap.state }` fully overrides it. Malformed
137
+ * persisted state is rejected before seedrandom sees it.
138
+ */
139
+ function restoreStream(snap) {
140
+ const state = snap?.state;
141
+ if (!state ||
142
+ !Number.isInteger(state.i) ||
143
+ state.i < 0 ||
144
+ state.i > 255 ||
145
+ !Number.isInteger(state.j) ||
146
+ state.j < 0 ||
147
+ state.j > 255 ||
148
+ !Array.isArray(state.S) ||
149
+ state.S.length !== 256 ||
150
+ state.S.some((value) => !Number.isInteger(value) || value < 0 || value > 255) ||
151
+ new Set(state.S).size !== 256) {
152
+ throw new TypeError("restoreStream: snapshot must contain a valid seedrandom ARC4 state.");
153
+ }
154
+ // Clone the permutation so the same immutable snapshot can safely seed more
155
+ // than one replay, even if seedrandom changes its internal copy behavior.
156
+ const prng = (0, seedrandom_1.default)("", { state: { i: state.i, j: state.j, S: [...state.S] } });
157
+ return { prng };
158
+ }
159
+ /**
160
+ * Rebuild both RNG layers from a snapshot taken by `snapshotLayers`. See
161
+ * `restoreStream` — applied independently to `gen` and `events`.
162
+ */
163
+ function restoreLayers(snap) {
164
+ if (typeof snap !== "object" || snap === null) {
165
+ throw new TypeError("restoreLayers: snapshot must contain gen and events stream states.");
166
+ }
167
+ return { gen: restoreStream(snap.gen), events: restoreStream(snap.events) };
168
+ }
@@ -0,0 +1,86 @@
1
+ import seedrandom from "seedrandom";
2
+ type State = seedrandom.State.Arc4;
3
+ type Stateful = seedrandom.StatefulPRNG<State>;
4
+ export type RngStream = {
5
+ prng: Stateful;
6
+ };
7
+ export type RngLayers = {
8
+ gen: RngStream;
9
+ events: RngStream;
10
+ };
11
+ export type RngSeeds = {
12
+ /** World-generation seed (immutable, defines the world's identity). */
13
+ readonly gen: string | number;
14
+ /** Per-playthrough events seed (changes per cycle for new playthroughs of the same world). */
15
+ readonly events: string | number;
16
+ };
17
+ /**
18
+ * Create both RNG layers from their seeds. Called once at world creation
19
+ * (see `createSimWorld` in `./world`) — `gen` and `events` are independent
20
+ * seedrandom instances from this point on, so consuming one never perturbs
21
+ * the other's sequence.
22
+ */
23
+ export declare function createRng(seeds: RngSeeds): RngLayers;
24
+ /**
25
+ * Draw the next raw unsigned 32-bit integer from `stream` (range
26
+ * `[0, 2**32)`). The lowest-level draw primitive — `nextFloat`, `nextInt`,
27
+ * and `chance` are all expressed in terms of `quick()`, not this function;
28
+ * reach for `nextU32` directly only when you need the full 32 bits of
29
+ * entropy (e.g. seeding another PRNG, hashing).
30
+ */
31
+ export declare function nextU32(stream: RngStream): number;
32
+ /**
33
+ * Draw the next float from `stream` in the half-open range `[0, 1)`. The
34
+ * primitive every other draw helper in this module (`nextInt`, `chance`)
35
+ * is built on.
36
+ */
37
+ export declare function nextFloat(stream: RngStream): number;
38
+ /**
39
+ * Draw a random integer from `stream` in the half-open range
40
+ * `[minInclusive, maxExclusive)`. Bounds must be safe integers, the range must
41
+ * be non-empty and no larger than `2**32`, and valid calls consume exactly one
42
+ * `nextFloat` draw. Invalid calls do not consume the stream.
43
+ */
44
+ export declare function nextInt(stream: RngStream, minInclusive: number, maxExclusive: number): number;
45
+ /**
46
+ * Roll a boolean weighted by finite probability `p` in `[0, 1]` (`0` never
47
+ * true, `1` always true). Valid calls consume exactly one `nextFloat` draw
48
+ * from `stream`, so calling `chance` where a system might otherwise skip a
49
+ * draw entirely will shift every later draw on that stream — keep draw counts
50
+ * deterministic across codepaths that must replay identically.
51
+ */
52
+ export declare function chance(stream: RngStream, p: number): boolean;
53
+ export type RngStreamSnapshot = {
54
+ state: State;
55
+ };
56
+ export type RngLayersSnapshot = {
57
+ gen: RngStreamSnapshot;
58
+ events: RngStreamSnapshot;
59
+ };
60
+ /**
61
+ * Capture `stream`'s internal seedrandom state as a plain, JSON-serializable
62
+ * object (deep-cloned, so later draws on `stream` cannot mutate the
63
+ * snapshot). Restoring it with `restoreStream` reproduces every subsequent
64
+ * draw byte-exact.
65
+ */
66
+ export declare function snapshotStream(stream: RngStream): RngStreamSnapshot;
67
+ /**
68
+ * Snapshot both RNG layers at once. See `snapshotStream` — this just applies
69
+ * it to `gen` and `events` independently and returns both.
70
+ */
71
+ export declare function snapshotLayers(rng: RngLayers): RngLayersSnapshot;
72
+ /**
73
+ * Rebuild an `RngStream` from a snapshot taken by `snapshotStream`. The next
74
+ * draw on the returned stream reproduces the exact next draw the snapshotted
75
+ * stream would have produced — the seed string passed to `seedrandom` here
76
+ * is irrelevant because `{ state: snap.state }` fully overrides it. Malformed
77
+ * persisted state is rejected before seedrandom sees it.
78
+ */
79
+ export declare function restoreStream(snap: RngStreamSnapshot): RngStream;
80
+ /**
81
+ * Rebuild both RNG layers from a snapshot taken by `snapshotLayers`. See
82
+ * `restoreStream` — applied independently to `gen` and `events`.
83
+ */
84
+ export declare function restoreLayers(snap: RngLayersSnapshot): RngLayers;
85
+ export {};
86
+ //# sourceMappingURL=rng.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../../src/rng.ts"],"names":[],"mappings":"AAqBA,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,KAAK,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC;AACnC,KAAK,QAAQ,GAAG,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;AAE/C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,SAAS,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC,CAAC;AAgBF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAUpD;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAc7F;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AACjD,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAEtF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,iBAAiB,CAGnE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,iBAAiB,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAqBhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAKhE"}
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ /**
3
+ * `src/traits/` — trait-authoring conventions, kept in a LEAF sub-package.
4
+ *
5
+ * Packaging pattern (from medieval-hexagon-gameboard): trait declarations
6
+ * live in a leaf sub-package with ZERO sibling imports — this file imports
7
+ * only `koota` (the npm dep) and pure types, never runtime values from
8
+ * sibling modules like ./world or ./rng. That is what prevents the
9
+ * koota ↔ world-facade ↔ game-scenario top-level evaluation cycle that a
10
+ * flat layout only dodges accidentally. Games adopting this package should
11
+ * follow the same rule: declare traits in their own leaf module, exported
12
+ * from exactly one location so trait reference-identity holds regardless of
13
+ * which subpath a consumer imports from.
14
+ *
15
+ * The AoS-aliasing footgun this module guards against (timber-town's
16
+ * documented convention): object-valued fields in a SoA schema must use the
17
+ * AoS-factory pattern (`() => ({...})`) so each entity gets its own object
18
+ * instance — koota's SoA pattern would otherwise alias state across
19
+ * entities (one shared object literal mutated by every entity that has the
20
+ * trait).
21
+ *
22
+ * @module
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.defineTrait = defineTrait;
26
+ const koota_1 = require("koota");
27
+ /**
28
+ * Thin wrapper over koota's `trait()` that enforces the AoS-factory
29
+ * convention for object-valued fields, at both type level (see SafeSchema)
30
+ * and runtime (throws with an explanation instead of silently aliasing).
31
+ *
32
+ * Accepted forms:
33
+ * defineTrait({ count: 0, label: "x" }) // SoA, primitives
34
+ * defineTrait({ pos: () => ({ x: 0, y: 0 }) }) // SoA, per-field factory
35
+ * defineTrait(() => ({ pos: { x: 0, y: 0 } })) // whole-trait AoS factory
36
+ *
37
+ * Rejected (the footgun):
38
+ * defineTrait({ pos: { x: 0, y: 0 } }) // shared literal → aliased
39
+ */
40
+ function defineTrait(schema) {
41
+ if (typeof schema !== "function") {
42
+ for (const [key, value] of Object.entries(schema)) {
43
+ if (value !== null && typeof value === "object") {
44
+ throw new TypeError(`defineTrait: field "${key}" is a bare object literal. koota's SoA layout ` +
45
+ `would share this single instance across every entity holding the trait ` +
46
+ `(state aliasing). Use an AoS factory instead: ${key}: () => ({...}).`);
47
+ }
48
+ }
49
+ }
50
+ // Pin Koota's normalization generic to the caller's schema. Passing the
51
+ // guard intersection through inference makes Koota 0.6.x normalize
52
+ // `S & SafeSchema<S>` and produces a narrower, incompatible Trait type.
53
+ return (0, koota_1.trait)(schema);
54
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `src/traits/` — trait-authoring conventions, kept in a LEAF sub-package.
3
+ *
4
+ * Packaging pattern (from medieval-hexagon-gameboard): trait declarations
5
+ * live in a leaf sub-package with ZERO sibling imports — this file imports
6
+ * only `koota` (the npm dep) and pure types, never runtime values from
7
+ * sibling modules like ./world or ./rng. That is what prevents the
8
+ * koota ↔ world-facade ↔ game-scenario top-level evaluation cycle that a
9
+ * flat layout only dodges accidentally. Games adopting this package should
10
+ * follow the same rule: declare traits in their own leaf module, exported
11
+ * from exactly one location so trait reference-identity holds regardless of
12
+ * which subpath a consumer imports from.
13
+ *
14
+ * The AoS-aliasing footgun this module guards against (timber-town's
15
+ * documented convention): object-valued fields in a SoA schema must use the
16
+ * AoS-factory pattern (`() => ({...})`) so each entity gets its own object
17
+ * instance — koota's SoA pattern would otherwise alias state across
18
+ * entities (one shared object literal mutated by every entity that has the
19
+ * trait).
20
+ *
21
+ * @module
22
+ */
23
+ import type { Norm, Schema, Trait } from "koota";
24
+ /**
25
+ * Compile-time guard: any object-valued field in a SoA schema must be an
26
+ * AoS factory (`() => ({...})`), never a bare object literal. Fields that
27
+ * are functions, primitives, null, or undefined pass through unchanged;
28
+ * bare object/array literals map to `never`, producing a type error at the
29
+ * call site.
30
+ */
31
+ export type SafeSchema<S> = S extends (...args: never[]) => unknown ? S : {
32
+ [K in keyof S]: S[K] extends (...args: never[]) => unknown ? S[K] : S[K] extends object ? never : S[K];
33
+ };
34
+ /**
35
+ * Thin wrapper over koota's `trait()` that enforces the AoS-factory
36
+ * convention for object-valued fields, at both type level (see SafeSchema)
37
+ * and runtime (throws with an explanation instead of silently aliasing).
38
+ *
39
+ * Accepted forms:
40
+ * defineTrait({ count: 0, label: "x" }) // SoA, primitives
41
+ * defineTrait({ pos: () => ({ x: 0, y: 0 }) }) // SoA, per-field factory
42
+ * defineTrait(() => ({ pos: { x: 0, y: 0 } })) // whole-trait AoS factory
43
+ *
44
+ * Rejected (the footgun):
45
+ * defineTrait({ pos: { x: 0, y: 0 } }) // shared literal → aliased
46
+ */
47
+ export declare function defineTrait<S extends Schema>(schema: S & SafeSchema<S>): Trait<Norm<S>>;
48
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/traits/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAGjD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GAC/D,CAAC,GACD;KACG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GACtD,CAAC,CAAC,CAAC,CAAC,GACJ,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GACjB,KAAK,GACL,CAAC,CAAC,CAAC,CAAC;CACX,CAAC;AAEN;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAgBvF"}
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ // World facade. Wraps koota's createWorld + trait helper so the rest of a
3
+ // game's codebase imports from here and lifecycle/determinism conventions stay
4
+ // behind one boundary.
5
+ //
6
+ // Games declare their traits with `defineTrait` (see ./traits) and drive the
7
+ // sim through the WorldHandle: world creation, trait re-export, tick clock,
8
+ // dual-layer RNG plumbing.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.trait = exports.relation = exports.createActions = void 0;
11
+ exports.createSimWorld = createSimWorld;
12
+ exports.destroySimWorld = destroySimWorld;
13
+ exports.advanceClock = advanceClock;
14
+ exports.snapshotWorld = snapshotWorld;
15
+ exports.restoreWorldHeader = restoreWorldHeader;
16
+ const koota_1 = require("koota");
17
+ Object.defineProperty(exports, "createActions", { enumerable: true, get: function () { return koota_1.createActions; } });
18
+ Object.defineProperty(exports, "relation", { enumerable: true, get: function () { return koota_1.relation; } });
19
+ Object.defineProperty(exports, "trait", { enumerable: true, get: function () { return koota_1.trait; } });
20
+ const rng_js_1 = require("./rng.cjs");
21
+ /**
22
+ * Create a fresh `WorldHandle`: a new koota world, both RNG layers seeded
23
+ * from `seeds`, a zeroed clock, and an empty scratch map. This is the one
24
+ * entry point for standing up a sim — call it once per world (per game
25
+ * session, per test, per worldgen run) rather than constructing the koota
26
+ * world and RNG separately, so the facade's invariants (immutable `seeds`,
27
+ * clock starting at zero, scratch starting empty) always hold.
28
+ */
29
+ function createSimWorld(seeds) {
30
+ // Validate before allocating a Koota world: invalid input must not consume a
31
+ // process-global world id or leave a world that the caller cannot destroy.
32
+ const rng = (0, rng_js_1.createRng)(seeds);
33
+ const immutableSeeds = Object.freeze({ gen: seeds.gen, events: seeds.events });
34
+ return {
35
+ world: (0, koota_1.createWorld)(),
36
+ rng,
37
+ seeds: immutableSeeds,
38
+ clock: { tickIndex: 0, simSeconds: 0 },
39
+ scratch: new Map(),
40
+ };
41
+ }
42
+ /**
43
+ * Tear down a simulation handle. This clears facade-owned scratch state and
44
+ * releases Koota's process-global world id. The operation is idempotent, which
45
+ * makes it safe to call from cleanup paths that may run more than once.
46
+ */
47
+ function destroySimWorld(handle) {
48
+ handle.scratch.clear();
49
+ if (handle.world.isInitialized) {
50
+ handle.world.destroy();
51
+ }
52
+ }
53
+ /**
54
+ * Advance `handle`'s clock by one fixed-timestep tick: increments
55
+ * `tickIndex` by 1 and accumulates `dt` seconds into `simSeconds`. Call once
56
+ * per sim tick, before running that tick's systems, so `clock` always
57
+ * reflects "how far the sim has progressed" rather than "how far it will
58
+ * have progressed after this tick's work."
59
+ */
60
+ function advanceClock(handle, dt) {
61
+ if (!Number.isFinite(dt) || dt < 0) {
62
+ throw new RangeError(`advanceClock: dt must be a finite, non-negative number; received ${dt}.`);
63
+ }
64
+ if (!Number.isSafeInteger(handle.clock.tickIndex) || handle.clock.tickIndex < 0) {
65
+ throw new RangeError("advanceClock: clock.tickIndex must be a non-negative safe integer.");
66
+ }
67
+ if (!Number.isFinite(handle.clock.simSeconds) || handle.clock.simSeconds < 0) {
68
+ throw new RangeError("advanceClock: clock.simSeconds must be finite and non-negative.");
69
+ }
70
+ const tickIndex = handle.clock.tickIndex + 1;
71
+ const simSeconds = handle.clock.simSeconds + dt;
72
+ if (!Number.isSafeInteger(tickIndex) || !Number.isFinite(simSeconds)) {
73
+ throw new RangeError("advanceClock: advancing the clock would exceed its numeric range.");
74
+ }
75
+ handle.clock.tickIndex = tickIndex;
76
+ handle.clock.simSeconds = simSeconds;
77
+ }
78
+ /**
79
+ * Capture `handle`'s RNG state and clock into a plain, JSON-serializable
80
+ * `WorldSnapshot`. Pair with `restoreWorldHeader` for save/load — restoring
81
+ * a snapshot reproduces every subsequent RNG draw on both layers byte-exact
82
+ * and resumes the clock from the exact tick/second it was captured at.
83
+ */
84
+ function snapshotWorld(handle) {
85
+ return {
86
+ rng: (0, rng_js_1.snapshotLayers)(handle.rng),
87
+ clock: { tickIndex: handle.clock.tickIndex, simSeconds: handle.clock.simSeconds },
88
+ };
89
+ }
90
+ /**
91
+ * Restore `handle`'s RNG layers and clock from a `WorldSnapshot` taken by
92
+ * `snapshotWorld`. Mutates `handle` in place (replaces `handle.rng` and
93
+ * `handle.clock` with freshly rebuilt values) — entity/component state is
94
+ * untouched, since the facade never captured it in the first place; restore
95
+ * that separately using whatever serialization your game chose.
96
+ */
97
+ function restoreWorldHeader(handle, snap) {
98
+ const clock = snap?.clock;
99
+ if (!clock ||
100
+ !Number.isSafeInteger(clock.tickIndex) ||
101
+ clock.tickIndex < 0 ||
102
+ !Number.isFinite(clock.simSeconds) ||
103
+ clock.simSeconds < 0) {
104
+ throw new TypeError("restoreWorldHeader: snapshot.clock must contain a non-negative safe integer tickIndex and finite, non-negative simSeconds.");
105
+ }
106
+ // Build and validate every replacement before mutating the handle so a bad
107
+ // persisted snapshot cannot leave it half-restored.
108
+ const rng = (0, rng_js_1.restoreLayers)(snap.rng);
109
+ handle.rng = rng;
110
+ handle.clock = { tickIndex: clock.tickIndex, simSeconds: clock.simSeconds };
111
+ }
@@ -0,0 +1,83 @@
1
+ import type { World } from "koota";
2
+ import { createActions, relation, trait } from "koota";
3
+ import type { RngLayers, RngLayersSnapshot, RngSeeds } from "./rng.cjs";
4
+ export type { Entity } from "koota";
5
+ export type { World };
6
+ export { createActions, relation, trait };
7
+ export type WorldHandle = {
8
+ readonly world: World;
9
+ rng: RngLayers;
10
+ /**
11
+ * The immutable seeds this world was created from. Stored so systems
12
+ * can derive deterministic per-entity seeds (e.g. noise keyed on an
13
+ * entity's stable grid coordinates) WITHOUT lazily consuming the
14
+ * rng.gen stream in query-iteration order — koota entity ids are not
15
+ * reset between worlds, so iteration-order rng draws are
16
+ * non-deterministic across two runs in one process. Derive from
17
+ * `seeds.gen`, never a lazy draw.
18
+ */
19
+ readonly seeds: Readonly<RngSeeds>;
20
+ clock: {
21
+ tickIndex: number;
22
+ simSeconds: number;
23
+ };
24
+ /**
25
+ * Per-system scratch caches owned by the WorldHandle (so they lifecycle with
26
+ * the world, not at module scope). Systems read/write via well-known keys.
27
+ * Examples: per-region noise fns, pathfinding grids, behavior-tree instances.
28
+ */
29
+ readonly scratch: Map<string, unknown>;
30
+ };
31
+ /**
32
+ * Create a fresh `WorldHandle`: a new koota world, both RNG layers seeded
33
+ * from `seeds`, a zeroed clock, and an empty scratch map. This is the one
34
+ * entry point for standing up a sim — call it once per world (per game
35
+ * session, per test, per worldgen run) rather than constructing the koota
36
+ * world and RNG separately, so the facade's invariants (immutable `seeds`,
37
+ * clock starting at zero, scratch starting empty) always hold.
38
+ */
39
+ export declare function createSimWorld(seeds: RngSeeds): WorldHandle;
40
+ /**
41
+ * Tear down a simulation handle. This clears facade-owned scratch state and
42
+ * releases Koota's process-global world id. The operation is idempotent, which
43
+ * makes it safe to call from cleanup paths that may run more than once.
44
+ */
45
+ export declare function destroySimWorld(handle: WorldHandle): void;
46
+ /**
47
+ * Advance `handle`'s clock by one fixed-timestep tick: increments
48
+ * `tickIndex` by 1 and accumulates `dt` seconds into `simSeconds`. Call once
49
+ * per sim tick, before running that tick's systems, so `clock` always
50
+ * reflects "how far the sim has progressed" rather than "how far it will
51
+ * have progressed after this tick's work."
52
+ */
53
+ export declare function advanceClock(handle: WorldHandle, dt: number): void;
54
+ /**
55
+ * The serializable "header" of a world: both RNG layers' byte-exact state
56
+ * plus the clock. Deliberately excludes entity/component state — koota
57
+ * entities are the caller's data to serialize however their game needs
58
+ * (full ECS dump, delta log, etc.); this snapshot only covers what the
59
+ * facade itself owns.
60
+ */
61
+ export type WorldSnapshot = {
62
+ rng: RngLayersSnapshot;
63
+ clock: {
64
+ tickIndex: number;
65
+ simSeconds: number;
66
+ };
67
+ };
68
+ /**
69
+ * Capture `handle`'s RNG state and clock into a plain, JSON-serializable
70
+ * `WorldSnapshot`. Pair with `restoreWorldHeader` for save/load — restoring
71
+ * a snapshot reproduces every subsequent RNG draw on both layers byte-exact
72
+ * and resumes the clock from the exact tick/second it was captured at.
73
+ */
74
+ export declare function snapshotWorld(handle: WorldHandle): WorldSnapshot;
75
+ /**
76
+ * Restore `handle`'s RNG layers and clock from a `WorldSnapshot` taken by
77
+ * `snapshotWorld`. Mutates `handle` in place (replaces `handle.rng` and
78
+ * `handle.clock` with freshly rebuilt values) — entity/component state is
79
+ * untouched, since the facade never captured it in the first place; restore
80
+ * that separately using whatever serialization your game chose.
81
+ */
82
+ export declare function restoreWorldHeader(handle: WorldHandle, snap: WorldSnapshot): void;
83
+ //# sourceMappingURL=world.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"world.d.ts","sourceRoot":"","sources":["../../src/world.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EAAE,aAAa,EAAe,QAAQ,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAGvE,YAAY,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AACpC,YAAY,EAAE,KAAK,EAAE,CAAC;AACtB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAE1C,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,GAAG,EAAE,SAAS,CAAC;IACf;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACnC,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,WAAW,CAY3D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAKzD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAiBlE;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,iBAAiB,CAAC;IACvB,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAClD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,aAAa,CAKhE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CAkBjF"}
@@ -0,0 +1,49 @@
1
+ import type { WorldHandle } from "./world.js";
2
+ /**
3
+ * A published event log: append here, drain or peek from the other side.
4
+ *
5
+ * Returned as an object rather than loose functions so a system declares its log
6
+ * ONCE at module scope and the key is written in exactly one place. Two string
7
+ * literals for the same log is the typo that produces an event stream nobody
8
+ * receives, and no type checker can catch it.
9
+ */
10
+ export type EventLog<T> = {
11
+ /** The scratch key, exposed for debugging and for snapshot tooling. */
12
+ readonly key: string;
13
+ /** Append an event. Called by the system that owns the fact. */
14
+ push: (handle: WorldHandle, event: T) => void;
15
+ /**
16
+ * Take every event and EMPTY the log — for the single consuming system.
17
+ *
18
+ * If two systems both need the same events, that is a design question and not
19
+ * a reason to drain twice: publish two logs, or have the second peek. A log
20
+ * drained by two consumers delivers each event to whichever ran first.
21
+ */
22
+ drain: (handle: WorldHandle) => T[];
23
+ /**
24
+ * Read every event WITHOUT emptying — for observers: harnesses, tests, HUDs.
25
+ *
26
+ * Returns a readonly copy, so a caller cannot mutate the live array and
27
+ * cannot hold a reference that changes underneath them.
28
+ */
29
+ peek: (handle: WorldHandle) => readonly T[];
30
+ /** Drop everything without reading. For run teardown and test setup. */
31
+ clear: (handle: WorldHandle) => void;
32
+ /** How many events are pending, without allocating a copy. */
33
+ size: (handle: WorldHandle) => number;
34
+ };
35
+ /**
36
+ * Declare an event log under `key`.
37
+ *
38
+ * The array is created lazily on first use and lives in `handle.scratch`, so it
39
+ * lifecycles with the world: a new run starts with empty logs and cannot inherit
40
+ * the last one's events. That is not a detail — a log at module scope survives
41
+ * the world that produced it, and stale events replay into a fresh run.
42
+ *
43
+ * Keys should be namespaced by system (`"combat:hits"`, `"voices:events"`) for
44
+ * the same reason trait names are: scratch is one flat map shared by every
45
+ * system in the world. Empty keys are rejected, as is a collision with an
46
+ * existing non-array scratch value.
47
+ */
48
+ export declare function defineEventLog<T>(key: string): EventLog<T>;
49
+ //# sourceMappingURL=eventLog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventLog.d.ts","sourceRoot":"","sources":["../../src/eventLog.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;GAOG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;IACxB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9C;;;;;;OAMG;IACH,KAAK,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,CAAC,EAAE,CAAC;IACpC;;;;;OAKG;IACH,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,SAAS,CAAC,EAAE,CAAC;IAC5C,wEAAwE;IACxE,KAAK,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACrC,8DAA8D;IAC9D,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,CAAC;CACvC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CA6C1D"}