@moku-labs/game 0.0.1 → 0.0.2
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/README.md +424 -37
- package/dist/assets.d.mts +161 -0
- package/dist/assets.mjs +4729 -0
- package/dist/component-DGg5DqKK.mjs +44 -0
- package/dist/control.d.mts +162 -0
- package/dist/control.mjs +758 -0
- package/dist/define-sFoO3y6X.d.mts +263 -0
- package/dist/headless-CvamCUcR.mjs +186 -0
- package/dist/headless-KaSWcd0s.d.mts +181 -0
- package/dist/index.d.mts +1204 -50
- package/dist/index.mjs +20978 -2010
- package/dist/inspect.d.mts +109 -0
- package/dist/inspect.mjs +519 -0
- package/dist/jsx-dev-runtime.d.mts +2 -0
- package/dist/jsx-dev-runtime.mjs +2 -0
- package/dist/jsx-runtime.d.mts +2 -0
- package/dist/jsx-runtime.mjs +2 -0
- package/dist/memory-CvgdnsQO.mjs +259 -0
- package/dist/registry-DlpRCibU.mjs +384 -0
- package/dist/runtime-DRlwxkIv.mjs +182 -0
- package/dist/runtime-DiOTkDZz.d.mts +77 -0
- package/dist/session-DmAxY6Ll.mjs +145 -0
- package/dist/testing.d.mts +23 -111
- package/dist/testing.mjs +12 -289
- package/dist/types-BxkNNYul.d.mts +2840 -0
- package/dist/types-CTPS9GBu.d.mts +742 -0
- package/dist/types-DD-QrG_z.d.mts +588 -0
- package/dist/types-DWILGrPn.d.mts +622 -0
- package/dist/types-DYLnSgMI.d.mts +570 -0
- package/dist/types-JNc_UQBo.d.mts +2896 -0
- package/dist/types-yg_ywtT-.d.mts +1859 -0
- package/dist/visual-BDUHSRvf.mjs +734 -0
- package/package.json +26 -4
- package/dist/registry-DWV5C0Mf.mjs +0 -666
- package/dist/types-BfsmUzLC.d.mts +0 -1908
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moku-labs/game",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "2D puzzle game engine on TypeScript and PixiJS, built on @moku-labs/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -14,6 +14,26 @@
|
|
|
14
14
|
"./testing": {
|
|
15
15
|
"types": "./dist/testing.d.mts",
|
|
16
16
|
"default": "./dist/testing.mjs"
|
|
17
|
+
},
|
|
18
|
+
"./assets": {
|
|
19
|
+
"types": "./dist/assets.d.mts",
|
|
20
|
+
"default": "./dist/assets.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./inspect": {
|
|
23
|
+
"types": "./dist/inspect.d.mts",
|
|
24
|
+
"default": "./dist/inspect.mjs"
|
|
25
|
+
},
|
|
26
|
+
"./control": {
|
|
27
|
+
"types": "./dist/control.d.mts",
|
|
28
|
+
"default": "./dist/control.mjs"
|
|
29
|
+
},
|
|
30
|
+
"./jsx-runtime": {
|
|
31
|
+
"types": "./dist/jsx-runtime.d.mts",
|
|
32
|
+
"default": "./dist/jsx-runtime.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./jsx-dev-runtime": {
|
|
35
|
+
"types": "./dist/jsx-dev-runtime.d.mts",
|
|
36
|
+
"default": "./dist/jsx-dev-runtime.mjs"
|
|
17
37
|
}
|
|
18
38
|
},
|
|
19
39
|
"license": "MIT",
|
|
@@ -56,6 +76,7 @@
|
|
|
56
76
|
"@arethetypeswrong/cli": "0.18.3",
|
|
57
77
|
"@arethetypeswrong/core": "0.18.3",
|
|
58
78
|
"@biomejs/biome": "2.4.16",
|
|
79
|
+
"@formatjs/icu-messageformat-parser": "3.5.20",
|
|
59
80
|
"@moku-labs/ci": "1.2.6",
|
|
60
81
|
"@types/bun": "1.3.14",
|
|
61
82
|
"@vitest/coverage-istanbul": "4.0.18",
|
|
@@ -75,9 +96,10 @@
|
|
|
75
96
|
"vitest": "4.0.18"
|
|
76
97
|
},
|
|
77
98
|
"dependencies": {
|
|
78
|
-
"@moku-labs/common": "0.3.
|
|
79
|
-
"@moku-labs/core": "1.
|
|
80
|
-
"immer": "11.1.18"
|
|
99
|
+
"@moku-labs/common": "0.3.3",
|
|
100
|
+
"@moku-labs/core": "1.7.0",
|
|
101
|
+
"immer": "11.1.18",
|
|
102
|
+
"yoga-layout": "3.2.1"
|
|
81
103
|
},
|
|
82
104
|
"publishConfig": {
|
|
83
105
|
"access": "public"
|
|
@@ -1,666 +0,0 @@
|
|
|
1
|
-
import { applyPatches, createDraft, enablePatches, finishDraft } from "immer";
|
|
2
|
-
//#region src/plugins/model/store/drafts.ts
|
|
3
|
-
/** Root order of a commit result, so two commits never report the same roots in another order. */
|
|
4
|
-
const rootOrder = Object.freeze([
|
|
5
|
-
"player",
|
|
6
|
-
"session",
|
|
7
|
-
"rng"
|
|
8
|
-
]);
|
|
9
|
-
/**
|
|
10
|
-
* Turns on Immer's patch recording. Idempotent, and called from the state factory rather than at
|
|
11
|
-
* module scope: a module-scope call would run on import, even when no app is created.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```ts
|
|
15
|
-
* enableDraftPatches();
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
|
-
function enableDraftPatches() {
|
|
19
|
-
enablePatches();
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Freezes a tree and every branch of it. Immer freezes what it produces; this covers the trees
|
|
23
|
-
* that never went through a draft — the initial trees and the ones handed to `restore()`.
|
|
24
|
-
*
|
|
25
|
-
* @param tree - The tree to freeze, in place.
|
|
26
|
-
* @returns The same reference, now frozen.
|
|
27
|
-
* @example
|
|
28
|
-
* ```ts
|
|
29
|
-
* state.store.session = deepFreeze(structuredClone(config.initialSession));
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
function deepFreeze(tree) {
|
|
33
|
-
if (!tree || typeof tree !== "object") return tree;
|
|
34
|
-
for (const branch of Object.values(tree)) deepFreeze(branch);
|
|
35
|
-
return Object.freeze(tree);
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Maps one Immer patch to our own structural patch, so no Immer type reaches a public type.
|
|
39
|
-
* A removal carries no value, and `exactOptionalPropertyTypes` forbids an explicit `undefined`.
|
|
40
|
-
*
|
|
41
|
-
* @param patch - The Immer patch.
|
|
42
|
-
* @param path - Path of the patch inside its own tree, without the container segment.
|
|
43
|
-
* @returns The structural patch.
|
|
44
|
-
* @example
|
|
45
|
-
* ```ts
|
|
46
|
-
* const own = toPatch({ op: "replace", path: ["doc", "player"], value: 1 }, ["player"]);
|
|
47
|
-
* ```
|
|
48
|
-
*/
|
|
49
|
-
function toPatch(patch, path) {
|
|
50
|
-
if (patch.op === "remove") return {
|
|
51
|
-
op: patch.op,
|
|
52
|
-
path
|
|
53
|
-
};
|
|
54
|
-
return {
|
|
55
|
-
op: patch.op,
|
|
56
|
-
path,
|
|
57
|
-
value: patch.value
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Derives the touched roots from the patches of one commit, in a fixed order.
|
|
62
|
-
*
|
|
63
|
-
* @param docPatches - Patches of the save document.
|
|
64
|
-
* @param sessionPatches - Patches of the session tree.
|
|
65
|
-
* @returns The roots a listener has to reconcile.
|
|
66
|
-
* @example
|
|
67
|
-
* ```ts
|
|
68
|
-
* const roots = rootsOf([{ op: "replace", path: ["player"], value: 1 }], []);
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
function rootsOf(docPatches, sessionPatches) {
|
|
72
|
-
const touched = /* @__PURE__ */ new Set();
|
|
73
|
-
for (const patch of docPatches) {
|
|
74
|
-
const [head] = patch.path;
|
|
75
|
-
if (head === "player" || head === "rng") touched.add(head);
|
|
76
|
-
}
|
|
77
|
-
if (sessionPatches.length > 0) touched.add("session");
|
|
78
|
-
return rootOrder.filter((root) => touched.has(root));
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Opens mutable drafts of the frozen save document and session tree.
|
|
82
|
-
* Both live in one draft container, so one `finishDraft` closes the transaction and the patches
|
|
83
|
-
* arrive in one list, tagged by their container key.
|
|
84
|
-
*
|
|
85
|
-
* @param doc - Frozen save document.
|
|
86
|
-
* @param session - Frozen session tree.
|
|
87
|
-
* @returns The open drafts of one transaction.
|
|
88
|
-
* @example
|
|
89
|
-
* ```ts
|
|
90
|
-
* const pair = openDrafts(state.store.doc, state.store.session);
|
|
91
|
-
* ```
|
|
92
|
-
*/
|
|
93
|
-
function openDrafts(doc, session) {
|
|
94
|
-
return createDraft({
|
|
95
|
-
doc,
|
|
96
|
-
session
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Finishes the drafts: the new frozen trees, the patches per tree and the touched roots.
|
|
101
|
-
* The drafts are revoked, so a draft that leaked into a view throws on the next touch instead of
|
|
102
|
-
* corrupting the committed tree in silence.
|
|
103
|
-
*
|
|
104
|
-
* @param pair - Open drafts of one transaction.
|
|
105
|
-
* @returns The frozen trees, the patches split by tree, and the touched roots.
|
|
106
|
-
* @example
|
|
107
|
-
* ```ts
|
|
108
|
-
* const { doc, session, patches, roots } = finishDrafts(pair);
|
|
109
|
-
* ```
|
|
110
|
-
*/
|
|
111
|
-
function finishDrafts(pair) {
|
|
112
|
-
const collected = [];
|
|
113
|
-
const finished = finishDraft(pair, (patches) => {
|
|
114
|
-
collected.push(...patches);
|
|
115
|
-
});
|
|
116
|
-
const docPatches = [];
|
|
117
|
-
const sessionPatches = [];
|
|
118
|
-
for (const patch of collected) {
|
|
119
|
-
const [container, ...path] = patch.path;
|
|
120
|
-
(container === "doc" ? docPatches : sessionPatches).push(toPatch(patch, path));
|
|
121
|
-
}
|
|
122
|
-
return {
|
|
123
|
-
doc: finished.doc,
|
|
124
|
-
session: finished.session,
|
|
125
|
-
patches: {
|
|
126
|
-
doc: docPatches,
|
|
127
|
-
session: sessionPatches
|
|
128
|
-
},
|
|
129
|
-
roots: rootsOf(docPatches, sessionPatches)
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Applies the patches of one commit to a document, the way a save seam stores what it was handed.
|
|
134
|
-
* A whole-document patch carries the path `[]` and replaces the document. The input is left as it
|
|
135
|
-
* is: the result is a new frozen document.
|
|
136
|
-
*
|
|
137
|
-
* @param document - The document the patches apply to.
|
|
138
|
-
* @param patches - The patches of one commit, in order.
|
|
139
|
-
* @returns The document after the patches.
|
|
140
|
-
* @throws {Error} When a patch names a path the document does not have.
|
|
141
|
-
* @example
|
|
142
|
-
* ```ts
|
|
143
|
-
* held.document = applyTo(held.document, [{ op: "replace", path: [], value: doc }]);
|
|
144
|
-
* ```
|
|
145
|
-
*/
|
|
146
|
-
function applyTo(document, patches) {
|
|
147
|
-
enablePatches();
|
|
148
|
-
return applyPatches(document, patches);
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Drops the drafts without a result: the base trees stay as they are and the drafts are revoked.
|
|
152
|
-
*
|
|
153
|
-
* @param pair - Open drafts of one transaction.
|
|
154
|
-
* @example
|
|
155
|
-
* ```ts
|
|
156
|
-
* dropDrafts(pair);
|
|
157
|
-
* ```
|
|
158
|
-
*/
|
|
159
|
-
function dropDrafts(pair) {
|
|
160
|
-
finishDraft(pair);
|
|
161
|
-
}
|
|
162
|
-
//#endregion
|
|
163
|
-
//#region src/plugins/model/store/providers/memory.ts
|
|
164
|
-
/** Seed of a save built by `saveOf` when the caller does not pin one. */
|
|
165
|
-
const defaultSeed = 1;
|
|
166
|
-
/** The base a patch applies to when the provider was never handed a document. */
|
|
167
|
-
const noDocument = {};
|
|
168
|
-
/**
|
|
169
|
-
* Creates an in-memory provider that keeps what it was committed and records every call.
|
|
170
|
-
* It is the default when a game configures no `playerProvider`, and the double every test uses:
|
|
171
|
-
* `provider.calls` is the whole persistence protocol of one run, in order. The document lives as
|
|
172
|
-
* long as the provider does, so a second app over the same instance reads what the first one
|
|
173
|
-
* wrote, and nothing survives the process.
|
|
174
|
-
*
|
|
175
|
-
* @param fixture - The save `load()` starts from. Omitted: a new player.
|
|
176
|
-
* @param fixture.state - The saved document.
|
|
177
|
-
* @param fixture.version - Schema version of the saved document.
|
|
178
|
-
* @returns A provider that keeps its document and records its calls.
|
|
179
|
-
* @example
|
|
180
|
-
* ```ts
|
|
181
|
-
* const provider = memory({ state: saveOf({ coins: 5 }, 42), version: 1 });
|
|
182
|
-
* ```
|
|
183
|
-
*/
|
|
184
|
-
function memory(fixture) {
|
|
185
|
-
const calls = [];
|
|
186
|
-
const held = {
|
|
187
|
-
document: fixture?.state,
|
|
188
|
-
version: fixture?.version ?? 0
|
|
189
|
-
};
|
|
190
|
-
/**
|
|
191
|
-
* Applies the patches of one commit to the held document.
|
|
192
|
-
*
|
|
193
|
-
* @param patches - Doc patches since the last commit.
|
|
194
|
-
* @param version - Schema version this build writes.
|
|
195
|
-
* @throws {Error} When a patch names a path the held document does not have.
|
|
196
|
-
* @example
|
|
197
|
-
* ```ts
|
|
198
|
-
* hold(patches, 1);
|
|
199
|
-
* ```
|
|
200
|
-
*/
|
|
201
|
-
const hold = (patches, version) => {
|
|
202
|
-
held.document = applyTo(held.document ?? noDocument, patches);
|
|
203
|
-
held.version = version;
|
|
204
|
-
};
|
|
205
|
-
return {
|
|
206
|
-
calls,
|
|
207
|
-
/**
|
|
208
|
-
* Reads the save: the fixture, or everything committed since. Without either, the player is
|
|
209
|
-
* new.
|
|
210
|
-
*
|
|
211
|
-
* @returns The held document, or `null` for a new player.
|
|
212
|
-
* @example
|
|
213
|
-
* ```ts
|
|
214
|
-
* const saved = await provider.load();
|
|
215
|
-
* ```
|
|
216
|
-
*/
|
|
217
|
-
load: async () => {
|
|
218
|
-
calls.push({ method: "load" });
|
|
219
|
-
const document = held.document;
|
|
220
|
-
if (document === void 0) return null;
|
|
221
|
-
return {
|
|
222
|
-
state: document,
|
|
223
|
-
version: held.version
|
|
224
|
-
};
|
|
225
|
-
},
|
|
226
|
-
/**
|
|
227
|
-
* Records a rest-point commit and applies it to the held document.
|
|
228
|
-
*
|
|
229
|
-
* @param patches - Doc patches since the last commit.
|
|
230
|
-
* @param version - Schema version this build writes.
|
|
231
|
-
* @example
|
|
232
|
-
* ```ts
|
|
233
|
-
* provider.commit(patches, 1);
|
|
234
|
-
* ```
|
|
235
|
-
*/
|
|
236
|
-
commit: (patches, version) => {
|
|
237
|
-
calls.push({
|
|
238
|
-
method: "commit",
|
|
239
|
-
patches,
|
|
240
|
-
version
|
|
241
|
-
});
|
|
242
|
-
hold(patches, version);
|
|
243
|
-
},
|
|
244
|
-
/**
|
|
245
|
-
* Records a durable commit and applies it. It resolves at once: there is no disk behind it.
|
|
246
|
-
*
|
|
247
|
-
* @param patches - Doc patches since the last commit.
|
|
248
|
-
* @param txId - Id of the transaction that left the barrier node.
|
|
249
|
-
* @param version - Schema version this build writes.
|
|
250
|
-
* @returns Resolves immediately.
|
|
251
|
-
* @example
|
|
252
|
-
* ```ts
|
|
253
|
-
* await provider.commitDurable(patches, "tx-1", 1);
|
|
254
|
-
* ```
|
|
255
|
-
*/
|
|
256
|
-
commitDurable: async (patches, txId, version) => {
|
|
257
|
-
calls.push({
|
|
258
|
-
method: "commitDurable",
|
|
259
|
-
patches,
|
|
260
|
-
txId,
|
|
261
|
-
version
|
|
262
|
-
});
|
|
263
|
-
hold(patches, version);
|
|
264
|
-
},
|
|
265
|
-
/**
|
|
266
|
-
* Records a flush.
|
|
267
|
-
*
|
|
268
|
-
* @returns Resolves immediately.
|
|
269
|
-
* @example
|
|
270
|
-
* ```ts
|
|
271
|
-
* await provider.flush();
|
|
272
|
-
* ```
|
|
273
|
-
*/
|
|
274
|
-
flush: async () => {
|
|
275
|
-
calls.push({ method: "flush" });
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Builds a save document from a player tree, with no rng streams drawn yet.
|
|
281
|
-
* Short fixtures in tests read as `saveOf({ coins: 5 })` instead of spelling out the rng branch.
|
|
282
|
-
*
|
|
283
|
-
* @param player - Player tree of the save.
|
|
284
|
-
* @param seed - Rng seed of the save.
|
|
285
|
-
* @returns The save document.
|
|
286
|
-
* @example
|
|
287
|
-
* ```ts
|
|
288
|
-
* const provider = memory({ state: saveOf({ coins: 5 }, 42), version: 1 });
|
|
289
|
-
* ```
|
|
290
|
-
*/
|
|
291
|
-
function saveOf(player, seed = defaultSeed) {
|
|
292
|
-
return {
|
|
293
|
-
player,
|
|
294
|
-
rng: {
|
|
295
|
-
seed,
|
|
296
|
-
streams: {}
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
//#endregion
|
|
301
|
-
//#region src/plugins/flow/runner/journal.ts
|
|
302
|
-
/** FNV-1a 32-bit offset basis. */
|
|
303
|
-
const OFFSET_BASIS = 2166136261;
|
|
304
|
-
/** FNV-1a 32-bit prime. */
|
|
305
|
-
const PRIME = 16777619;
|
|
306
|
-
/** Separates the parts of a hashed edge, so "a|b" and "a" + "b" never collide. */
|
|
307
|
-
const SEPARATOR = "\0";
|
|
308
|
-
/**
|
|
309
|
-
* Hashes text with FNV-1a, 32 bit. Pure and deterministic: the same text always gives the same
|
|
310
|
-
* eight hexadecimal characters, in every runtime.
|
|
311
|
-
*
|
|
312
|
-
* @param text - The text to hash.
|
|
313
|
-
* @returns Eight lowercase hexadecimal characters.
|
|
314
|
-
* @example
|
|
315
|
-
* ```ts
|
|
316
|
-
* const digest = hashText("board/merge");
|
|
317
|
-
* ```
|
|
318
|
-
*/
|
|
319
|
-
function hashText(text) {
|
|
320
|
-
let hash = OFFSET_BASIS;
|
|
321
|
-
for (const character of text) hash = Math.imul(hash ^ (character.codePointAt(0) ?? 0), PRIME);
|
|
322
|
-
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Hashes one edge, so a replay against edited code fails loudly in dev.
|
|
326
|
-
*
|
|
327
|
-
* @param path - Path of the node that produced the outcome.
|
|
328
|
-
* @param outcome - Outcome name.
|
|
329
|
-
* @param next - Rendered edge target.
|
|
330
|
-
* @returns The digest of the three parts together.
|
|
331
|
-
* @example
|
|
332
|
-
* ```ts
|
|
333
|
-
* const hash = entryHash("board/merge", "done", "awaitIntent");
|
|
334
|
-
* ```
|
|
335
|
-
*/
|
|
336
|
-
function entryHash(path, outcome, next) {
|
|
337
|
-
return hashText([
|
|
338
|
-
path,
|
|
339
|
-
outcome,
|
|
340
|
-
next
|
|
341
|
-
].join(SEPARATOR));
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Appends an entry with the next index and its hash, and drops the oldest entries above the limit.
|
|
345
|
-
* The index keeps counting while entries are dropped, so two entries never share a number.
|
|
346
|
-
*
|
|
347
|
-
* @param state - Runner state that holds the journal.
|
|
348
|
-
* @param entry - The taken edge without `index` and `hash`.
|
|
349
|
-
* @param limit - Journal entries kept between checkpoints.
|
|
350
|
-
* @returns The stored entry.
|
|
351
|
-
* @example
|
|
352
|
-
* ```ts
|
|
353
|
-
* const entry = pushEntry(state.runner, { path, outcome, payload, next, now }, config.journalLimit);
|
|
354
|
-
* ```
|
|
355
|
-
*/
|
|
356
|
-
function pushEntry(state, entry, limit) {
|
|
357
|
-
const stored = {
|
|
358
|
-
...entry,
|
|
359
|
-
index: state.journalIndex,
|
|
360
|
-
hash: entryHash(entry.path, entry.outcome, entry.next)
|
|
361
|
-
};
|
|
362
|
-
state.journalIndex += 1;
|
|
363
|
-
state.journal.push(stored);
|
|
364
|
-
if (state.journal.length > limit) state.journal.splice(0, state.journal.length - limit);
|
|
365
|
-
return stored;
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Clears the journal at a checkpoint. The running index is kept, and so is the array itself: a
|
|
369
|
-
* reader that holds `history()` sees the compaction.
|
|
370
|
-
*
|
|
371
|
-
* @param state - Runner state that holds the journal.
|
|
372
|
-
* @example
|
|
373
|
-
* ```ts
|
|
374
|
-
* if (node.checkpoint) compact(state.runner);
|
|
375
|
-
* ```
|
|
376
|
-
*/
|
|
377
|
-
function compact(state) {
|
|
378
|
-
state.journal.length = 0;
|
|
379
|
-
}
|
|
380
|
-
//#endregion
|
|
381
|
-
//#region src/plugins/flow/runner/registry.ts
|
|
382
|
-
/**
|
|
383
|
-
* Creates the empty flow map. It lives in its own function because the plugin's lint rule L5
|
|
384
|
-
* refuses a collection built inside an exported declaration.
|
|
385
|
-
*
|
|
386
|
-
* @returns An empty map of flow id to flow.
|
|
387
|
-
* @example
|
|
388
|
-
* ```ts
|
|
389
|
-
* const flows = emptyFlows();
|
|
390
|
-
* ```
|
|
391
|
-
*/
|
|
392
|
-
function emptyFlows() {
|
|
393
|
-
return /* @__PURE__ */ new Map();
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Adds a flow and every flow it reaches by reference to the map. The first flow under an id wins,
|
|
397
|
-
* so a collision is visible to `validateGraph` instead of silently replacing a flow.
|
|
398
|
-
*
|
|
399
|
-
* @param flow - The flow to add.
|
|
400
|
-
* @param flows - The map being filled.
|
|
401
|
-
* @example
|
|
402
|
-
* ```ts
|
|
403
|
-
* addFlow(mainFlow, flows);
|
|
404
|
-
* ```
|
|
405
|
-
*/
|
|
406
|
-
function addFlow(flow, flows) {
|
|
407
|
-
if (flows.has(flow.id)) return;
|
|
408
|
-
flows.set(flow.id, flow);
|
|
409
|
-
for (const entry of Object.values(flow.nodes)) if (entry.kind === "flow") addFlow(entry, flows);
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Collects every flow reachable from the main flow by reference, plus the registered extras.
|
|
413
|
-
* The main flow is the first entry: `describeGraph` reads it as the entry point of the graph.
|
|
414
|
-
*
|
|
415
|
-
* @param main - The top-level flow.
|
|
416
|
-
* @param extra - Flows added with `register`, not reachable by reference.
|
|
417
|
-
* @returns Flow id to flow, main first.
|
|
418
|
-
* @example
|
|
419
|
-
* ```ts
|
|
420
|
-
* const flows = collectFlows(mainFlow, [debugFlow]);
|
|
421
|
-
* ```
|
|
422
|
-
*/
|
|
423
|
-
function collectFlows(main, extra) {
|
|
424
|
-
const flows = emptyFlows();
|
|
425
|
-
addFlow(main, flows);
|
|
426
|
-
for (const flow of extra) addFlow(flow, flows);
|
|
427
|
-
return flows;
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Renders a position as a path: the node of every frame, outermost first.
|
|
431
|
-
*
|
|
432
|
-
* @param stack - One frame per nesting level.
|
|
433
|
-
* @returns The path, for example `"board/awaitIntent"`; empty without a position.
|
|
434
|
-
* @example
|
|
435
|
-
* ```ts
|
|
436
|
-
* const path = framePath(ctx.state.runner.stack);
|
|
437
|
-
* ```
|
|
438
|
-
*/
|
|
439
|
-
function framePath(stack) {
|
|
440
|
-
return stack.map((frame) => frame.node).join("/");
|
|
441
|
-
}
|
|
442
|
-
/**
|
|
443
|
-
* Finds the node, sub-flow or slot a path names, starting at the main flow. A path descends one
|
|
444
|
-
* sub-flow per segment; a segment after a plain node finds nothing.
|
|
445
|
-
*
|
|
446
|
-
* @param main - The top-level flow.
|
|
447
|
-
* @param path - A path as `framePath` renders it.
|
|
448
|
-
* @returns Where the path leads, or `undefined` when no such node exists.
|
|
449
|
-
* @example
|
|
450
|
-
* ```ts
|
|
451
|
-
* const location = findNode(mainFlow, "board/awaitIntent");
|
|
452
|
-
* ```
|
|
453
|
-
*/
|
|
454
|
-
function findNode(main, path) {
|
|
455
|
-
const names = path.split("/").filter(Boolean);
|
|
456
|
-
const trail = [];
|
|
457
|
-
let flow = main;
|
|
458
|
-
for (const [step, name] of names.entries()) {
|
|
459
|
-
const entry = flow.nodes[name];
|
|
460
|
-
if (!entry) return void 0;
|
|
461
|
-
trail.push({
|
|
462
|
-
flow: flow.id,
|
|
463
|
-
node: name
|
|
464
|
-
});
|
|
465
|
-
if (step === names.length - 1) return {
|
|
466
|
-
flow,
|
|
467
|
-
name,
|
|
468
|
-
entry,
|
|
469
|
-
trail
|
|
470
|
-
};
|
|
471
|
-
if (entry.kind !== "flow") return void 0;
|
|
472
|
-
flow = entry;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* Renders an edge target as a string: a node name, `"exit:win"` or `"map:node"`.
|
|
477
|
-
*
|
|
478
|
-
* @param target - The target as the edge table holds it.
|
|
479
|
-
* @returns The rendered target.
|
|
480
|
-
* @example
|
|
481
|
-
* ```ts
|
|
482
|
-
* const next = renderTarget(flow.edges.merge?.done);
|
|
483
|
-
* ```
|
|
484
|
-
*/
|
|
485
|
-
function renderTarget(target) {
|
|
486
|
-
if (typeof target === "string") return target;
|
|
487
|
-
return target.kind === "exit" ? `exit:${target.outcome}` : `map:${target.target}`;
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* Renders the edge table of one flow.
|
|
491
|
-
*
|
|
492
|
-
* @param flow - The flow to render.
|
|
493
|
-
* @returns Node name to outcome name to rendered target.
|
|
494
|
-
* @example
|
|
495
|
-
* ```ts
|
|
496
|
-
* const edges = renderEdges(boardFlow);
|
|
497
|
-
* ```
|
|
498
|
-
*/
|
|
499
|
-
function renderEdges(flow) {
|
|
500
|
-
const edges = {};
|
|
501
|
-
for (const [name, row] of Object.entries(flow.edges)) {
|
|
502
|
-
const rendered = {};
|
|
503
|
-
for (const [outcome, target] of Object.entries(row)) rendered[outcome] = renderTarget(target);
|
|
504
|
-
edges[name] = rendered;
|
|
505
|
-
}
|
|
506
|
-
return edges;
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Maps every node and flow a feature brought to the name of that feature.
|
|
510
|
-
*
|
|
511
|
-
* @param features - Features API.
|
|
512
|
-
* @returns Node or flow object to feature name.
|
|
513
|
-
* @example
|
|
514
|
-
* ```ts
|
|
515
|
-
* const owners = collectOwners(features);
|
|
516
|
-
* ```
|
|
517
|
-
*/
|
|
518
|
-
function collectOwners(features) {
|
|
519
|
-
const owners = /* @__PURE__ */ new Map();
|
|
520
|
-
for (const { name, description } of features.all()) {
|
|
521
|
-
for (const node of description.nodes ?? []) owners.set(node, name);
|
|
522
|
-
for (const flow of description.flows ?? []) owners.set(flow, name);
|
|
523
|
-
}
|
|
524
|
-
return owners;
|
|
525
|
-
}
|
|
526
|
-
/**
|
|
527
|
-
* Describes one entry of a flow: its flags, its outcome names, and the slot, sub-flow or owning
|
|
528
|
-
* feature when it has one.
|
|
529
|
-
*
|
|
530
|
-
* @param flow - The flow that holds the entry.
|
|
531
|
-
* @param name - Name of the entry inside that flow.
|
|
532
|
-
* @param entry - The node, sub-flow or slot.
|
|
533
|
-
* @param owner - Name of the feature that brought it, when a feature did.
|
|
534
|
-
* @returns The entry as JSON.
|
|
535
|
-
* @example
|
|
536
|
-
* ```ts
|
|
537
|
-
* const node = describeNode(boardFlow, "merge", merge, "board");
|
|
538
|
-
* ```
|
|
539
|
-
*/
|
|
540
|
-
function describeNode(flow, name, entry, owner) {
|
|
541
|
-
const node = entry.kind === "node" ? entry : void 0;
|
|
542
|
-
return {
|
|
543
|
-
path: `${flow.id}/${name}`,
|
|
544
|
-
flow: flow.id,
|
|
545
|
-
node: name,
|
|
546
|
-
rest: node?.rest ?? false,
|
|
547
|
-
over: node?.over ?? false,
|
|
548
|
-
checkpoint: node?.checkpoint ?? false,
|
|
549
|
-
barrier: node?.barrier ?? false,
|
|
550
|
-
outcomes: Object.keys(entry.outcomes),
|
|
551
|
-
...entry.kind === "slot" ? { slot: entry.name } : {},
|
|
552
|
-
...entry.kind === "flow" ? { subFlow: entry.id } : {},
|
|
553
|
-
...owner === void 0 ? {} : { owner }
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
/**
|
|
557
|
-
* Describes every entry of one flow.
|
|
558
|
-
*
|
|
559
|
-
* @param flow - The flow to describe.
|
|
560
|
-
* @param owners - Node or flow object to feature name.
|
|
561
|
-
* @returns Node name to description.
|
|
562
|
-
* @example
|
|
563
|
-
* ```ts
|
|
564
|
-
* const nodes = describeNodes(boardFlow, owners);
|
|
565
|
-
* ```
|
|
566
|
-
*/
|
|
567
|
-
function describeNodes(flow, owners) {
|
|
568
|
-
const nodes = {};
|
|
569
|
-
for (const [name, entry] of Object.entries(flow.nodes)) nodes[name] = describeNode(flow, name, entry, owners.get(entry) ?? owners.get(flow));
|
|
570
|
-
return nodes;
|
|
571
|
-
}
|
|
572
|
-
/**
|
|
573
|
-
* Collects the slot names of every flow of the graph. `describe()` and `validate` both read the
|
|
574
|
-
* contributions of exactly these slots.
|
|
575
|
-
*
|
|
576
|
-
* @param flows - Every collected flow by id.
|
|
577
|
-
* @returns The slot names, in the order the graph declares them, each once.
|
|
578
|
-
* @example
|
|
579
|
-
* ```ts
|
|
580
|
-
* const names = slotNames(flows);
|
|
581
|
-
* ```
|
|
582
|
-
*/
|
|
583
|
-
function slotNames(flows) {
|
|
584
|
-
const names = [];
|
|
585
|
-
for (const flow of flows.values()) for (const entry of Object.values(flow.nodes)) if (entry.kind === "slot" && !names.includes(entry.name)) names.push(entry.name);
|
|
586
|
-
return names;
|
|
587
|
-
}
|
|
588
|
-
/**
|
|
589
|
-
* Renders the whole graph as JSON without running the game: nodes, flags, outcomes, edges,
|
|
590
|
-
* slots and who contributed. Edge targets become `"node"`, `"exit:win"`, `"map:node"`.
|
|
591
|
-
*
|
|
592
|
-
* @param flows - Every collected flow by id, the main flow first, as `collectFlows` returns them.
|
|
593
|
-
* @param features - Features API: owners and slot contributions.
|
|
594
|
-
* @returns The graph as plain JSON.
|
|
595
|
-
* @example
|
|
596
|
-
* ```ts
|
|
597
|
-
* const graph = describeGraph(flows, features);
|
|
598
|
-
* ```
|
|
599
|
-
*/
|
|
600
|
-
function describeGraph(flows, features) {
|
|
601
|
-
const owners = collectOwners(features);
|
|
602
|
-
const described = {};
|
|
603
|
-
const slots = {};
|
|
604
|
-
for (const flow of flows.values()) described[flow.id] = {
|
|
605
|
-
nodes: describeNodes(flow, owners),
|
|
606
|
-
start: flow.start,
|
|
607
|
-
edges: renderEdges(flow)
|
|
608
|
-
};
|
|
609
|
-
for (const name of slotNames(flows)) slots[name] = features.contributions(name).map(({ feature, flow, order }) => ({
|
|
610
|
-
feature,
|
|
611
|
-
flow: flow.id,
|
|
612
|
-
order
|
|
613
|
-
}));
|
|
614
|
-
return {
|
|
615
|
-
main: [...flows.keys()][0] ?? "",
|
|
616
|
-
flows: described,
|
|
617
|
-
slots
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
/**
|
|
621
|
-
* Compares two object keys without a locale, so the rendering is the same everywhere.
|
|
622
|
-
*
|
|
623
|
-
* @param first - First key.
|
|
624
|
-
* @param second - Second key.
|
|
625
|
-
* @returns A negative number, zero or a positive number.
|
|
626
|
-
* @example
|
|
627
|
-
* ```ts
|
|
628
|
-
* entries.sort(([first], [second]) => compareKeys(first, second));
|
|
629
|
-
* ```
|
|
630
|
-
*/
|
|
631
|
-
function compareKeys(first, second) {
|
|
632
|
-
if (first === second) return 0;
|
|
633
|
-
return first < second ? -1 : 1;
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Renders a JSON value with its object keys sorted, so two equal graphs written in a different
|
|
637
|
-
* order hash the same.
|
|
638
|
-
*
|
|
639
|
-
* @param value - Any JSON value.
|
|
640
|
-
* @returns The value as text.
|
|
641
|
-
* @example
|
|
642
|
-
* ```ts
|
|
643
|
-
* const text = stableJson({ b: 1, a: 2 });
|
|
644
|
-
* ```
|
|
645
|
-
*/
|
|
646
|
-
function stableJson(value) {
|
|
647
|
-
if (Array.isArray(value)) return `[${value.map((item) => stableJson(item)).join(",")}]`;
|
|
648
|
-
if (typeof value !== "object" || !value) return JSON.stringify(value) ?? "";
|
|
649
|
-
return `{${Object.entries(value).filter(([, item]) => item !== void 0).toSorted(([first], [second]) => compareKeys(first, second)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
|
|
650
|
-
}
|
|
651
|
-
/**
|
|
652
|
-
* Hashes a graph description. A bookmark of a plain rest node is accepted only while this hash
|
|
653
|
-
* is unchanged.
|
|
654
|
-
*
|
|
655
|
-
* @param graph - Result of `describeGraph`.
|
|
656
|
-
* @returns Eight lowercase hexadecimal characters.
|
|
657
|
-
* @example
|
|
658
|
-
* ```ts
|
|
659
|
-
* const hash = graphHash(describeGraph(flows, features));
|
|
660
|
-
* ```
|
|
661
|
-
*/
|
|
662
|
-
function graphHash(graph) {
|
|
663
|
-
return hashText(stableJson(graph));
|
|
664
|
-
}
|
|
665
|
-
//#endregion
|
|
666
|
-
export { graphHash as a, pushEntry as c, deepFreeze as d, dropDrafts as f, openDrafts as h, framePath as i, memory as l, finishDrafts as m, describeGraph as n, slotNames as o, enableDraftPatches as p, findNode as r, compact as s, collectFlows as t, saveOf as u };
|