@storylet-studio/with-patter 0.1.0 → 0.2.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/README.md CHANGED
@@ -25,3 +25,19 @@ flow.play(card.gameId, p.outcome, "the-inn");
25
25
  One Patter flow per performed box, named after the box, entered with `goto` for each card, so
26
26
  Patter's memory (visits, shuffles) carries across the box's cards. An option is marked not
27
27
  `enabled` when Patter's condition on it fails or the outcome it leads to is gated shut.
28
+
29
+ After a load, `performer.resume(savedPerformance, outcomes)` carries on: Patter restores the flow
30
+ itself, and the Performance (plain JSON) is yours to save beside the two engines.
31
+
32
+ ## At build time
33
+
34
+ `checkPairing(storyletBundle, patterBundle, ["village"])` compares the two published bundles and
35
+ returns one readable line per problem: a card in a performed box with no scene, a scene no card
36
+ plays, an outcome a scene names that its card doesn't declare, a branch that can't say which of
37
+ several outcomes it reached. Run it in your build.
38
+
39
+ ## Without a bundler
40
+
41
+ `@storylet-studio/with-patter/with-patter.min.js` is a browser drop-in: everything under a
42
+ `StoryletsWithPatter` global, beside `patterplay.min.js` and `storyletengine.min.js`.
43
+
package/dist/index.cjs CHANGED
@@ -21,6 +21,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  Performer: () => Performer,
24
+ checkPairing: () => checkPairing,
25
+ optionsOf: () => optionsOf,
26
+ outcomesReported: () => outcomesReported,
24
27
  sceneIdFor: () => sceneIdFor
25
28
  });
26
29
  module.exports = __toCommonJS(index_exports);
@@ -70,6 +73,19 @@ var Performer = class {
70
73
  }
71
74
  return this.run(p, flow, outcomes);
72
75
  }
76
+ /**
77
+ * Pick a performance back up after a load. Patter restores the flow's position itself, and a
78
+ * flow paused at a choice comes back still paused, so the options are read off it again. What
79
+ * the host supplies is `saved`, the Performance it stored: the transcript and the outcome settled
80
+ * so far are presentation and bookkeeping, which Patter hands over once and does not keep.
81
+ */
82
+ resume(saved, outcomes) {
83
+ if (saved.ended) return saved.outcome !== void 0 || saved.problem !== void 0 ? saved : this.finish(saved, outcomes);
84
+ const flow = this.patter.getFlow(saved.box);
85
+ if (!flow) return { ...saved, ended: true, problem: `The save has no Patter flow "${saved.box}".` };
86
+ const options = flow.getChoices();
87
+ return { ...saved, options: optionsFor(options ?? [], outcomes) };
88
+ }
73
89
  /** The player picks an option; the scene runs on to its next choice or its end. */
74
90
  choose(p, optionId, outcomes) {
75
91
  const flow = this.patter.getFlow(p.box);
@@ -105,12 +121,7 @@ var Performer = class {
105
121
  const named = step.gameData?.["outcome"];
106
122
  if (typeof named === "string") p.lastEvent = named;
107
123
  } else if (step.type === "choice") {
108
- p.options = step.options.map((o) => {
109
- const named = o.gameData?.["outcome"];
110
- const outcome = typeof named === "string" ? named : void 0;
111
- const shut = outcome !== void 0 && outcomes.find((x) => x.gameId === outcome)?.available === false;
112
- return { id: o.id, text: o.prompt?.text || o.id, enabled: o.eligible && !shut, ...outcome !== void 0 ? { outcome } : {} };
113
- });
124
+ p.options = optionsFor(step.options, outcomes);
114
125
  return p;
115
126
  } else {
116
127
  return this.finish(p, outcomes);
@@ -118,8 +129,10 @@ var Performer = class {
118
129
  }
119
130
  return { ...p, ended: true, problem: `The scene ran ${MAX_STEPS} steps without a choice or an end.` };
120
131
  }
121
- /** Last word wins: an event, else the option's label, else the card's only outcome. */
132
+ /** Last word wins: an event, else the option's label, else the card's only outcome, and a card
133
+ * with no outcomes at all is played with none (""). */
122
134
  finish(p, outcomes) {
135
+ if (p.lastEvent === void 0 && p.lastLabel === void 0 && outcomes.length === 0) return { ...p, ended: true, outcome: "" };
123
136
  const reached = p.lastEvent ?? p.lastLabel ?? (outcomes.length === 1 ? outcomes[0].gameId : void 0);
124
137
  if (reached === void 0) {
125
138
  return { ...p, ended: true, problem: "The scene ended without saying which outcome it reached." };
@@ -130,9 +143,117 @@ var Performer = class {
130
143
  return { ...p, ended: true, outcome: reached };
131
144
  }
132
145
  };
146
+ function optionsFor(options, outcomes) {
147
+ return options.map((o) => {
148
+ const named = o.gameData?.["outcome"];
149
+ const outcome = typeof named === "string" ? named : void 0;
150
+ const shut = outcome !== void 0 && outcomes.find((x) => x.gameId === outcome)?.available === false;
151
+ const why = !o.eligible ? "not available here" : shut ? "requirements not met" : void 0;
152
+ return {
153
+ id: o.id,
154
+ text: o.prompt?.text || o.id,
155
+ enabled: o.eligible && !shut,
156
+ ...outcome !== void 0 ? { outcome } : {},
157
+ ...why !== void 0 ? { why } : {}
158
+ };
159
+ });
160
+ }
161
+
162
+ // src/pairing.ts
163
+ var import_model = require("@storylet-studio/model");
164
+ function outcomesReported(node) {
165
+ const found = [];
166
+ (function walk(n) {
167
+ if (Array.isArray(n)) {
168
+ n.forEach(walk);
169
+ return;
170
+ }
171
+ if (!n || typeof n !== "object") return;
172
+ const o = n;
173
+ if (o.kind === "gameEvent" && typeof o.gameData?.outcome === "string") found.push(o.gameData.outcome);
174
+ for (const v of Object.values(n)) walk(v);
175
+ })(node);
176
+ return found;
177
+ }
178
+ function optionsOf(scene) {
179
+ const found = [];
180
+ (function walk(n) {
181
+ if (Array.isArray(n)) {
182
+ n.forEach(walk);
183
+ return;
184
+ }
185
+ if (!n || typeof n !== "object") return;
186
+ const o = n;
187
+ if (o.type === "group" && o.prompt !== void 0) {
188
+ found.push({
189
+ id: String(o.id),
190
+ ...typeof o.prompt?.id === "string" ? { promptId: o.prompt.id } : {},
191
+ outcome: typeof o.gameData?.outcome === "string" ? o.gameData.outcome : null,
192
+ overrides: outcomesReported(o.children ?? [])
193
+ });
194
+ }
195
+ for (const [k, v] of Object.entries(n)) if (k !== "prompt") walk(v);
196
+ })(scene);
197
+ return found;
198
+ }
199
+ function checkPairing(storyletBundle, patterBundle, boxes) {
200
+ const problems = [];
201
+ const scenes = patterBundle.scenes ?? {};
202
+ const address = (s) => s.gameId?.trim() || (0, import_model.gameIdify)(s.name ?? "");
203
+ const byAddress = new Map(Object.entries(scenes).map(([id, s]) => [address(s), id]));
204
+ const sceneFor = (ref) => scenes[ref] ? ref : byAddress.get(ref);
205
+ const played = /* @__PURE__ */ new Set();
206
+ const allBoxes = storyletBundle.boxes ?? [];
207
+ for (const box of allBoxes) {
208
+ if (boxes && !boxes.includes(box.gameId ?? box.id)) continue;
209
+ for (const deck of box.decks ?? []) {
210
+ for (const card of deck.cards ?? []) {
211
+ const name = card.gameId ?? card.id;
212
+ const id = sceneFor(name);
213
+ if (id === void 0) {
214
+ if (boxes) problems.push(`card "${name}" has no scene of that name`);
215
+ continue;
216
+ }
217
+ played.add(id);
218
+ const scene = scenes[id];
219
+ const declared = (card.outcomes ?? []).map((o) => o.gameId ?? o.id);
220
+ const options = optionsOf(scene);
221
+ const events = outcomesReported(scene);
222
+ const named = [.../* @__PURE__ */ new Set([...events, ...options.flatMap((o) => o.outcome ? [o.outcome] : [])])];
223
+ for (const n of named) {
224
+ if (!declared.includes(n)) {
225
+ problems.push(`scene "${name}" names outcome "${n}", which that card does not declare (it declares: ${declared.join(", ") || "none"})`);
226
+ }
227
+ }
228
+ if (declared.length > 1) {
229
+ if (options.length === 0 && events.length === 0) {
230
+ problems.push(`scene "${name}" says nothing about its outcome, and its card declares ${declared.length} (${declared.join(", ")}): label its options, or fire a gameEvent`);
231
+ }
232
+ for (const o of options) {
233
+ if (!o.outcome && o.overrides.length === 0) {
234
+ problems.push(`option "${o.id}" in scene "${name}" names no outcome and fires no gameEvent, so taking it leaves the host guessing between ${declared.join(", ")}`);
235
+ }
236
+ }
237
+ for (const d of declared) {
238
+ if (!named.includes(d)) problems.push(`outcome "${d}" of card "${name}" is named by no option and no gameEvent`);
239
+ }
240
+ }
241
+ }
242
+ }
243
+ }
244
+ if (boxes) {
245
+ for (const [id, scene] of Object.entries(scenes)) {
246
+ if (!played.has(id)) problems.push(`scene "${scene.gameId?.trim() || id}" belongs to no card, so nothing can ever play it`);
247
+ }
248
+ }
249
+ return problems;
250
+ }
133
251
  // Annotate the CommonJS export names for ESM import in node:
134
252
  0 && (module.exports = {
135
253
  Performer,
254
+ checkPairing,
255
+ optionsOf,
256
+ outcomesReported,
136
257
  sceneIdFor
137
258
  });
138
259
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/performer.ts"],"sourcesContent":["// @storylet-studio/with-patter: perform a dealt storylet card as the Patter scene named after it.\nexport { Performer, sceneIdFor } from \"./performer.js\";\nexport type { Beat, CardOutcome, Performance, PerformerLink, SceneOption } from \"./performer.js\";\n","// ---------------------------------------------------------------------------\n// Performing a dealt card through Patter: the card's gameId names a Patter\n// scene, the scene runs, and the scene decides which of the card's outcomes\n// was reached (the Storylets-with-Patter contract; the Hamlet's\n// `performance.js`, lifted so a game, Storyletter's Board and the playable\n// page all run the same code).\n//\n// The Hamlet's rules, kept exactly because they are the host's contract:\n// - ONE Patter flow per performed box, named after the box, entered with\n// `goto` for each card: a flow is Patter's memory, and a fresh one per card\n// would forget its visits and restart its seeded shuffles (joint demo\n// finding 14).\n// - The outcome is the LAST word: a gameEvent carrying one, else the label on\n// the option the player took, else the card's only outcome (finding 15).\n// - An option is greyed when either engine says no: Patter's `eligible`, or\n// the Storylet Engine's gate on the outcome the option names.\n//\n// This module knows nothing of the DOM, or of the Storylet Engine: the host\n// deals, draws what `start` and `choose` return, and plays the outcome.\n// ---------------------------------------------------------------------------\n\nimport type { Bundle as PatterBundle, Engine as PatterEngine, Flow as PatterFlow, StepResult } from \"@patterkit/runtime\";\n\n/**\n * A card's scene reference (its gameId) to the scene's internal id, as Patter's runtime resolves a\n * reference: an internal id first, else a scene's address by Patter's own rules. Undefined when no\n * scene matches.\n */\nexport function sceneIdFor(engine: PatterEngine, bundle: PatterBundle, ref: string): string | undefined {\n if (bundle.scenes[ref]) return ref;\n return Object.keys(bundle.scenes).find((id) => engine.sceneAddress(id) === ref);\n}\n\n/** One thing the scene has said, for the transcript. */\nexport type Beat =\n | { kind: \"line\"; who?: string; text: string }\n | { kind: \"text\"; text: string }\n /** The player's pick, shown back in the transcript. */\n | { kind: \"chose\"; text: string };\n\nexport interface SceneOption {\n id: string;\n text: string;\n /** False when Patter's condition fails, or the outcome it names is gated shut. */\n enabled: boolean;\n /** The outcome the option names, when it names one. */\n outcome?: string;\n}\n\n/** A card being performed: what has been said, and what the player can do now. */\nexport interface Performance {\n card: string;\n box: string;\n /** The scene's internal id, for reporting the position to Patterpad. */\n sceneId?: string;\n transcript: Beat[];\n /** The choice on screen, when the scene is waiting on one. */\n options?: SceneOption[];\n /** True once the scene has run to its end. */\n ended: boolean;\n /** The outcome the scene reached (last word wins); undefined until it ends, and after it\n * ends when nothing named one and the card has several. */\n outcome?: string;\n /** Why the scene cannot be performed, or why it ended without an outcome. */\n problem?: string;\n /** Bookkeeping for the resolution. */\n lastEvent?: string;\n lastLabel?: string;\n}\n\n/** An outcome of the card, as the Storylet Engine reports it (`Table.outcomes`). */\nexport interface CardOutcome { gameId: string; available: boolean }\n\n/** Where the Board reports its position, so Patterpad's playhead follows the line being played:\n * the subset of play-helpers' `DebugLink` this needs. */\nexport interface PerformerLink {\n flowOpened(flowId: string): void;\n observe(flowId: string, sceneId: string | null, beatId: string | null, type: string, choiceId?: string): void;\n}\n\n/** A guard against a scene that loops without asking anything. */\nconst MAX_STEPS = 500;\n\nexport class Performer {\n /** Where to report the position; set once Patterpad's Live Link is up. */\n link: PerformerLink | undefined;\n\n constructor(\n private patter: PatterEngine,\n private readonly boxes: ReadonlySet<string>,\n /** A card's scene reference (its gameId) to the scene's internal id, as the runtime resolves it. */\n private readonly sceneIdOf: (ref: string) => string | undefined = () => undefined,\n ) {}\n\n /** A live refresh replaced the engine (a structural hot swap): carry on with the new one. */\n setEngine(patter: PatterEngine): void {\n this.patter = patter;\n }\n\n /** Does the project say Patter performs this box? */\n performs(boxGameId: string): boolean {\n return this.boxes.has(boxGameId);\n }\n\n /** Start a card's scene: its flow entered at the scene named after the card, run to the first\n * choice or to its end. */\n start(card: { id: string; gameId: string }, boxGameId: string, outcomes: readonly CardOutcome[]): Performance {\n const sceneId = this.sceneIdOf(card.gameId);\n const p: Performance = { card: card.id, box: boxGameId, transcript: [], ended: false, ...(sceneId !== undefined ? { sceneId } : {}) };\n let flow: PatterFlow;\n try {\n // The engine's own answer, not a list kept here: a loaded save brings its flows back.\n const existing = this.patter.getFlow(boxGameId);\n if (existing) {\n if (!existing.goto(card.gameId)) return { ...p, ended: true, problem: `The Patter project has no scene named \"${card.gameId}\".` };\n flow = existing;\n } else {\n flow = this.patter.openFlow(boxGameId, { scene: card.gameId });\n this.link?.flowOpened(boxGameId);\n }\n } catch (e) {\n return { ...p, ended: true, problem: `The Patter project has no scene named \"${card.gameId}\" (${e instanceof Error ? e.message : String(e)}).` };\n }\n return this.run(p, flow, outcomes);\n }\n\n /** The player picks an option; the scene runs on to its next choice or its end. */\n choose(p: Performance, optionId: string, outcomes: readonly CardOutcome[]): Performance {\n const flow = this.patter.getFlow(p.box);\n const option = p.options?.find((o) => o.id === optionId);\n if (!flow || !option || !option.enabled) return p;\n flow.choose(optionId);\n this.link?.observe(p.box, p.sceneId ?? null, null, \"choose\", optionId);\n const next: Performance = {\n ...p, options: undefined, transcript: [...p.transcript, { kind: \"chose\", text: option.text }],\n ...(option.outcome !== undefined ? { lastLabel: option.outcome } : {}),\n };\n return this.run(next, flow, outcomes);\n }\n\n /** A new run: every box's flow closes, so the next card opens a fresh one. Patter's own\n * properties are the registry's, and the Board resets those with the rest. */\n reset(): void {\n for (const box of this.boxes) {\n if (this.patter.getFlow(box)) this.patter.closeFlow(box);\n }\n }\n\n private run(start: Performance, flow: PatterFlow, outcomes: readonly CardOutcome[]): Performance {\n const p: Performance = { ...start, transcript: [...start.transcript] };\n for (let i = 0; i < MAX_STEPS; i++) {\n const step: StepResult = flow.advance();\n this.link?.observe(p.box, p.sceneId ?? null, \"id\" in step ? step.id : null, step.type);\n if (step.type === \"line\") {\n p.transcript.push({ kind: \"line\", ...(step.characterName ?? step.character ? { who: step.characterName ?? step.character } : {}), text: step.text });\n } else if (step.type === \"text\") {\n p.transcript.push({ kind: \"text\", text: step.text });\n } else if (step.type === \"gameEvent\") {\n const named = step.gameData?.[\"outcome\"];\n if (typeof named === \"string\") p.lastEvent = named;\n } else if (step.type === \"choice\") {\n p.options = step.options.map((o) => {\n const named = o.gameData?.[\"outcome\"];\n const outcome = typeof named === \"string\" ? named : undefined;\n // Two gates, each engine's own: Patter's condition, and ours on the outcome it names.\n const shut = outcome !== undefined && outcomes.find((x) => x.gameId === outcome)?.available === false;\n return { id: o.id, text: o.prompt?.text || o.id, enabled: o.eligible && !shut, ...(outcome !== undefined ? { outcome } : {}) };\n });\n return p;\n } else {\n return this.finish(p, outcomes);\n }\n }\n return { ...p, ended: true, problem: `The scene ran ${MAX_STEPS} steps without a choice or an end.` };\n }\n\n /** Last word wins: an event, else the option's label, else the card's only outcome. */\n private finish(p: Performance, outcomes: readonly CardOutcome[]): Performance {\n const reached = p.lastEvent ?? p.lastLabel ?? (outcomes.length === 1 ? outcomes[0]!.gameId : undefined);\n if (reached === undefined) {\n return { ...p, ended: true, problem: \"The scene ended without saying which outcome it reached.\" };\n }\n if (!outcomes.some((o) => o.gameId === reached)) {\n return { ...p, ended: true, problem: `The scene reached \"${reached}\", which this card doesn't have.` };\n }\n return { ...p, ended: true, outcome: reached };\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/performer.ts","../src/pairing.ts"],"sourcesContent":["// @storylet-studio/with-patter: perform a dealt storylet card as the Patter scene named after it,\n// and check, at build time, that the cards and the scenes line up.\nexport { Performer, sceneIdFor } from \"./performer.js\";\nexport type { Beat, CardOutcome, Performance, PerformerLink, SceneOption } from \"./performer.js\";\nexport { checkPairing, optionsOf, outcomesReported } from \"./pairing.js\";\nexport type { BundleOption } from \"./pairing.js\";\n","// ---------------------------------------------------------------------------\n// Performing a dealt card through Patter: the card's gameId names a Patter\n// scene, the scene runs, and the scene decides which of the card's outcomes\n// was reached (the Storylets-with-Patter contract; the Hamlet's\n// `performance.js`, lifted so a game, Storyletter's Board and the playable\n// page all run the same code).\n//\n// The Hamlet's rules, kept exactly because they are the host's contract:\n// - ONE Patter flow per performed box, named after the box, entered with\n// `goto` for each card: a flow is Patter's memory, and a fresh one per card\n// would forget its visits and restart its seeded shuffles (joint demo\n// finding 14).\n// - The outcome is the LAST word: a gameEvent carrying one, else the label on\n// the option the player took, else the card's only outcome (finding 15).\n// - An option is greyed when either engine says no: Patter's `eligible`, or\n// the Storylet Engine's gate on the outcome the option names.\n//\n// This module knows nothing of the DOM, or of the Storylet Engine: the host\n// deals, draws what `start` and `choose` return, and plays the outcome.\n// ---------------------------------------------------------------------------\n\nimport type { Bundle as PatterBundle, ChoiceOption, Engine as PatterEngine, Flow as PatterFlow, StepResult } from \"@patterkit/runtime\";\n\n/**\n * A card's scene reference (its gameId) to the scene's internal id, as Patter's runtime resolves a\n * reference: an internal id first, else a scene's address by Patter's own rules. Undefined when no\n * scene matches.\n */\nexport function sceneIdFor(engine: PatterEngine, bundle: PatterBundle, ref: string): string | undefined {\n if (bundle.scenes[ref]) return ref;\n return Object.keys(bundle.scenes).find((id) => engine.sceneAddress(id) === ref);\n}\n\n/** One thing the scene has said, for the transcript. */\nexport type Beat =\n | { kind: \"line\"; who?: string; text: string }\n | { kind: \"text\"; text: string }\n /** The player's pick, shown back in the transcript. */\n | { kind: \"chose\"; text: string };\n\nexport interface SceneOption {\n id: string;\n text: string;\n /** False when Patter's condition fails, or the outcome it names is gated shut. */\n enabled: boolean;\n /** Why it isn't enabled, in a few plain words (\"not available here\" when Patter's condition\n * fails, \"requirements not met\" when the outcome is shut); absent when it is. A game will want\n * its own voice here; these say which engine said no. */\n why?: string;\n /** The outcome the option names, when it names one. */\n outcome?: string;\n}\n\n/** A card being performed: what has been said, and what the player can do now. */\nexport interface Performance {\n card: string;\n box: string;\n /** The scene's internal id, for reporting the position to Patterpad. */\n sceneId?: string;\n transcript: Beat[];\n /** The choice on screen, when the scene is waiting on one. */\n options?: SceneOption[];\n /** True once the scene has run to its end. */\n ended: boolean;\n /** The outcome the scene reached (last word wins): \"\" for a card with no outcomes at all, which\n * is played with none. Undefined until it ends, and after it ends when nothing named one and\n * the card has several. */\n outcome?: string;\n /** Why the scene cannot be performed, or why it ended without an outcome. */\n problem?: string;\n /** Bookkeeping for the resolution. */\n lastEvent?: string;\n lastLabel?: string;\n}\n\n/** An outcome of the card, as the Storylet Engine reports it (`Table.outcomes`). */\nexport interface CardOutcome { gameId: string; available: boolean }\n\n/** Where the Board reports its position, so Patterpad's playhead follows the line being played:\n * the subset of play-helpers' `DebugLink` this needs. */\nexport interface PerformerLink {\n flowOpened(flowId: string): void;\n observe(flowId: string, sceneId: string | null, beatId: string | null, type: string, choiceId?: string): void;\n}\n\n/** A guard against a scene that loops without asking anything. */\nconst MAX_STEPS = 500;\n\nexport class Performer {\n /** Where to report the position; set once Patterpad's Live Link is up. */\n link: PerformerLink | undefined;\n\n constructor(\n private patter: PatterEngine,\n private readonly boxes: ReadonlySet<string>,\n /** A card's scene reference (its gameId) to the scene's internal id, as the runtime resolves it. */\n private readonly sceneIdOf: (ref: string) => string | undefined = () => undefined,\n ) {}\n\n /** A live refresh replaced the engine (a structural hot swap): carry on with the new one. */\n setEngine(patter: PatterEngine): void {\n this.patter = patter;\n }\n\n /** Does the project say Patter performs this box? */\n performs(boxGameId: string): boolean {\n return this.boxes.has(boxGameId);\n }\n\n /** Start a card's scene: its flow entered at the scene named after the card, run to the first\n * choice or to its end. */\n start(card: { id: string; gameId: string }, boxGameId: string, outcomes: readonly CardOutcome[]): Performance {\n const sceneId = this.sceneIdOf(card.gameId);\n const p: Performance = { card: card.id, box: boxGameId, transcript: [], ended: false, ...(sceneId !== undefined ? { sceneId } : {}) };\n let flow: PatterFlow;\n try {\n // The engine's own answer, not a list kept here: a loaded save brings its flows back.\n const existing = this.patter.getFlow(boxGameId);\n if (existing) {\n if (!existing.goto(card.gameId)) return { ...p, ended: true, problem: `The Patter project has no scene named \"${card.gameId}\".` };\n flow = existing;\n } else {\n flow = this.patter.openFlow(boxGameId, { scene: card.gameId });\n this.link?.flowOpened(boxGameId);\n }\n } catch (e) {\n return { ...p, ended: true, problem: `The Patter project has no scene named \"${card.gameId}\" (${e instanceof Error ? e.message : String(e)}).` };\n }\n return this.run(p, flow, outcomes);\n }\n\n /**\n * Pick a performance back up after a load. Patter restores the flow's position itself, and a\n * flow paused at a choice comes back still paused, so the options are read off it again. What\n * the host supplies is `saved`, the Performance it stored: the transcript and the outcome settled\n * so far are presentation and bookkeeping, which Patter hands over once and does not keep.\n */\n resume(saved: Performance, outcomes: readonly CardOutcome[]): Performance {\n // Ended and not yet continued: nothing to ask Patter, but a save that kept only the bookkeeping\n // (another host's, say) gets its outcome worked out again, by the same rule.\n if (saved.ended) return saved.outcome !== undefined || saved.problem !== undefined ? saved : this.finish(saved, outcomes);\n const flow = this.patter.getFlow(saved.box);\n if (!flow) return { ...saved, ended: true, problem: `The save has no Patter flow \"${saved.box}\".` };\n const options = flow.getChoices();\n return { ...saved, options: optionsFor(options ?? [], outcomes) };\n }\n\n /** The player picks an option; the scene runs on to its next choice or its end. */\n choose(p: Performance, optionId: string, outcomes: readonly CardOutcome[]): Performance {\n const flow = this.patter.getFlow(p.box);\n const option = p.options?.find((o) => o.id === optionId);\n if (!flow || !option || !option.enabled) return p;\n flow.choose(optionId);\n this.link?.observe(p.box, p.sceneId ?? null, null, \"choose\", optionId);\n const next: Performance = {\n ...p, options: undefined, transcript: [...p.transcript, { kind: \"chose\", text: option.text }],\n ...(option.outcome !== undefined ? { lastLabel: option.outcome } : {}),\n };\n return this.run(next, flow, outcomes);\n }\n\n /** A new run: every box's flow closes, so the next card opens a fresh one. Patter's own\n * properties are the registry's, and the Board resets those with the rest. */\n reset(): void {\n for (const box of this.boxes) {\n if (this.patter.getFlow(box)) this.patter.closeFlow(box);\n }\n }\n\n private run(start: Performance, flow: PatterFlow, outcomes: readonly CardOutcome[]): Performance {\n const p: Performance = { ...start, transcript: [...start.transcript] };\n for (let i = 0; i < MAX_STEPS; i++) {\n const step: StepResult = flow.advance();\n this.link?.observe(p.box, p.sceneId ?? null, \"id\" in step ? step.id : null, step.type);\n if (step.type === \"line\") {\n p.transcript.push({ kind: \"line\", ...(step.characterName ?? step.character ? { who: step.characterName ?? step.character } : {}), text: step.text });\n } else if (step.type === \"text\") {\n p.transcript.push({ kind: \"text\", text: step.text });\n } else if (step.type === \"gameEvent\") {\n const named = step.gameData?.[\"outcome\"];\n if (typeof named === \"string\") p.lastEvent = named;\n } else if (step.type === \"choice\") {\n p.options = optionsFor(step.options, outcomes);\n return p;\n } else {\n return this.finish(p, outcomes);\n }\n }\n return { ...p, ended: true, problem: `The scene ran ${MAX_STEPS} steps without a choice or an end.` };\n }\n\n /** Last word wins: an event, else the option's label, else the card's only outcome, and a card\n * with no outcomes at all is played with none (\"\"). */\n private finish(p: Performance, outcomes: readonly CardOutcome[]): Performance {\n if (p.lastEvent === undefined && p.lastLabel === undefined && outcomes.length === 0) return { ...p, ended: true, outcome: \"\" };\n const reached = p.lastEvent ?? p.lastLabel ?? (outcomes.length === 1 ? outcomes[0]!.gameId : undefined);\n if (reached === undefined) {\n return { ...p, ended: true, problem: \"The scene ended without saying which outcome it reached.\" };\n }\n if (!outcomes.some((o) => o.gameId === reached)) {\n return { ...p, ended: true, problem: `The scene reached \"${reached}\", which this card doesn't have.` };\n }\n return { ...p, ended: true, outcome: reached };\n }\n}\n\n/** A choice's options with both engines' gates applied: Patter's own condition (`eligible`), and\n * the Storylet Engine's on the outcome each option names. */\nfunction optionsFor(options: readonly ChoiceOption[], outcomes: readonly CardOutcome[]): SceneOption[] {\n return options.map((o) => {\n const named = o.gameData?.[\"outcome\"];\n const outcome = typeof named === \"string\" ? named : undefined;\n const shut = outcome !== undefined && outcomes.find((x) => x.gameId === outcome)?.available === false;\n const why = !o.eligible ? \"not available here\" : shut ? \"requirements not met\" : undefined;\n return {\n id: o.id, text: o.prompt?.text || o.id, enabled: o.eligible && !shut,\n ...(outcome !== undefined ? { outcome } : {}), ...(why !== undefined ? { why } : {}),\n };\n });\n}\n","// ---------------------------------------------------------------------------\n// Cards against their scenes, on the two PUBLISHED bundles: the build-time\n// check a game runs so the naming convention is safe (lifted from the Hamlet's\n// scripts/pairing.mjs, whose messages it keeps). Nothing declares the links, so\n// nothing else validates them: the failure it catches is a card that performs\n// no dialogue, or a branch that ends without saying what happened, and both\n// look exactly like content somebody meant to write.\n//\n// A card finds its scene as Patter's runtime resolves a reference: an internal\n// id first, else an address (a pinned gameId, else the name's slug). Storyletter\n// runs the same analysis on the project as you edit (ops `patter-link.ts`, which\n// takes the two walkers below from here); this is the game's copy, on what ships.\n// ---------------------------------------------------------------------------\n\nimport { gameIdify } from \"@storylet-studio/model\";\n\n/** Every gameEvent outcome id anywhere under a compiled node, in document order. */\nexport function outcomesReported(node: unknown): string[] {\n const found: string[] = [];\n (function walk(n: unknown): void {\n if (Array.isArray(n)) { n.forEach(walk); return; }\n if (!n || typeof n !== \"object\") return;\n const o = n as { kind?: unknown; gameData?: { outcome?: unknown } };\n if (o.kind === \"gameEvent\" && typeof o.gameData?.outcome === \"string\") found.push(o.gameData.outcome);\n for (const v of Object.values(n)) walk(v);\n })(node);\n return found;\n}\n\n/** One choice option in a compiled scene, as the pairing reads it. */\nexport interface BundleOption {\n id: string;\n /** The string id of the words the player picks, when the option has any. */\n promptId?: string;\n /** The outcome the option labels itself with, if any. */\n outcome: string | null;\n /** The gameEvent outcomes its branch fires, which win over the label. */\n overrides: string[];\n}\n\n/** Every choice option in a compiled scene (a group carrying a prompt). */\nexport function optionsOf(scene: unknown): BundleOption[] {\n const found: BundleOption[] = [];\n (function walk(n: unknown): void {\n if (Array.isArray(n)) { n.forEach(walk); return; }\n if (!n || typeof n !== \"object\") return;\n const o = n as { type?: unknown; prompt?: { id?: unknown }; id?: unknown; gameData?: { outcome?: unknown }; children?: unknown };\n if (o.type === \"group\" && o.prompt !== undefined) {\n found.push({\n id: String(o.id),\n ...(typeof o.prompt?.id === \"string\" ? { promptId: o.prompt.id } : {}),\n outcome: typeof o.gameData?.outcome === \"string\" ? o.gameData.outcome : null,\n overrides: outcomesReported(o.children ?? []),\n });\n }\n // The prompt is text, never structure: walking it could mistake a nested group for an option.\n for (const [k, v] of Object.entries(n)) if (k !== \"prompt\") walk(v);\n })(scene);\n return found;\n}\n\ninterface BundleCard { id: string; gameId?: string; outcomes?: { gameId?: string; id: string }[] }\ninterface BundleScene { name?: string; gameId?: string; [key: string]: unknown }\n\n/**\n * Compare a compiled storylet bundle with a compiled Patter bundle. Returns the problems, one\n * readable line each; empty means they line up.\n *\n * `boxes` (box gameIds) limits the check to the boxes the game performs through Patter: in those,\n * a card with no scene is a problem, and so is a scene no card plays. Without it, every card that\n * has a scene is checked and none is required to have one.\n *\n * The rule enforced is the one the Performer plays by: a gameEvent wins, else the label on the\n * option taken, else the card's only outcome. So a scene whose card has one outcome need say\n * nothing, and one whose card has several must leave no path that says nothing.\n */\nexport function checkPairing(storyletBundle: unknown, patterBundle: unknown, boxes?: readonly string[]): string[] {\n const problems: string[] = [];\n const scenes = ((patterBundle as { scenes?: Record<string, BundleScene> }).scenes) ?? {};\n const address = (s: BundleScene): string => s.gameId?.trim() || gameIdify(s.name ?? \"\");\n const byAddress = new Map(Object.entries(scenes).map(([id, s]) => [address(s), id]));\n const sceneFor = (ref: string): string | undefined => (scenes[ref] ? ref : byAddress.get(ref));\n const played = new Set<string>();\n\n const allBoxes = ((storyletBundle as { boxes?: { gameId?: string; id: string; decks?: { cards?: BundleCard[] }[] }[] }).boxes) ?? [];\n for (const box of allBoxes) {\n if (boxes && !boxes.includes(box.gameId ?? box.id)) continue;\n for (const deck of box.decks ?? []) {\n for (const card of deck.cards ?? []) {\n const name = card.gameId ?? card.id;\n const id = sceneFor(name);\n if (id === undefined) {\n if (boxes) problems.push(`card \"${name}\" has no scene of that name`);\n continue;\n }\n played.add(id);\n const scene = scenes[id];\n const declared = (card.outcomes ?? []).map((o) => o.gameId ?? o.id);\n const options = optionsOf(scene);\n const events = outcomesReported(scene);\n const named = [...new Set([...events, ...options.flatMap((o) => (o.outcome ? [o.outcome] : []))])];\n\n for (const n of named) {\n if (!declared.includes(n)) {\n problems.push(`scene \"${name}\" names outcome \"${n}\", which that card does not declare (it declares: ${declared.join(\", \") || \"none\"})`);\n }\n }\n if (declared.length > 1) {\n if (options.length === 0 && events.length === 0) {\n problems.push(`scene \"${name}\" says nothing about its outcome, and its card declares ${declared.length} (${declared.join(\", \")}): label its options, or fire a gameEvent`);\n }\n for (const o of options) {\n if (!o.outcome && o.overrides.length === 0) {\n problems.push(`option \"${o.id}\" in scene \"${name}\" names no outcome and fires no gameEvent, so taking it leaves the host guessing between ${declared.join(\", \")}`);\n }\n }\n for (const d of declared) {\n if (!named.includes(d)) problems.push(`outcome \"${d}\" of card \"${name}\" is named by no option and no gameEvent`);\n }\n }\n }\n }\n }\n if (boxes) {\n for (const [id, scene] of Object.entries(scenes)) {\n if (!played.has(id)) problems.push(`scene \"${scene.gameId?.trim() || id}\" belongs to no card, so nothing can ever play it`);\n }\n }\n return problems;\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -25,6 +25,10 @@ interface SceneOption {
25
25
  text: string;
26
26
  /** False when Patter's condition fails, or the outcome it names is gated shut. */
27
27
  enabled: boolean;
28
+ /** Why it isn't enabled, in a few plain words ("not available here" when Patter's condition
29
+ * fails, "requirements not met" when the outcome is shut); absent when it is. A game will want
30
+ * its own voice here; these say which engine said no. */
31
+ why?: string;
28
32
  /** The outcome the option names, when it names one. */
29
33
  outcome?: string;
30
34
  }
@@ -39,8 +43,9 @@ interface Performance {
39
43
  options?: SceneOption[];
40
44
  /** True once the scene has run to its end. */
41
45
  ended: boolean;
42
- /** The outcome the scene reached (last word wins); undefined until it ends, and after it
43
- * ends when nothing named one and the card has several. */
46
+ /** The outcome the scene reached (last word wins): "" for a card with no outcomes at all, which
47
+ * is played with none. Undefined until it ends, and after it ends when nothing named one and
48
+ * the card has several. */
44
49
  outcome?: string;
45
50
  /** Why the scene cannot be performed, or why it ended without an outcome. */
46
51
  problem?: string;
@@ -79,14 +84,50 @@ declare class Performer {
79
84
  id: string;
80
85
  gameId: string;
81
86
  }, boxGameId: string, outcomes: readonly CardOutcome[]): Performance;
87
+ /**
88
+ * Pick a performance back up after a load. Patter restores the flow's position itself, and a
89
+ * flow paused at a choice comes back still paused, so the options are read off it again. What
90
+ * the host supplies is `saved`, the Performance it stored: the transcript and the outcome settled
91
+ * so far are presentation and bookkeeping, which Patter hands over once and does not keep.
92
+ */
93
+ resume(saved: Performance, outcomes: readonly CardOutcome[]): Performance;
82
94
  /** The player picks an option; the scene runs on to its next choice or its end. */
83
95
  choose(p: Performance, optionId: string, outcomes: readonly CardOutcome[]): Performance;
84
96
  /** A new run: every box's flow closes, so the next card opens a fresh one. Patter's own
85
97
  * properties are the registry's, and the Board resets those with the rest. */
86
98
  reset(): void;
87
99
  private run;
88
- /** Last word wins: an event, else the option's label, else the card's only outcome. */
100
+ /** Last word wins: an event, else the option's label, else the card's only outcome, and a card
101
+ * with no outcomes at all is played with none (""). */
89
102
  private finish;
90
103
  }
91
104
 
92
- export { type Beat, type CardOutcome, type Performance, Performer, type PerformerLink, type SceneOption, sceneIdFor };
105
+ /** Every gameEvent outcome id anywhere under a compiled node, in document order. */
106
+ declare function outcomesReported(node: unknown): string[];
107
+ /** One choice option in a compiled scene, as the pairing reads it. */
108
+ interface BundleOption {
109
+ id: string;
110
+ /** The string id of the words the player picks, when the option has any. */
111
+ promptId?: string;
112
+ /** The outcome the option labels itself with, if any. */
113
+ outcome: string | null;
114
+ /** The gameEvent outcomes its branch fires, which win over the label. */
115
+ overrides: string[];
116
+ }
117
+ /** Every choice option in a compiled scene (a group carrying a prompt). */
118
+ declare function optionsOf(scene: unknown): BundleOption[];
119
+ /**
120
+ * Compare a compiled storylet bundle with a compiled Patter bundle. Returns the problems, one
121
+ * readable line each; empty means they line up.
122
+ *
123
+ * `boxes` (box gameIds) limits the check to the boxes the game performs through Patter: in those,
124
+ * a card with no scene is a problem, and so is a scene no card plays. Without it, every card that
125
+ * has a scene is checked and none is required to have one.
126
+ *
127
+ * The rule enforced is the one the Performer plays by: a gameEvent wins, else the label on the
128
+ * option taken, else the card's only outcome. So a scene whose card has one outcome need say
129
+ * nothing, and one whose card has several must leave no path that says nothing.
130
+ */
131
+ declare function checkPairing(storyletBundle: unknown, patterBundle: unknown, boxes?: readonly string[]): string[];
132
+
133
+ export { type Beat, type BundleOption, type CardOutcome, type Performance, Performer, type PerformerLink, type SceneOption, checkPairing, optionsOf, outcomesReported, sceneIdFor };
package/dist/index.d.ts CHANGED
@@ -25,6 +25,10 @@ interface SceneOption {
25
25
  text: string;
26
26
  /** False when Patter's condition fails, or the outcome it names is gated shut. */
27
27
  enabled: boolean;
28
+ /** Why it isn't enabled, in a few plain words ("not available here" when Patter's condition
29
+ * fails, "requirements not met" when the outcome is shut); absent when it is. A game will want
30
+ * its own voice here; these say which engine said no. */
31
+ why?: string;
28
32
  /** The outcome the option names, when it names one. */
29
33
  outcome?: string;
30
34
  }
@@ -39,8 +43,9 @@ interface Performance {
39
43
  options?: SceneOption[];
40
44
  /** True once the scene has run to its end. */
41
45
  ended: boolean;
42
- /** The outcome the scene reached (last word wins); undefined until it ends, and after it
43
- * ends when nothing named one and the card has several. */
46
+ /** The outcome the scene reached (last word wins): "" for a card with no outcomes at all, which
47
+ * is played with none. Undefined until it ends, and after it ends when nothing named one and
48
+ * the card has several. */
44
49
  outcome?: string;
45
50
  /** Why the scene cannot be performed, or why it ended without an outcome. */
46
51
  problem?: string;
@@ -79,14 +84,50 @@ declare class Performer {
79
84
  id: string;
80
85
  gameId: string;
81
86
  }, boxGameId: string, outcomes: readonly CardOutcome[]): Performance;
87
+ /**
88
+ * Pick a performance back up after a load. Patter restores the flow's position itself, and a
89
+ * flow paused at a choice comes back still paused, so the options are read off it again. What
90
+ * the host supplies is `saved`, the Performance it stored: the transcript and the outcome settled
91
+ * so far are presentation and bookkeeping, which Patter hands over once and does not keep.
92
+ */
93
+ resume(saved: Performance, outcomes: readonly CardOutcome[]): Performance;
82
94
  /** The player picks an option; the scene runs on to its next choice or its end. */
83
95
  choose(p: Performance, optionId: string, outcomes: readonly CardOutcome[]): Performance;
84
96
  /** A new run: every box's flow closes, so the next card opens a fresh one. Patter's own
85
97
  * properties are the registry's, and the Board resets those with the rest. */
86
98
  reset(): void;
87
99
  private run;
88
- /** Last word wins: an event, else the option's label, else the card's only outcome. */
100
+ /** Last word wins: an event, else the option's label, else the card's only outcome, and a card
101
+ * with no outcomes at all is played with none (""). */
89
102
  private finish;
90
103
  }
91
104
 
92
- export { type Beat, type CardOutcome, type Performance, Performer, type PerformerLink, type SceneOption, sceneIdFor };
105
+ /** Every gameEvent outcome id anywhere under a compiled node, in document order. */
106
+ declare function outcomesReported(node: unknown): string[];
107
+ /** One choice option in a compiled scene, as the pairing reads it. */
108
+ interface BundleOption {
109
+ id: string;
110
+ /** The string id of the words the player picks, when the option has any. */
111
+ promptId?: string;
112
+ /** The outcome the option labels itself with, if any. */
113
+ outcome: string | null;
114
+ /** The gameEvent outcomes its branch fires, which win over the label. */
115
+ overrides: string[];
116
+ }
117
+ /** Every choice option in a compiled scene (a group carrying a prompt). */
118
+ declare function optionsOf(scene: unknown): BundleOption[];
119
+ /**
120
+ * Compare a compiled storylet bundle with a compiled Patter bundle. Returns the problems, one
121
+ * readable line each; empty means they line up.
122
+ *
123
+ * `boxes` (box gameIds) limits the check to the boxes the game performs through Patter: in those,
124
+ * a card with no scene is a problem, and so is a scene no card plays. Without it, every card that
125
+ * has a scene is checked and none is required to have one.
126
+ *
127
+ * The rule enforced is the one the Performer plays by: a gameEvent wins, else the label on the
128
+ * option taken, else the card's only outcome. So a scene whose card has one outcome need say
129
+ * nothing, and one whose card has several must leave no path that says nothing.
130
+ */
131
+ declare function checkPairing(storyletBundle: unknown, patterBundle: unknown, boxes?: readonly string[]): string[];
132
+
133
+ export { type Beat, type BundleOption, type CardOutcome, type Performance, Performer, type PerformerLink, type SceneOption, checkPairing, optionsOf, outcomesReported, sceneIdFor };
package/dist/index.js CHANGED
@@ -43,6 +43,19 @@ var Performer = class {
43
43
  }
44
44
  return this.run(p, flow, outcomes);
45
45
  }
46
+ /**
47
+ * Pick a performance back up after a load. Patter restores the flow's position itself, and a
48
+ * flow paused at a choice comes back still paused, so the options are read off it again. What
49
+ * the host supplies is `saved`, the Performance it stored: the transcript and the outcome settled
50
+ * so far are presentation and bookkeeping, which Patter hands over once and does not keep.
51
+ */
52
+ resume(saved, outcomes) {
53
+ if (saved.ended) return saved.outcome !== void 0 || saved.problem !== void 0 ? saved : this.finish(saved, outcomes);
54
+ const flow = this.patter.getFlow(saved.box);
55
+ if (!flow) return { ...saved, ended: true, problem: `The save has no Patter flow "${saved.box}".` };
56
+ const options = flow.getChoices();
57
+ return { ...saved, options: optionsFor(options ?? [], outcomes) };
58
+ }
46
59
  /** The player picks an option; the scene runs on to its next choice or its end. */
47
60
  choose(p, optionId, outcomes) {
48
61
  const flow = this.patter.getFlow(p.box);
@@ -78,12 +91,7 @@ var Performer = class {
78
91
  const named = step.gameData?.["outcome"];
79
92
  if (typeof named === "string") p.lastEvent = named;
80
93
  } else if (step.type === "choice") {
81
- p.options = step.options.map((o) => {
82
- const named = o.gameData?.["outcome"];
83
- const outcome = typeof named === "string" ? named : void 0;
84
- const shut = outcome !== void 0 && outcomes.find((x) => x.gameId === outcome)?.available === false;
85
- return { id: o.id, text: o.prompt?.text || o.id, enabled: o.eligible && !shut, ...outcome !== void 0 ? { outcome } : {} };
86
- });
94
+ p.options = optionsFor(step.options, outcomes);
87
95
  return p;
88
96
  } else {
89
97
  return this.finish(p, outcomes);
@@ -91,8 +99,10 @@ var Performer = class {
91
99
  }
92
100
  return { ...p, ended: true, problem: `The scene ran ${MAX_STEPS} steps without a choice or an end.` };
93
101
  }
94
- /** Last word wins: an event, else the option's label, else the card's only outcome. */
102
+ /** Last word wins: an event, else the option's label, else the card's only outcome, and a card
103
+ * with no outcomes at all is played with none (""). */
95
104
  finish(p, outcomes) {
105
+ if (p.lastEvent === void 0 && p.lastLabel === void 0 && outcomes.length === 0) return { ...p, ended: true, outcome: "" };
96
106
  const reached = p.lastEvent ?? p.lastLabel ?? (outcomes.length === 1 ? outcomes[0].gameId : void 0);
97
107
  if (reached === void 0) {
98
108
  return { ...p, ended: true, problem: "The scene ended without saying which outcome it reached." };
@@ -103,8 +113,116 @@ var Performer = class {
103
113
  return { ...p, ended: true, outcome: reached };
104
114
  }
105
115
  };
116
+ function optionsFor(options, outcomes) {
117
+ return options.map((o) => {
118
+ const named = o.gameData?.["outcome"];
119
+ const outcome = typeof named === "string" ? named : void 0;
120
+ const shut = outcome !== void 0 && outcomes.find((x) => x.gameId === outcome)?.available === false;
121
+ const why = !o.eligible ? "not available here" : shut ? "requirements not met" : void 0;
122
+ return {
123
+ id: o.id,
124
+ text: o.prompt?.text || o.id,
125
+ enabled: o.eligible && !shut,
126
+ ...outcome !== void 0 ? { outcome } : {},
127
+ ...why !== void 0 ? { why } : {}
128
+ };
129
+ });
130
+ }
131
+
132
+ // src/pairing.ts
133
+ import { gameIdify } from "@storylet-studio/model";
134
+ function outcomesReported(node) {
135
+ const found = [];
136
+ (function walk(n) {
137
+ if (Array.isArray(n)) {
138
+ n.forEach(walk);
139
+ return;
140
+ }
141
+ if (!n || typeof n !== "object") return;
142
+ const o = n;
143
+ if (o.kind === "gameEvent" && typeof o.gameData?.outcome === "string") found.push(o.gameData.outcome);
144
+ for (const v of Object.values(n)) walk(v);
145
+ })(node);
146
+ return found;
147
+ }
148
+ function optionsOf(scene) {
149
+ const found = [];
150
+ (function walk(n) {
151
+ if (Array.isArray(n)) {
152
+ n.forEach(walk);
153
+ return;
154
+ }
155
+ if (!n || typeof n !== "object") return;
156
+ const o = n;
157
+ if (o.type === "group" && o.prompt !== void 0) {
158
+ found.push({
159
+ id: String(o.id),
160
+ ...typeof o.prompt?.id === "string" ? { promptId: o.prompt.id } : {},
161
+ outcome: typeof o.gameData?.outcome === "string" ? o.gameData.outcome : null,
162
+ overrides: outcomesReported(o.children ?? [])
163
+ });
164
+ }
165
+ for (const [k, v] of Object.entries(n)) if (k !== "prompt") walk(v);
166
+ })(scene);
167
+ return found;
168
+ }
169
+ function checkPairing(storyletBundle, patterBundle, boxes) {
170
+ const problems = [];
171
+ const scenes = patterBundle.scenes ?? {};
172
+ const address = (s) => s.gameId?.trim() || gameIdify(s.name ?? "");
173
+ const byAddress = new Map(Object.entries(scenes).map(([id, s]) => [address(s), id]));
174
+ const sceneFor = (ref) => scenes[ref] ? ref : byAddress.get(ref);
175
+ const played = /* @__PURE__ */ new Set();
176
+ const allBoxes = storyletBundle.boxes ?? [];
177
+ for (const box of allBoxes) {
178
+ if (boxes && !boxes.includes(box.gameId ?? box.id)) continue;
179
+ for (const deck of box.decks ?? []) {
180
+ for (const card of deck.cards ?? []) {
181
+ const name = card.gameId ?? card.id;
182
+ const id = sceneFor(name);
183
+ if (id === void 0) {
184
+ if (boxes) problems.push(`card "${name}" has no scene of that name`);
185
+ continue;
186
+ }
187
+ played.add(id);
188
+ const scene = scenes[id];
189
+ const declared = (card.outcomes ?? []).map((o) => o.gameId ?? o.id);
190
+ const options = optionsOf(scene);
191
+ const events = outcomesReported(scene);
192
+ const named = [.../* @__PURE__ */ new Set([...events, ...options.flatMap((o) => o.outcome ? [o.outcome] : [])])];
193
+ for (const n of named) {
194
+ if (!declared.includes(n)) {
195
+ problems.push(`scene "${name}" names outcome "${n}", which that card does not declare (it declares: ${declared.join(", ") || "none"})`);
196
+ }
197
+ }
198
+ if (declared.length > 1) {
199
+ if (options.length === 0 && events.length === 0) {
200
+ problems.push(`scene "${name}" says nothing about its outcome, and its card declares ${declared.length} (${declared.join(", ")}): label its options, or fire a gameEvent`);
201
+ }
202
+ for (const o of options) {
203
+ if (!o.outcome && o.overrides.length === 0) {
204
+ problems.push(`option "${o.id}" in scene "${name}" names no outcome and fires no gameEvent, so taking it leaves the host guessing between ${declared.join(", ")}`);
205
+ }
206
+ }
207
+ for (const d of declared) {
208
+ if (!named.includes(d)) problems.push(`outcome "${d}" of card "${name}" is named by no option and no gameEvent`);
209
+ }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ if (boxes) {
215
+ for (const [id, scene] of Object.entries(scenes)) {
216
+ if (!played.has(id)) problems.push(`scene "${scene.gameId?.trim() || id}" belongs to no card, so nothing can ever play it`);
217
+ }
218
+ }
219
+ return problems;
220
+ }
106
221
  export {
107
222
  Performer,
223
+ checkPairing,
224
+ optionsOf,
225
+ outcomesReported,
108
226
  sceneIdFor
109
227
  };
110
228
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/performer.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Performing a dealt card through Patter: the card's gameId names a Patter\n// scene, the scene runs, and the scene decides which of the card's outcomes\n// was reached (the Storylets-with-Patter contract; the Hamlet's\n// `performance.js`, lifted so a game, Storyletter's Board and the playable\n// page all run the same code).\n//\n// The Hamlet's rules, kept exactly because they are the host's contract:\n// - ONE Patter flow per performed box, named after the box, entered with\n// `goto` for each card: a flow is Patter's memory, and a fresh one per card\n// would forget its visits and restart its seeded shuffles (joint demo\n// finding 14).\n// - The outcome is the LAST word: a gameEvent carrying one, else the label on\n// the option the player took, else the card's only outcome (finding 15).\n// - An option is greyed when either engine says no: Patter's `eligible`, or\n// the Storylet Engine's gate on the outcome the option names.\n//\n// This module knows nothing of the DOM, or of the Storylet Engine: the host\n// deals, draws what `start` and `choose` return, and plays the outcome.\n// ---------------------------------------------------------------------------\n\nimport type { Bundle as PatterBundle, Engine as PatterEngine, Flow as PatterFlow, StepResult } from \"@patterkit/runtime\";\n\n/**\n * A card's scene reference (its gameId) to the scene's internal id, as Patter's runtime resolves a\n * reference: an internal id first, else a scene's address by Patter's own rules. Undefined when no\n * scene matches.\n */\nexport function sceneIdFor(engine: PatterEngine, bundle: PatterBundle, ref: string): string | undefined {\n if (bundle.scenes[ref]) return ref;\n return Object.keys(bundle.scenes).find((id) => engine.sceneAddress(id) === ref);\n}\n\n/** One thing the scene has said, for the transcript. */\nexport type Beat =\n | { kind: \"line\"; who?: string; text: string }\n | { kind: \"text\"; text: string }\n /** The player's pick, shown back in the transcript. */\n | { kind: \"chose\"; text: string };\n\nexport interface SceneOption {\n id: string;\n text: string;\n /** False when Patter's condition fails, or the outcome it names is gated shut. */\n enabled: boolean;\n /** The outcome the option names, when it names one. */\n outcome?: string;\n}\n\n/** A card being performed: what has been said, and what the player can do now. */\nexport interface Performance {\n card: string;\n box: string;\n /** The scene's internal id, for reporting the position to Patterpad. */\n sceneId?: string;\n transcript: Beat[];\n /** The choice on screen, when the scene is waiting on one. */\n options?: SceneOption[];\n /** True once the scene has run to its end. */\n ended: boolean;\n /** The outcome the scene reached (last word wins); undefined until it ends, and after it\n * ends when nothing named one and the card has several. */\n outcome?: string;\n /** Why the scene cannot be performed, or why it ended without an outcome. */\n problem?: string;\n /** Bookkeeping for the resolution. */\n lastEvent?: string;\n lastLabel?: string;\n}\n\n/** An outcome of the card, as the Storylet Engine reports it (`Table.outcomes`). */\nexport interface CardOutcome { gameId: string; available: boolean }\n\n/** Where the Board reports its position, so Patterpad's playhead follows the line being played:\n * the subset of play-helpers' `DebugLink` this needs. */\nexport interface PerformerLink {\n flowOpened(flowId: string): void;\n observe(flowId: string, sceneId: string | null, beatId: string | null, type: string, choiceId?: string): void;\n}\n\n/** A guard against a scene that loops without asking anything. */\nconst MAX_STEPS = 500;\n\nexport class Performer {\n /** Where to report the position; set once Patterpad's Live Link is up. */\n link: PerformerLink | undefined;\n\n constructor(\n private patter: PatterEngine,\n private readonly boxes: ReadonlySet<string>,\n /** A card's scene reference (its gameId) to the scene's internal id, as the runtime resolves it. */\n private readonly sceneIdOf: (ref: string) => string | undefined = () => undefined,\n ) {}\n\n /** A live refresh replaced the engine (a structural hot swap): carry on with the new one. */\n setEngine(patter: PatterEngine): void {\n this.patter = patter;\n }\n\n /** Does the project say Patter performs this box? */\n performs(boxGameId: string): boolean {\n return this.boxes.has(boxGameId);\n }\n\n /** Start a card's scene: its flow entered at the scene named after the card, run to the first\n * choice or to its end. */\n start(card: { id: string; gameId: string }, boxGameId: string, outcomes: readonly CardOutcome[]): Performance {\n const sceneId = this.sceneIdOf(card.gameId);\n const p: Performance = { card: card.id, box: boxGameId, transcript: [], ended: false, ...(sceneId !== undefined ? { sceneId } : {}) };\n let flow: PatterFlow;\n try {\n // The engine's own answer, not a list kept here: a loaded save brings its flows back.\n const existing = this.patter.getFlow(boxGameId);\n if (existing) {\n if (!existing.goto(card.gameId)) return { ...p, ended: true, problem: `The Patter project has no scene named \"${card.gameId}\".` };\n flow = existing;\n } else {\n flow = this.patter.openFlow(boxGameId, { scene: card.gameId });\n this.link?.flowOpened(boxGameId);\n }\n } catch (e) {\n return { ...p, ended: true, problem: `The Patter project has no scene named \"${card.gameId}\" (${e instanceof Error ? e.message : String(e)}).` };\n }\n return this.run(p, flow, outcomes);\n }\n\n /** The player picks an option; the scene runs on to its next choice or its end. */\n choose(p: Performance, optionId: string, outcomes: readonly CardOutcome[]): Performance {\n const flow = this.patter.getFlow(p.box);\n const option = p.options?.find((o) => o.id === optionId);\n if (!flow || !option || !option.enabled) return p;\n flow.choose(optionId);\n this.link?.observe(p.box, p.sceneId ?? null, null, \"choose\", optionId);\n const next: Performance = {\n ...p, options: undefined, transcript: [...p.transcript, { kind: \"chose\", text: option.text }],\n ...(option.outcome !== undefined ? { lastLabel: option.outcome } : {}),\n };\n return this.run(next, flow, outcomes);\n }\n\n /** A new run: every box's flow closes, so the next card opens a fresh one. Patter's own\n * properties are the registry's, and the Board resets those with the rest. */\n reset(): void {\n for (const box of this.boxes) {\n if (this.patter.getFlow(box)) this.patter.closeFlow(box);\n }\n }\n\n private run(start: Performance, flow: PatterFlow, outcomes: readonly CardOutcome[]): Performance {\n const p: Performance = { ...start, transcript: [...start.transcript] };\n for (let i = 0; i < MAX_STEPS; i++) {\n const step: StepResult = flow.advance();\n this.link?.observe(p.box, p.sceneId ?? null, \"id\" in step ? step.id : null, step.type);\n if (step.type === \"line\") {\n p.transcript.push({ kind: \"line\", ...(step.characterName ?? step.character ? { who: step.characterName ?? step.character } : {}), text: step.text });\n } else if (step.type === \"text\") {\n p.transcript.push({ kind: \"text\", text: step.text });\n } else if (step.type === \"gameEvent\") {\n const named = step.gameData?.[\"outcome\"];\n if (typeof named === \"string\") p.lastEvent = named;\n } else if (step.type === \"choice\") {\n p.options = step.options.map((o) => {\n const named = o.gameData?.[\"outcome\"];\n const outcome = typeof named === \"string\" ? named : undefined;\n // Two gates, each engine's own: Patter's condition, and ours on the outcome it names.\n const shut = outcome !== undefined && outcomes.find((x) => x.gameId === outcome)?.available === false;\n return { id: o.id, text: o.prompt?.text || o.id, enabled: o.eligible && !shut, ...(outcome !== undefined ? { outcome } : {}) };\n });\n return p;\n } else {\n return this.finish(p, outcomes);\n }\n }\n return { ...p, ended: true, problem: `The scene ran ${MAX_STEPS} steps without a choice or an end.` };\n }\n\n /** Last word wins: an event, else the option's label, else the card's only outcome. */\n private finish(p: Performance, outcomes: readonly CardOutcome[]): Performance {\n const reached = p.lastEvent ?? p.lastLabel ?? (outcomes.length === 1 ? outcomes[0]!.gameId : undefined);\n if (reached === undefined) {\n return { ...p, ended: true, problem: \"The scene ended without saying which outcome it reached.\" };\n }\n if (!outcomes.some((o) => o.gameId === reached)) {\n return { ...p, ended: true, problem: `The scene reached \"${reached}\", which this card doesn't have.` };\n }\n return { ...p, ended: true, outcome: reached };\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/performer.ts","../src/pairing.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Performing a dealt card through Patter: the card's gameId names a Patter\n// scene, the scene runs, and the scene decides which of the card's outcomes\n// was reached (the Storylets-with-Patter contract; the Hamlet's\n// `performance.js`, lifted so a game, Storyletter's Board and the playable\n// page all run the same code).\n//\n// The Hamlet's rules, kept exactly because they are the host's contract:\n// - ONE Patter flow per performed box, named after the box, entered with\n// `goto` for each card: a flow is Patter's memory, and a fresh one per card\n// would forget its visits and restart its seeded shuffles (joint demo\n// finding 14).\n// - The outcome is the LAST word: a gameEvent carrying one, else the label on\n// the option the player took, else the card's only outcome (finding 15).\n// - An option is greyed when either engine says no: Patter's `eligible`, or\n// the Storylet Engine's gate on the outcome the option names.\n//\n// This module knows nothing of the DOM, or of the Storylet Engine: the host\n// deals, draws what `start` and `choose` return, and plays the outcome.\n// ---------------------------------------------------------------------------\n\nimport type { Bundle as PatterBundle, ChoiceOption, Engine as PatterEngine, Flow as PatterFlow, StepResult } from \"@patterkit/runtime\";\n\n/**\n * A card's scene reference (its gameId) to the scene's internal id, as Patter's runtime resolves a\n * reference: an internal id first, else a scene's address by Patter's own rules. Undefined when no\n * scene matches.\n */\nexport function sceneIdFor(engine: PatterEngine, bundle: PatterBundle, ref: string): string | undefined {\n if (bundle.scenes[ref]) return ref;\n return Object.keys(bundle.scenes).find((id) => engine.sceneAddress(id) === ref);\n}\n\n/** One thing the scene has said, for the transcript. */\nexport type Beat =\n | { kind: \"line\"; who?: string; text: string }\n | { kind: \"text\"; text: string }\n /** The player's pick, shown back in the transcript. */\n | { kind: \"chose\"; text: string };\n\nexport interface SceneOption {\n id: string;\n text: string;\n /** False when Patter's condition fails, or the outcome it names is gated shut. */\n enabled: boolean;\n /** Why it isn't enabled, in a few plain words (\"not available here\" when Patter's condition\n * fails, \"requirements not met\" when the outcome is shut); absent when it is. A game will want\n * its own voice here; these say which engine said no. */\n why?: string;\n /** The outcome the option names, when it names one. */\n outcome?: string;\n}\n\n/** A card being performed: what has been said, and what the player can do now. */\nexport interface Performance {\n card: string;\n box: string;\n /** The scene's internal id, for reporting the position to Patterpad. */\n sceneId?: string;\n transcript: Beat[];\n /** The choice on screen, when the scene is waiting on one. */\n options?: SceneOption[];\n /** True once the scene has run to its end. */\n ended: boolean;\n /** The outcome the scene reached (last word wins): \"\" for a card with no outcomes at all, which\n * is played with none. Undefined until it ends, and after it ends when nothing named one and\n * the card has several. */\n outcome?: string;\n /** Why the scene cannot be performed, or why it ended without an outcome. */\n problem?: string;\n /** Bookkeeping for the resolution. */\n lastEvent?: string;\n lastLabel?: string;\n}\n\n/** An outcome of the card, as the Storylet Engine reports it (`Table.outcomes`). */\nexport interface CardOutcome { gameId: string; available: boolean }\n\n/** Where the Board reports its position, so Patterpad's playhead follows the line being played:\n * the subset of play-helpers' `DebugLink` this needs. */\nexport interface PerformerLink {\n flowOpened(flowId: string): void;\n observe(flowId: string, sceneId: string | null, beatId: string | null, type: string, choiceId?: string): void;\n}\n\n/** A guard against a scene that loops without asking anything. */\nconst MAX_STEPS = 500;\n\nexport class Performer {\n /** Where to report the position; set once Patterpad's Live Link is up. */\n link: PerformerLink | undefined;\n\n constructor(\n private patter: PatterEngine,\n private readonly boxes: ReadonlySet<string>,\n /** A card's scene reference (its gameId) to the scene's internal id, as the runtime resolves it. */\n private readonly sceneIdOf: (ref: string) => string | undefined = () => undefined,\n ) {}\n\n /** A live refresh replaced the engine (a structural hot swap): carry on with the new one. */\n setEngine(patter: PatterEngine): void {\n this.patter = patter;\n }\n\n /** Does the project say Patter performs this box? */\n performs(boxGameId: string): boolean {\n return this.boxes.has(boxGameId);\n }\n\n /** Start a card's scene: its flow entered at the scene named after the card, run to the first\n * choice or to its end. */\n start(card: { id: string; gameId: string }, boxGameId: string, outcomes: readonly CardOutcome[]): Performance {\n const sceneId = this.sceneIdOf(card.gameId);\n const p: Performance = { card: card.id, box: boxGameId, transcript: [], ended: false, ...(sceneId !== undefined ? { sceneId } : {}) };\n let flow: PatterFlow;\n try {\n // The engine's own answer, not a list kept here: a loaded save brings its flows back.\n const existing = this.patter.getFlow(boxGameId);\n if (existing) {\n if (!existing.goto(card.gameId)) return { ...p, ended: true, problem: `The Patter project has no scene named \"${card.gameId}\".` };\n flow = existing;\n } else {\n flow = this.patter.openFlow(boxGameId, { scene: card.gameId });\n this.link?.flowOpened(boxGameId);\n }\n } catch (e) {\n return { ...p, ended: true, problem: `The Patter project has no scene named \"${card.gameId}\" (${e instanceof Error ? e.message : String(e)}).` };\n }\n return this.run(p, flow, outcomes);\n }\n\n /**\n * Pick a performance back up after a load. Patter restores the flow's position itself, and a\n * flow paused at a choice comes back still paused, so the options are read off it again. What\n * the host supplies is `saved`, the Performance it stored: the transcript and the outcome settled\n * so far are presentation and bookkeeping, which Patter hands over once and does not keep.\n */\n resume(saved: Performance, outcomes: readonly CardOutcome[]): Performance {\n // Ended and not yet continued: nothing to ask Patter, but a save that kept only the bookkeeping\n // (another host's, say) gets its outcome worked out again, by the same rule.\n if (saved.ended) return saved.outcome !== undefined || saved.problem !== undefined ? saved : this.finish(saved, outcomes);\n const flow = this.patter.getFlow(saved.box);\n if (!flow) return { ...saved, ended: true, problem: `The save has no Patter flow \"${saved.box}\".` };\n const options = flow.getChoices();\n return { ...saved, options: optionsFor(options ?? [], outcomes) };\n }\n\n /** The player picks an option; the scene runs on to its next choice or its end. */\n choose(p: Performance, optionId: string, outcomes: readonly CardOutcome[]): Performance {\n const flow = this.patter.getFlow(p.box);\n const option = p.options?.find((o) => o.id === optionId);\n if (!flow || !option || !option.enabled) return p;\n flow.choose(optionId);\n this.link?.observe(p.box, p.sceneId ?? null, null, \"choose\", optionId);\n const next: Performance = {\n ...p, options: undefined, transcript: [...p.transcript, { kind: \"chose\", text: option.text }],\n ...(option.outcome !== undefined ? { lastLabel: option.outcome } : {}),\n };\n return this.run(next, flow, outcomes);\n }\n\n /** A new run: every box's flow closes, so the next card opens a fresh one. Patter's own\n * properties are the registry's, and the Board resets those with the rest. */\n reset(): void {\n for (const box of this.boxes) {\n if (this.patter.getFlow(box)) this.patter.closeFlow(box);\n }\n }\n\n private run(start: Performance, flow: PatterFlow, outcomes: readonly CardOutcome[]): Performance {\n const p: Performance = { ...start, transcript: [...start.transcript] };\n for (let i = 0; i < MAX_STEPS; i++) {\n const step: StepResult = flow.advance();\n this.link?.observe(p.box, p.sceneId ?? null, \"id\" in step ? step.id : null, step.type);\n if (step.type === \"line\") {\n p.transcript.push({ kind: \"line\", ...(step.characterName ?? step.character ? { who: step.characterName ?? step.character } : {}), text: step.text });\n } else if (step.type === \"text\") {\n p.transcript.push({ kind: \"text\", text: step.text });\n } else if (step.type === \"gameEvent\") {\n const named = step.gameData?.[\"outcome\"];\n if (typeof named === \"string\") p.lastEvent = named;\n } else if (step.type === \"choice\") {\n p.options = optionsFor(step.options, outcomes);\n return p;\n } else {\n return this.finish(p, outcomes);\n }\n }\n return { ...p, ended: true, problem: `The scene ran ${MAX_STEPS} steps without a choice or an end.` };\n }\n\n /** Last word wins: an event, else the option's label, else the card's only outcome, and a card\n * with no outcomes at all is played with none (\"\"). */\n private finish(p: Performance, outcomes: readonly CardOutcome[]): Performance {\n if (p.lastEvent === undefined && p.lastLabel === undefined && outcomes.length === 0) return { ...p, ended: true, outcome: \"\" };\n const reached = p.lastEvent ?? p.lastLabel ?? (outcomes.length === 1 ? outcomes[0]!.gameId : undefined);\n if (reached === undefined) {\n return { ...p, ended: true, problem: \"The scene ended without saying which outcome it reached.\" };\n }\n if (!outcomes.some((o) => o.gameId === reached)) {\n return { ...p, ended: true, problem: `The scene reached \"${reached}\", which this card doesn't have.` };\n }\n return { ...p, ended: true, outcome: reached };\n }\n}\n\n/** A choice's options with both engines' gates applied: Patter's own condition (`eligible`), and\n * the Storylet Engine's on the outcome each option names. */\nfunction optionsFor(options: readonly ChoiceOption[], outcomes: readonly CardOutcome[]): SceneOption[] {\n return options.map((o) => {\n const named = o.gameData?.[\"outcome\"];\n const outcome = typeof named === \"string\" ? named : undefined;\n const shut = outcome !== undefined && outcomes.find((x) => x.gameId === outcome)?.available === false;\n const why = !o.eligible ? \"not available here\" : shut ? \"requirements not met\" : undefined;\n return {\n id: o.id, text: o.prompt?.text || o.id, enabled: o.eligible && !shut,\n ...(outcome !== undefined ? { outcome } : {}), ...(why !== undefined ? { why } : {}),\n };\n });\n}\n","// ---------------------------------------------------------------------------\n// Cards against their scenes, on the two PUBLISHED bundles: the build-time\n// check a game runs so the naming convention is safe (lifted from the Hamlet's\n// scripts/pairing.mjs, whose messages it keeps). Nothing declares the links, so\n// nothing else validates them: the failure it catches is a card that performs\n// no dialogue, or a branch that ends without saying what happened, and both\n// look exactly like content somebody meant to write.\n//\n// A card finds its scene as Patter's runtime resolves a reference: an internal\n// id first, else an address (a pinned gameId, else the name's slug). Storyletter\n// runs the same analysis on the project as you edit (ops `patter-link.ts`, which\n// takes the two walkers below from here); this is the game's copy, on what ships.\n// ---------------------------------------------------------------------------\n\nimport { gameIdify } from \"@storylet-studio/model\";\n\n/** Every gameEvent outcome id anywhere under a compiled node, in document order. */\nexport function outcomesReported(node: unknown): string[] {\n const found: string[] = [];\n (function walk(n: unknown): void {\n if (Array.isArray(n)) { n.forEach(walk); return; }\n if (!n || typeof n !== \"object\") return;\n const o = n as { kind?: unknown; gameData?: { outcome?: unknown } };\n if (o.kind === \"gameEvent\" && typeof o.gameData?.outcome === \"string\") found.push(o.gameData.outcome);\n for (const v of Object.values(n)) walk(v);\n })(node);\n return found;\n}\n\n/** One choice option in a compiled scene, as the pairing reads it. */\nexport interface BundleOption {\n id: string;\n /** The string id of the words the player picks, when the option has any. */\n promptId?: string;\n /** The outcome the option labels itself with, if any. */\n outcome: string | null;\n /** The gameEvent outcomes its branch fires, which win over the label. */\n overrides: string[];\n}\n\n/** Every choice option in a compiled scene (a group carrying a prompt). */\nexport function optionsOf(scene: unknown): BundleOption[] {\n const found: BundleOption[] = [];\n (function walk(n: unknown): void {\n if (Array.isArray(n)) { n.forEach(walk); return; }\n if (!n || typeof n !== \"object\") return;\n const o = n as { type?: unknown; prompt?: { id?: unknown }; id?: unknown; gameData?: { outcome?: unknown }; children?: unknown };\n if (o.type === \"group\" && o.prompt !== undefined) {\n found.push({\n id: String(o.id),\n ...(typeof o.prompt?.id === \"string\" ? { promptId: o.prompt.id } : {}),\n outcome: typeof o.gameData?.outcome === \"string\" ? o.gameData.outcome : null,\n overrides: outcomesReported(o.children ?? []),\n });\n }\n // The prompt is text, never structure: walking it could mistake a nested group for an option.\n for (const [k, v] of Object.entries(n)) if (k !== \"prompt\") walk(v);\n })(scene);\n return found;\n}\n\ninterface BundleCard { id: string; gameId?: string; outcomes?: { gameId?: string; id: string }[] }\ninterface BundleScene { name?: string; gameId?: string; [key: string]: unknown }\n\n/**\n * Compare a compiled storylet bundle with a compiled Patter bundle. Returns the problems, one\n * readable line each; empty means they line up.\n *\n * `boxes` (box gameIds) limits the check to the boxes the game performs through Patter: in those,\n * a card with no scene is a problem, and so is a scene no card plays. Without it, every card that\n * has a scene is checked and none is required to have one.\n *\n * The rule enforced is the one the Performer plays by: a gameEvent wins, else the label on the\n * option taken, else the card's only outcome. So a scene whose card has one outcome need say\n * nothing, and one whose card has several must leave no path that says nothing.\n */\nexport function checkPairing(storyletBundle: unknown, patterBundle: unknown, boxes?: readonly string[]): string[] {\n const problems: string[] = [];\n const scenes = ((patterBundle as { scenes?: Record<string, BundleScene> }).scenes) ?? {};\n const address = (s: BundleScene): string => s.gameId?.trim() || gameIdify(s.name ?? \"\");\n const byAddress = new Map(Object.entries(scenes).map(([id, s]) => [address(s), id]));\n const sceneFor = (ref: string): string | undefined => (scenes[ref] ? ref : byAddress.get(ref));\n const played = new Set<string>();\n\n const allBoxes = ((storyletBundle as { boxes?: { gameId?: string; id: string; decks?: { cards?: BundleCard[] }[] }[] }).boxes) ?? [];\n for (const box of allBoxes) {\n if (boxes && !boxes.includes(box.gameId ?? box.id)) continue;\n for (const deck of box.decks ?? []) {\n for (const card of deck.cards ?? []) {\n const name = card.gameId ?? card.id;\n const id = sceneFor(name);\n if (id === undefined) {\n if (boxes) problems.push(`card \"${name}\" has no scene of that name`);\n continue;\n }\n played.add(id);\n const scene = scenes[id];\n const declared = (card.outcomes ?? []).map((o) => o.gameId ?? o.id);\n const options = optionsOf(scene);\n const events = outcomesReported(scene);\n const named = [...new Set([...events, ...options.flatMap((o) => (o.outcome ? [o.outcome] : []))])];\n\n for (const n of named) {\n if (!declared.includes(n)) {\n problems.push(`scene \"${name}\" names outcome \"${n}\", which that card does not declare (it declares: ${declared.join(\", \") || \"none\"})`);\n }\n }\n if (declared.length > 1) {\n if (options.length === 0 && events.length === 0) {\n problems.push(`scene \"${name}\" says nothing about its outcome, and its card declares ${declared.length} (${declared.join(\", \")}): label its options, or fire a gameEvent`);\n }\n for (const o of options) {\n if (!o.outcome && o.overrides.length === 0) {\n problems.push(`option \"${o.id}\" in scene \"${name}\" names no outcome and fires no gameEvent, so taking it leaves the host guessing between ${declared.join(\", \")}`);\n }\n }\n for (const d of declared) {\n if (!named.includes(d)) problems.push(`outcome \"${d}\" of card \"${name}\" is named by no option and no gameEvent`);\n }\n }\n }\n }\n }\n if (boxes) {\n for (const [id, scene] of Object.entries(scenes)) {\n if (!played.has(id)) problems.push(`scene \"${scene.gameId?.trim() || id}\" belongs to no card, so nothing can ever play it`);\n }\n }\n return problems;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,2 @@
1
+ "use strict";var StoryletsWithPatter=(()=>{var x=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var M=Object.prototype.hasOwnProperty;var T=(i,e)=>{for(var r in e)x(i,r,{get:e[r],enumerable:!0})},D=(i,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of B(e))!M.call(i,t)&&t!==r&&x(i,t,{get:()=>e[t],enumerable:!(n=O(e,t))||n.enumerable});return i};var H=i=>D(x({},"__esModule",{value:!0}),i);var $={};T($,{Performer:()=>u,checkPairing:()=>I,optionsOf:()=>b,outcomesReported:()=>f,sceneIdFor:()=>R});function R(i,e,r){return e.scenes[r]?r:Object.keys(e.scenes).find(n=>i.sceneAddress(n)===r)}var v=500,u=class{constructor(e,r,n=()=>{}){this.patter=e;this.boxes=r;this.sceneIdOf=n}patter;boxes;sceneIdOf;link;setEngine(e){this.patter=e}performs(e){return this.boxes.has(e)}start(e,r,n){let t=this.sceneIdOf(e.gameId),s={card:e.id,box:r,transcript:[],ended:!1,...t!==void 0?{sceneId:t}:{}},o;try{let a=this.patter.getFlow(r);if(a){if(!a.goto(e.gameId))return{...s,ended:!0,problem:`The Patter project has no scene named "${e.gameId}".`};o=a}else o=this.patter.openFlow(r,{scene:e.gameId}),this.link?.flowOpened(r)}catch(a){return{...s,ended:!0,problem:`The Patter project has no scene named "${e.gameId}" (${a instanceof Error?a.message:String(a)}).`}}return this.run(s,o,n)}resume(e,r){if(e.ended)return e.outcome!==void 0||e.problem!==void 0?e:this.finish(e,r);let n=this.patter.getFlow(e.box);if(!n)return{...e,ended:!0,problem:`The save has no Patter flow "${e.box}".`};let t=n.getChoices();return{...e,options:C(t??[],r)}}choose(e,r,n){let t=this.patter.getFlow(e.box),s=e.options?.find(a=>a.id===r);if(!t||!s||!s.enabled)return e;t.choose(r),this.link?.observe(e.box,e.sceneId??null,null,"choose",r);let o={...e,options:void 0,transcript:[...e.transcript,{kind:"chose",text:s.text}],...s.outcome!==void 0?{lastLabel:s.outcome}:{}};return this.run(o,t,n)}reset(){for(let e of this.boxes)this.patter.getFlow(e)&&this.patter.closeFlow(e)}run(e,r,n){let t={...e,transcript:[...e.transcript]};for(let s=0;s<v;s++){let o=r.advance();if(this.link?.observe(t.box,t.sceneId??null,"id"in o?o.id:null,o.type),o.type==="line")t.transcript.push({kind:"line",...o.characterName??o.character?{who:o.characterName??o.character}:{},text:o.text});else if(o.type==="text")t.transcript.push({kind:"text",text:o.text});else if(o.type==="gameEvent"){let a=o.gameData?.outcome;typeof a=="string"&&(t.lastEvent=a)}else return o.type==="choice"?(t.options=C(o.options,n),t):this.finish(t,n)}return{...t,ended:!0,problem:`The scene ran ${v} steps without a choice or an end.`}}finish(e,r){if(e.lastEvent===void 0&&e.lastLabel===void 0&&r.length===0)return{...e,ended:!0,outcome:""};let n=e.lastEvent??e.lastLabel??(r.length===1?r[0].gameId:void 0);return n===void 0?{...e,ended:!0,problem:"The scene ended without saying which outcome it reached."}:r.some(t=>t.gameId===n)?{...e,ended:!0,outcome:n}:{...e,ended:!0,problem:`The scene reached "${n}", which this card doesn't have.`}}};function C(i,e){return i.map(r=>{let n=r.gameData?.outcome,t=typeof n=="string"?n:void 0,s=t!==void 0&&e.find(a=>a.gameId===t)?.available===!1,o=r.eligible?s?"requirements not met":void 0:"not available here";return{id:r.id,text:r.prompt?.text||r.id,enabled:r.eligible&&!s,...t!==void 0?{outcome:t}:{},...o!==void 0?{why:o}:{}}})}function k(i){return i.toLowerCase().replace(/['’]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function f(i){let e=[];return(function r(n){if(Array.isArray(n)){n.forEach(r);return}if(!n||typeof n!="object")return;let t=n;t.kind==="gameEvent"&&typeof t.gameData?.outcome=="string"&&e.push(t.gameData.outcome);for(let s of Object.values(n))r(s)})(i),e}function b(i){let e=[];return(function r(n){if(Array.isArray(n)){n.forEach(r);return}if(!n||typeof n!="object")return;let t=n;t.type==="group"&&t.prompt!==void 0&&e.push({id:String(t.id),...typeof t.prompt?.id=="string"?{promptId:t.prompt.id}:{},outcome:typeof t.gameData?.outcome=="string"?t.gameData.outcome:null,overrides:f(t.children??[])});for(let[s,o]of Object.entries(n))s!=="prompt"&&r(o)})(i),e}function I(i,e,r){let n=[],t=e.scenes??{},s=d=>d.gameId?.trim()||k(d.name??""),o=new Map(Object.entries(t).map(([d,l])=>[s(l),d])),a=d=>t[d]?d:o.get(d),E=new Set,A=i.boxes??[];for(let d of A)if(!(r&&!r.includes(d.gameId??d.id)))for(let l of d.decks??[])for(let m of l.cards??[]){let g=m.gameId??m.id,y=a(g);if(y===void 0){r&&n.push(`card "${g}" has no scene of that name`);continue}E.add(y);let P=t[y],p=(m.outcomes??[]).map(c=>c.gameId??c.id),h=b(P),S=f(P),w=[...new Set([...S,...h.flatMap(c=>c.outcome?[c.outcome]:[])])];for(let c of w)p.includes(c)||n.push(`scene "${g}" names outcome "${c}", which that card does not declare (it declares: ${p.join(", ")||"none"})`);if(p.length>1){h.length===0&&S.length===0&&n.push(`scene "${g}" says nothing about its outcome, and its card declares ${p.length} (${p.join(", ")}): label its options, or fire a gameEvent`);for(let c of h)!c.outcome&&c.overrides.length===0&&n.push(`option "${c.id}" in scene "${g}" names no outcome and fires no gameEvent, so taking it leaves the host guessing between ${p.join(", ")}`);for(let c of p)w.includes(c)||n.push(`outcome "${c}" of card "${g}" is named by no option and no gameEvent`)}}if(r)for(let[d,l]of Object.entries(t))E.has(d)||n.push(`scene "${l.gameId?.trim()||d}" belongs to no card, so nothing can ever play it`);return n}return H($);})();
2
+ //# sourceMappingURL=with-patter.min.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/performer.ts","../../model/src/index.ts","../src/pairing.ts"],"sourcesContent":["// @storylet-studio/with-patter: perform a dealt storylet card as the Patter scene named after it,\n// and check, at build time, that the cards and the scenes line up.\nexport { Performer, sceneIdFor } from \"./performer.js\";\nexport type { Beat, CardOutcome, Performance, PerformerLink, SceneOption } from \"./performer.js\";\nexport { checkPairing, optionsOf, outcomesReported } from \"./pairing.js\";\nexport type { BundleOption } from \"./pairing.js\";\n","// ---------------------------------------------------------------------------\n// Performing a dealt card through Patter: the card's gameId names a Patter\n// scene, the scene runs, and the scene decides which of the card's outcomes\n// was reached (the Storylets-with-Patter contract; the Hamlet's\n// `performance.js`, lifted so a game, Storyletter's Board and the playable\n// page all run the same code).\n//\n// The Hamlet's rules, kept exactly because they are the host's contract:\n// - ONE Patter flow per performed box, named after the box, entered with\n// `goto` for each card: a flow is Patter's memory, and a fresh one per card\n// would forget its visits and restart its seeded shuffles (joint demo\n// finding 14).\n// - The outcome is the LAST word: a gameEvent carrying one, else the label on\n// the option the player took, else the card's only outcome (finding 15).\n// - An option is greyed when either engine says no: Patter's `eligible`, or\n// the Storylet Engine's gate on the outcome the option names.\n//\n// This module knows nothing of the DOM, or of the Storylet Engine: the host\n// deals, draws what `start` and `choose` return, and plays the outcome.\n// ---------------------------------------------------------------------------\n\nimport type { Bundle as PatterBundle, ChoiceOption, Engine as PatterEngine, Flow as PatterFlow, StepResult } from \"@patterkit/runtime\";\n\n/**\n * A card's scene reference (its gameId) to the scene's internal id, as Patter's runtime resolves a\n * reference: an internal id first, else a scene's address by Patter's own rules. Undefined when no\n * scene matches.\n */\nexport function sceneIdFor(engine: PatterEngine, bundle: PatterBundle, ref: string): string | undefined {\n if (bundle.scenes[ref]) return ref;\n return Object.keys(bundle.scenes).find((id) => engine.sceneAddress(id) === ref);\n}\n\n/** One thing the scene has said, for the transcript. */\nexport type Beat =\n | { kind: \"line\"; who?: string; text: string }\n | { kind: \"text\"; text: string }\n /** The player's pick, shown back in the transcript. */\n | { kind: \"chose\"; text: string };\n\nexport interface SceneOption {\n id: string;\n text: string;\n /** False when Patter's condition fails, or the outcome it names is gated shut. */\n enabled: boolean;\n /** Why it isn't enabled, in a few plain words (\"not available here\" when Patter's condition\n * fails, \"requirements not met\" when the outcome is shut); absent when it is. A game will want\n * its own voice here; these say which engine said no. */\n why?: string;\n /** The outcome the option names, when it names one. */\n outcome?: string;\n}\n\n/** A card being performed: what has been said, and what the player can do now. */\nexport interface Performance {\n card: string;\n box: string;\n /** The scene's internal id, for reporting the position to Patterpad. */\n sceneId?: string;\n transcript: Beat[];\n /** The choice on screen, when the scene is waiting on one. */\n options?: SceneOption[];\n /** True once the scene has run to its end. */\n ended: boolean;\n /** The outcome the scene reached (last word wins): \"\" for a card with no outcomes at all, which\n * is played with none. Undefined until it ends, and after it ends when nothing named one and\n * the card has several. */\n outcome?: string;\n /** Why the scene cannot be performed, or why it ended without an outcome. */\n problem?: string;\n /** Bookkeeping for the resolution. */\n lastEvent?: string;\n lastLabel?: string;\n}\n\n/** An outcome of the card, as the Storylet Engine reports it (`Table.outcomes`). */\nexport interface CardOutcome { gameId: string; available: boolean }\n\n/** Where the Board reports its position, so Patterpad's playhead follows the line being played:\n * the subset of play-helpers' `DebugLink` this needs. */\nexport interface PerformerLink {\n flowOpened(flowId: string): void;\n observe(flowId: string, sceneId: string | null, beatId: string | null, type: string, choiceId?: string): void;\n}\n\n/** A guard against a scene that loops without asking anything. */\nconst MAX_STEPS = 500;\n\nexport class Performer {\n /** Where to report the position; set once Patterpad's Live Link is up. */\n link: PerformerLink | undefined;\n\n constructor(\n private patter: PatterEngine,\n private readonly boxes: ReadonlySet<string>,\n /** A card's scene reference (its gameId) to the scene's internal id, as the runtime resolves it. */\n private readonly sceneIdOf: (ref: string) => string | undefined = () => undefined,\n ) {}\n\n /** A live refresh replaced the engine (a structural hot swap): carry on with the new one. */\n setEngine(patter: PatterEngine): void {\n this.patter = patter;\n }\n\n /** Does the project say Patter performs this box? */\n performs(boxGameId: string): boolean {\n return this.boxes.has(boxGameId);\n }\n\n /** Start a card's scene: its flow entered at the scene named after the card, run to the first\n * choice or to its end. */\n start(card: { id: string; gameId: string }, boxGameId: string, outcomes: readonly CardOutcome[]): Performance {\n const sceneId = this.sceneIdOf(card.gameId);\n const p: Performance = { card: card.id, box: boxGameId, transcript: [], ended: false, ...(sceneId !== undefined ? { sceneId } : {}) };\n let flow: PatterFlow;\n try {\n // The engine's own answer, not a list kept here: a loaded save brings its flows back.\n const existing = this.patter.getFlow(boxGameId);\n if (existing) {\n if (!existing.goto(card.gameId)) return { ...p, ended: true, problem: `The Patter project has no scene named \"${card.gameId}\".` };\n flow = existing;\n } else {\n flow = this.patter.openFlow(boxGameId, { scene: card.gameId });\n this.link?.flowOpened(boxGameId);\n }\n } catch (e) {\n return { ...p, ended: true, problem: `The Patter project has no scene named \"${card.gameId}\" (${e instanceof Error ? e.message : String(e)}).` };\n }\n return this.run(p, flow, outcomes);\n }\n\n /**\n * Pick a performance back up after a load. Patter restores the flow's position itself, and a\n * flow paused at a choice comes back still paused, so the options are read off it again. What\n * the host supplies is `saved`, the Performance it stored: the transcript and the outcome settled\n * so far are presentation and bookkeeping, which Patter hands over once and does not keep.\n */\n resume(saved: Performance, outcomes: readonly CardOutcome[]): Performance {\n // Ended and not yet continued: nothing to ask Patter, but a save that kept only the bookkeeping\n // (another host's, say) gets its outcome worked out again, by the same rule.\n if (saved.ended) return saved.outcome !== undefined || saved.problem !== undefined ? saved : this.finish(saved, outcomes);\n const flow = this.patter.getFlow(saved.box);\n if (!flow) return { ...saved, ended: true, problem: `The save has no Patter flow \"${saved.box}\".` };\n const options = flow.getChoices();\n return { ...saved, options: optionsFor(options ?? [], outcomes) };\n }\n\n /** The player picks an option; the scene runs on to its next choice or its end. */\n choose(p: Performance, optionId: string, outcomes: readonly CardOutcome[]): Performance {\n const flow = this.patter.getFlow(p.box);\n const option = p.options?.find((o) => o.id === optionId);\n if (!flow || !option || !option.enabled) return p;\n flow.choose(optionId);\n this.link?.observe(p.box, p.sceneId ?? null, null, \"choose\", optionId);\n const next: Performance = {\n ...p, options: undefined, transcript: [...p.transcript, { kind: \"chose\", text: option.text }],\n ...(option.outcome !== undefined ? { lastLabel: option.outcome } : {}),\n };\n return this.run(next, flow, outcomes);\n }\n\n /** A new run: every box's flow closes, so the next card opens a fresh one. Patter's own\n * properties are the registry's, and the Board resets those with the rest. */\n reset(): void {\n for (const box of this.boxes) {\n if (this.patter.getFlow(box)) this.patter.closeFlow(box);\n }\n }\n\n private run(start: Performance, flow: PatterFlow, outcomes: readonly CardOutcome[]): Performance {\n const p: Performance = { ...start, transcript: [...start.transcript] };\n for (let i = 0; i < MAX_STEPS; i++) {\n const step: StepResult = flow.advance();\n this.link?.observe(p.box, p.sceneId ?? null, \"id\" in step ? step.id : null, step.type);\n if (step.type === \"line\") {\n p.transcript.push({ kind: \"line\", ...(step.characterName ?? step.character ? { who: step.characterName ?? step.character } : {}), text: step.text });\n } else if (step.type === \"text\") {\n p.transcript.push({ kind: \"text\", text: step.text });\n } else if (step.type === \"gameEvent\") {\n const named = step.gameData?.[\"outcome\"];\n if (typeof named === \"string\") p.lastEvent = named;\n } else if (step.type === \"choice\") {\n p.options = optionsFor(step.options, outcomes);\n return p;\n } else {\n return this.finish(p, outcomes);\n }\n }\n return { ...p, ended: true, problem: `The scene ran ${MAX_STEPS} steps without a choice or an end.` };\n }\n\n /** Last word wins: an event, else the option's label, else the card's only outcome, and a card\n * with no outcomes at all is played with none (\"\"). */\n private finish(p: Performance, outcomes: readonly CardOutcome[]): Performance {\n if (p.lastEvent === undefined && p.lastLabel === undefined && outcomes.length === 0) return { ...p, ended: true, outcome: \"\" };\n const reached = p.lastEvent ?? p.lastLabel ?? (outcomes.length === 1 ? outcomes[0]!.gameId : undefined);\n if (reached === undefined) {\n return { ...p, ended: true, problem: \"The scene ended without saying which outcome it reached.\" };\n }\n if (!outcomes.some((o) => o.gameId === reached)) {\n return { ...p, ended: true, problem: `The scene reached \"${reached}\", which this card doesn't have.` };\n }\n return { ...p, ended: true, outcome: reached };\n }\n}\n\n/** A choice's options with both engines' gates applied: Patter's own condition (`eligible`), and\n * the Storylet Engine's on the outcome each option names. */\nfunction optionsFor(options: readonly ChoiceOption[], outcomes: readonly CardOutcome[]): SceneOption[] {\n return options.map((o) => {\n const named = o.gameData?.[\"outcome\"];\n const outcome = typeof named === \"string\" ? named : undefined;\n const shut = outcome !== undefined && outcomes.find((x) => x.gameId === outcome)?.available === false;\n const why = !o.eligible ? \"not available here\" : shut ? \"requirements not met\" : undefined;\n return {\n id: o.id, text: o.prompt?.text || o.id, enabled: o.eligible && !shut,\n ...(outcome !== undefined ? { outcome } : {}), ...(why !== undefined ? { why } : {}),\n };\n });\n}\n","// ---------------------------------------------------------------------------\n// @storylet-studio/model - the shape source-of-truth.\n//\n// Transcribes design/storylets-schema.md (bundle, save) and\n// design/storylets-source.md (shards). Entity shapes are generic over their\n// expression representation E: source shards use plain `src` strings\n// (Card<string>), the compiled bundle uses { src, ast } envelopes\n// (Card<Expression>). No behaviour lives here.\n// ---------------------------------------------------------------------------\n\nimport type { Expression, ScalarValue } from \"@wildwinter/expr\";\n\nexport type { Expression, ScalarValue, AstNode } from \"@wildwinter/expr\";\n\n// --- shared declarations -----------------------------------------------------\n\nexport type PropertyType = \"boolean\" | \"number\" | \"string\" | \"enum\" | \"flags\" | \"quality\";\n\n/** A property declaration: @world / @story / @box / @deck / tag / hand\n * state. A declared property always has a value (`default` is required);\n * referencing an undeclared property is a publish-time error. */\nexport interface PropertyDecl {\n name: string;\n type: PropertyType;\n default: ScalarValue;\n values?: string[];\n /**\n * A quality's ordered ladder of stage names (design/quality.md). Order IS\n * the meaning: `>=` compares by position here, and `advance()` steps along\n * it. The one order-semantic list in the format, accepted as such: it is a\n * declaration, and inserting a stage mid-ladder is the design's whole point.\n */\n stages?: string[];\n /**\n * `@world` only. `false` makes the property read-only TO THE STORY: a\n * condition may read it, an outcome that writes it is a compile error. The\n * game still moves it through its resolver; this is the story's statement\n * of intent, not the game's policy. Mirrors Patter's `HostScopeDecl.writable`\n * name for name (Reboot.md 10, ruled 2026-09-03). Ignored on every other\n * scope. Absent = writable.\n */\n writable?: boolean;\n /**\n * The sharing axis (design/flows.md, Patter's flag adopted): is this\n * property's value one world value across all flows, or a copy per flow?\n * It does NOT change reference syntax - sharing is set here, on the\n * declaration, not by a different scope token. Absent = the scope\n * default: `@story` shared; box, deck, hand and tag properties per-flow.\n * On a `@world` declaration the flag is a validation error - `@world` is\n * the game's own state, always engine-level, never per-flow.\n */\n shared?: boolean;\n /**\n * The durability axis (design/engine-server.md 4.2), valid wherever `shared`\n * is valid and orthogonal to it: `shared` says whose value this is WITHIN a\n * run, `durable` says whether the value survives the run at all. A durable\n * shared property is the installation's memory (\"trolls defeated since we\n * opened\"); a durable per-flow one is the player's pocket (visits,\n * allegiance, what they earned).\n *\n * INERT TO THE RUNTIME. The engine partitions by `shared` alone and never\n * reads this. Durability is what the SERVER does at a run boundary: it reads\n * the declarations, lifts the durable values out of the partitions before the\n * world restarts, and writes them back into the fresh engine afterwards,\n * entirely through `getProperty` / `setProperty`.\n *\n * On a `@world` declaration the flag is a validation error, for the reason\n * `shared` is: @world is the game's own state, and how long the game keeps it\n * is the game's business.\n */\n durable?: boolean;\n purpose?: string;\n}\n\n/** A template field (box-defined), of the card template or of the outcome\n * fields. Data for the host; the engine never interprets fields and they are\n * not addressable from expressions. */\nexport interface FieldDecl {\n name: string;\n type: PropertyType;\n default: ScalarValue;\n values?: string[];\n purpose?: string;\n}\n\n/** Cooldown policy, in turns (schema 3.4). */\nexport type RedrawPolicy = \"always\" | \"never\" | number;\n\n// --- gameId derivation (Patter's effectiveGameId, adopted 2026-07-20) --------\n//\n// gameId is the renameable host-facing address; it is OPTIONAL in source and\n// derived from the entity's title until the author pins one, so a rename of\n// the title carries the address with it (no \"new-deck\" stuck placeholder).\n// The compiler fills a concrete gameId into every bundle entity.\n\n/** Slugify a human label into a filename- / address-safe gameId. */\nexport function gameIdify(text: string): string {\n return text.toLowerCase().replace(/['’]/g, \"\")\n .replace(/[^a-z0-9-]+/g, \"-\").replace(/-+/g, \"-\").replace(/^-+|-+$/g, \"\");\n}\n\nexport function isValidGameId(gameId: string): boolean {\n return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(gameId);\n}\n\n// --- property names (adopted 2026-08-18, with Patter, from one rule) ---------\n//\n// Design and argument: `@wildwinter/app-shell` src/property-names.ts. Not house\n// style: the rule is what `@wildwinter/expr` can parse. Its lexer takes an\n// identifier as /[a-zA-Z_][a-zA-Z0-9_]*/ and folds it to lower case, so\n// `@story.isNight` reaches a property called `isnight`, `@story.9lives` and\n// `@story.not` are parse errors, and `@story.is-night` is not an error at all: it\n// compiles to `@story.is` MINUS the string \"night\". That last one is why the rule\n// is enforced rather than trusted - it is the only violation that silently means\n// something else.\n//\n// Here rather than behind an import of the UI kit because the compiler, the CLI\n// and the embedded runtime resolve state by these. `property-name-parity.test.ts`\n// holds them to the shell's, and `property-name-grammar.test.ts` holds them to the\n// parser they came from.\n\n/** The words `@wildwinter/expr` lexes as keywords, so no property may be called one. */\n// A legal property NAME is a fact about the expression language, not about this\n// model: `not` is reserved because the tokeniser reads it as an operator. Both\n// families kept their own copy of the rule AND of the keyword list, a list\n// neither owned. @wildwinter/expr derives the list from its own tokeniser, so a\n// keyword added there cannot leave a stale copy here.\n//\n// Re-exported so nothing that imports them has to move.\nexport {\n propertyNameify, isValidPropertyName, isCaseOnlyPropertyName, RESERVED_PROPERTY_NAMES,\n} from \"@wildwinter/expr\";\n/** The effective address: a pinned gameId, else derived from the title, else\n * the immutable id (so there is always something addressable). */\nexport function effectiveGameId(entity: { gameId?: string; title?: string; id: string }): string {\n const pinned = entity.gameId?.trim();\n if (pinned) return pinned;\n const fromTitle = entity.title ? gameIdify(entity.title) : \"\";\n return fromTitle || entity.id;\n}\n\n// --- the value scope's owner segment (design/engine-server.md 4.4) -----------\n//\n// Every other owned scope names its owner with a gameId that is unique across\n// the bundle: box, deck, hand and card. A TAG's gameId is unique only within\n// its group, and a group's only within its box, so two boxes may each name a\n// tag \"docks\" - as ordinary as two boxes each having a \"zone\" group - and\n// `value.docks.danger` then names two stores.\n//\n// So the value scope's owner segment is box-qualified where it has to be:\n// `value.<boxGameId>/<tagGameId>.<name>`. The slash sits INSIDE the owner\n// segment, so the address still splits into three on the dot and no parser\n// changes shape. The qualified form is always accepted; the short form is\n// accepted while exactly one tag in the bundle carries that gameId, and\n// refused when more do, naming the qualified candidates. What a runtime\n// PRINTS - `listProperties`, a write on the trace, a load report, an\n// examiner - is the short form except where the gameId repeats.\n//\n// One definition, because the Board draws these addresses from the bundle\n// while the engine builds them from its own index, and an address the editor\n// shows that the engine will not take is the fault 4.4 was fixing.\n\n/** The three answers a value address needs, all derived from the bundle. */\nexport interface ValueAddresses {\n /** Tag internal id -> the owner segment an address PRINTS for it. */\n print: Map<string, string>;\n /** Every owner segment a value address ACCEPTS -> the tag's internal id.\n * Holds the qualified form for every tag and the short form only for a\n * gameId no other tag shares. */\n accept: Map<string, string>;\n /** A tag gameId more than one box uses -> its qualified forms, in bundle\n * order. Empty for the overwhelming majority of projects, and what a\n * refusal lists. */\n repeated: Map<string, string[]>;\n}\n\n/** The owner segment of every tag in the bundle, both ways round. */\nexport function valueAddresses(bundle: {\n boxes: readonly { id: string; gameId?: string; title?: string; tagGroups: readonly TagGroup[] }[];\n}): ValueAddresses {\n const tags: { id: string; gameId: string; qualified: string }[] = [];\n for (const box of bundle.boxes) {\n const boxGameId = effectiveGameId(box);\n for (const group of box.tagGroups) {\n for (const tag of group.tags) {\n const gameId = effectiveGameId(tag);\n tags.push({ id: tag.id, gameId, qualified: `${boxGameId}/${gameId}` });\n }\n }\n }\n // Distinct qualified forms per gameId. Distinct rather than a count: two\n // groups in ONE box may also name a tag the same way, and a refusal that\n // offered the same address twice would be no help at all. Those two share\n // the qualified segment, and the first in bundle order answers to it, which\n // is what the short form did for everything before this rule.\n //\n // That last case is closing at the source rather than here (question 16,\n // ruled 2026-09-06): the compiler WARNS that a tag gameId must be unique\n // within its box, across all of that box's groups, and refuses it from the\n // next release. So this stays as the reading rule for a bundle built before\n // that, and a bundle built after it has no repeated qualified form to read.\n const forms = new Map<string, string[]>();\n for (const tag of tags) {\n const list = forms.get(tag.gameId) ?? [];\n if (!list.includes(tag.qualified)) list.push(tag.qualified);\n forms.set(tag.gameId, list);\n }\n const print = new Map<string, string>();\n const accept = new Map<string, string>();\n const repeated = new Map<string, string[]>();\n for (const tag of tags) {\n const candidates = forms.get(tag.gameId) ?? [tag.qualified];\n const ambiguous = candidates.length > 1;\n print.set(tag.id, ambiguous ? tag.qualified : tag.gameId);\n if (!accept.has(tag.qualified)) accept.set(tag.qualified, tag.id);\n if (!ambiguous && !accept.has(tag.gameId)) accept.set(tag.gameId, tag.id);\n if (ambiguous) repeated.set(tag.gameId, candidates);\n }\n return { print, accept, repeated };\n}\n\n/** What an ambiguous short-form value address is told: the candidates, in\n * full, because \"that names two tags\" without them leaves a host reading a\n * bundle it did not write to find out which boxes. */\nexport function ambiguousValueAddressMessage(\n segment: string, name: string, candidates: readonly string[],\n): string {\n const forms = candidates.map((q) => `\"value.${q}.${name}\"`);\n const list = forms.length <= 1 ? (forms[0] ?? \"\")\n : `${forms.slice(0, -1).join(\", \")} or ${forms[forms.length - 1]}`;\n return `\"value.${segment}.${name}\" names a tag in ${candidates.length} boxes; write ${list}`;\n}\n\n/**\n * The first free gameId of the form `base`, `base-2`, `base-3`, ... not already\n * in `taken`.\n *\n * A gameId is API - `deal()` and the play log speak it - so a name minted for a\n * new or duplicated entity must not collide with an existing one. This lived in\n * two copies, one in the editor and one in the CLI's kit scaffolder, character\n * for character the same; two copies of an addressing rule can drift, and a\n * drift here means the same act produces different addresses depending on which\n * program did it. It is here, beside `gameIdify`, because both programs need it\n * and no UI touches it.\n */\nexport function freeGameId(base: string, taken: ReadonlySet<string>): string {\n let gameId = base;\n for (let n = 2; taken.has(gameId); n++) gameId = `${base}-${n}`;\n return gameId;\n}\n\n/**\n * The first free TITLE of the form `base`, `base 2`, `base 3`, ... whose\n * derived gameId is not already in `taken`.\n *\n * The sibling of `freeGameId` for the \"New box\", \"New deck\" case, where the\n * author is given a title and the address follows from it. The dedupe is on the\n * DERIVED gameId rather than the title, because two titles that slug to one\n * address are the collision that matters.\n */\n/**\n * An id-sorted collection in the order a person should SEE it.\n *\n * Storage is sorted by immutable id (source rule 5) so that two authors adding\n * one item each never touch the same line. That makes array position useless as\n * display order, so the order the author arranged rides in a sparse `order`\n * field, with position as the fallback and id to break a tie.\n *\n * One definition, because this rule has to give the same answer in four places\n * that a reader compares side by side: the compiler (what the bundle carries),\n * the editor (what the card document lists), the exports, and Find. When they\n * disagree, the editor shows one order and the game plays another.\n */\nexport function byDisplayOrder<T extends { id?: string; order?: number }>(items: readonly T[]): T[] {\n return items\n .map((x, i) => ({ x, key: x.order ?? i, i }))\n .sort((a, b) => a.key - b.key || ((a.x.id ?? \"\") < (b.x.id ?? \"\") ? -1 : (a.x.id ?? \"\") > (b.x.id ?? \"\") ? 1 : a.i - b.i))\n .map((e) => e.x);\n}\n\nexport function freeTitle(base: string, taken: ReadonlySet<string>): string {\n let title = base;\n for (let n = 2; taken.has(gameIdify(title)); n++) title = `${base} ${n}`;\n return title;\n}\n\n/**\n * A count of a TIMED box's turns, said as time (design/engine-server.md 4.8):\n * `turnSpan(30, 60)` is \"30 min\", and `turnSpan(30, 60, true)` is \"30 minutes\".\n *\n * One definition, because the conversion appears wherever a designer might\n * otherwise have to do it in their head: the card editor's Redraw field, the\n * box page, the Board's advance buttons, and the coverage report's turn\n * budget. Two of those want the unit spelled out and two want it short, which\n * is the whole of `long`.\n */\nexport function turnSpan(turns: number, seconds: number, long = false): string {\n const total = Math.max(0, Math.round(turns * seconds));\n const say = (n: number, short: string, one: string, many: string): string =>\n long ? `${n} ${n === 1 ? one : many}` : `${n} ${short}`;\n if (total < 60) return long ? say(total, \"s\", \"second\", \"seconds\") : `${total}s`;\n if (total < 3600) {\n const minutes = total % 60 === 0 ? total / 60 : Math.round(total / 6) / 10;\n return say(minutes, \"min\", \"minute\", \"minutes\");\n }\n let hours = Math.floor(total / 3600);\n let rest = Math.round((total % 3600) / 60);\n if (rest === 60) { hours += 1; rest = 0; }\n const said = say(hours, \"hr\", \"hour\", \"hours\");\n return rest === 0 ? said : `${said} ${say(rest, \"min\", \"minute\", \"minutes\")}`;\n}\n\n// --- entities (generic over expression representation E) ---------------------\n\nexport interface Outcome<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Authored display order (sparse; one without it falls back to its id\n * position). Unlike `Card.order` this one IS compiled into the bundle:\n * which option is offered first is authorial, and a host reading a dealt\n * card's outcomes is building the player's menu. */\n order?: number;\n /** Gating; availability is always evaluated against current state. */\n condition?: E;\n /** Target (\"@scope.name\") -> expression; all right-hand sides evaluate\n * against pre-play state (schema 3.7). */\n changes: Record<string, E>;\n /** Template data, as `Card.fields` is: field name -> value, declared by\n * the box's `outcomeFields`, validated at publish, and handed to the host\n * with the outcome. The engine never reads it: a press can say one line\n * (\"The notice is in your pocket\") without spending a card on it. */\n fields?: Record<string, ScalarValue>;\n}\n\nexport interface Card<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Authored display order within the deck (sparse; a card without one falls\n * back to its id position). Merges as a per-card value, so id-sorted storage\n * stays merge-clean (Reboot 7.4); dropped from the compiled bundle. */\n order?: number;\n condition?: E;\n /** Default 0; an expression must evaluate to a number. */\n priority: number | E;\n redraw: RedrawPolicy;\n /** Tags: tag group id -> tag ids. An absent group is a wildcard (matches\n * any binding of it), except the reserved home group, whose default\n * inverts (schema 2.4). Editors and fixtures speak gameIds; stored\n * references are ids. */\n tags?: Record<string, string[]>;\n /** How many hands may hold this card at once (schema 3.5): integer >= 1,\n * default 1. One copy is the exclusivity rule; copies: N is the\n * deliberate opt-out for interchangeable filler. Always counted WITHIN a\n * flow, whether or not the card is shared. */\n copies?: number;\n /** Scarcity across flows (design/shared-scarcity.md). Absent takes the\n * deck's flag; set here it overrides the deck, so a single unique card can\n * stay in the content it belongs to. A shared card's claims count every\n * flow's board, and a shared `redraw: \"never\"` is spent for everyone the\n * first time anyone plays it.\n *\n * A finite `redraw` stays PER FLOW even when shared: a cooldown is an\n * absolute turn of the card's box clock and clocks are per flow, so\n * \"3 turns of whose clock?\" has no answer. A world-wide timer is a @world\n * question, not an engine one (shared-scarcity 9.3.3). */\n shared?: boolean;\n /** The world cap: how many hands ACROSS EVERY FLOW may hold this at once.\n * Read only when the card is shared, and defaults to `copies`, so the\n * common case writes one number and \"five in the world, one to a customer\"\n * is `copies: 1, sharedCopies: 5`. */\n sharedCopies?: number;\n /** Does this card's `redraw: \"never\"` spend survive the run\n * (design/engine-server.md 4.2)? Absent takes the deck's flag, set here it\n * overrides the deck, exactly as `shared` does. `shared` decides who a\n * spend counts for WITHIN a run; this decides whether it outlives one.\n *\n * Only `\"never\"` crosses the run boundary, for the reason only `\"never\"`\n * crosses the flow boundary (shared-scarcity 9.3.2): a finite cooldown is\n * an absolute turn of a box clock, and the clock resets with the run. On\n * any other redraw the flag is a compile warning.\n *\n * INERT TO THE RUNTIME, like the declaration flag: the server lifts the\n * durable spends at run end (per-flow ones from the flow's `never`\n * cooldowns, shared ones from the engine's spent set) and puts them back\n * through `openFlow(id, { restore })` and `markTaken`. */\n durable?: boolean;\n /** Card-template data: field name -> value, validated at publish. */\n fields?: Record<string, ScalarValue>;\n outcomes: Outcome<E>[];\n}\n\nexport interface Deck<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** The deck gate, evaluated once per draw in the draw's environment. */\n condition?: E;\n /** This pile is scarce across flows (design/shared-scarcity.md): every card\n * in it is shared unless the card says otherwise. The container is where\n * Patter puts its own shared-memory flag, and the deck is our container. */\n shared?: boolean;\n /** Every `redraw: \"never\"` card in this pile is spent for good, past the end\n * of the run, unless the card says otherwise (design/engine-server.md 4.2).\n * The container carries the flag for the reason `shared` is carried here:\n * a pile is what an author reaches for when a rule is true of all of it. */\n durable?: boolean;\n properties: PropertyDecl[];\n cards: Card<E>[];\n}\n\nexport interface Tag {\n id: string;\n gameId?: string;\n /**\n * This tag's own starting values for properties its GROUP declares\n * (design/hand-typing.md). The group says what the property IS; a tag says\n * only where it starts, so \"every zone has a haunting level\" is written once\n * and \"the cave starts at 2\" is written where it belongs.\n *\n * A name here that the group does not declare is an error: it would be a\n * value for nothing.\n */\n values?: Record<string, ScalarValue>;\n /** Authored display order (sparse; one without it falls back to its id\n * position). Merges as a per-item value, so id-sorted storage stays\n * merge-clean (Reboot 7.4). */\n order?: number;\n properties?: PropertyDecl[];\n /** Template-of-play extras (e.g. spatial geometry). Source only: preserved\n * in shards, never compiled into the bundle. */\n templates?: Record<string, unknown>;\n}\n\n/** A named axis for cross-cutting cards (schema 2.4, renamed from\n * Dimension). Tags are declared, not freeform. */\nexport interface TagGroup {\n id: string;\n gameId?: string;\n purpose?: string;\n /**\n * A property reference (`\"@story.act\"`) whose value names a tag in this group\n * by gameId. The engine reads it at every ask and binds the group, exactly as\n * if the asking hand had chosen that tag; a hand's own binding wins.\n *\n * For an axis driven by STATE rather than by place: acts, chapters, a\n * difficulty band. Without it, only a hand can bind a group, so such an axis\n * had nowhere to gate and every card needed its own condition.\n *\n * A reference rather than an expression on purpose (design/where-and-\n * selectors.md Part B): a computed binding belongs in a property the outcomes\n * maintain, and an expression here would make this type generic for no gain.\n */\n boundBy?: string;\n /**\n * What omitting this group means for a card. Default false: omission is a\n * wildcard, so the card matches whatever the group is bound to. True inverts\n * it, so a card that names no tag here is unavailable wherever the group IS\n * bound (and unaffected where it is not).\n *\n * `place` is the built-in instance of this pair: bound to the asking hand,\n * and inverted per card rather than per group.\n */\n required?: boolean;\n /** Authored display order (sparse; one without it falls back to its id\n * position). Merges as a per-item value, so id-sorted storage stays\n * merge-clean (Reboot 7.4). */\n order?: number;\n /**\n * Properties EVERY tag in this group has (design/hand-typing.md). The\n * declaration lives here and each tag carries only its own starting value in\n * `Tag.values`, which is the separation the format was missing: a tag's own\n * `properties` entry has to restate the type on every tag purely in order to\n * say the value, and a tag added later silently arrives without it.\n *\n * Compiled by FLATTENING onto each tag, so the bundle keeps its per-tag\n * shape and no runtime, port or bundle schema changes: source is where the\n * author works and where merges happen, the bundle is a compiled artefact\n * that can afford to be explicit.\n *\n * A tag may still declare its own `properties` for a group whose tags\n * genuinely differ. Declaring the same NAME both ways is an error.\n */\n properties?: PropertyDecl[];\n tags: Tag[];\n /** Template-of-play extras for the GROUP, the same bag its tags carry: this is\n * where a group is marked spatial and where that template keeps its own\n * group-level configuration. Source only, preserved but never compiled.\n *\n * A bag rather than a `spatial: true` flag because the marker and the\n * configuration are one thing (see model/spatial.ts), and because core is not\n * meant to grow a field per template of play. */\n templates?: Record<string, unknown>;\n}\n\n/** The reserved tag group (schema 2.4): present in every box without\n * declaration, its tags the box's hand ids. Every hand implicitly binds it to\n * itself; a card that names a place is available only at that place.\n *\n * Called `place` rather than `home` since 2026-08-21: one word for one thing\n * across the format, the editor and the exports. \"Where\" is the QUESTION a\n * card answers (at a place, or anywhere in a region); \"place\" is the direct\n * half of that answer. `home` was a metaphor an author had to learn, and it\n * leaked into hand-edited shards and the docs. */\nexport const PLACE_GROUP = \"place\";\n\n/** The scopes a movable hole may be filled from (design/engine-server.md 4.6):\n * the two `boundBy` already allows, plus `@hand` - the asking hand's OWN\n * declared property, resolved before tag composition so a movable hole can\n * never depend on the tags it is choosing. */\nexport type HoleRefScope = \"hand\" | \"story\" | \"world\";\n\n/** A parsed hole reference: `@hand.zone` -> `{ scope: \"hand\", name: \"zone\" }`. */\nexport interface HoleRef {\n scope: HoleRefScope;\n name: string;\n}\n\nconst HOLE_REF = /^@(hand|world|story)\\.([a-z][a-z0-9_-]*)$/;\n\n/**\n * Is this `chosen` / binding value MEANT as a property reference rather than a\n * tag id?\n *\n * The test is the leading `@` alone, deliberately: a value that starts with\n * one and does not parse is a mistyped reference, which the compiler should\n * name as such, not a tag id that happens to look odd. Tag ids never begin\n * with `@`.\n */\nexport const isHoleRef = (value: string): boolean => value.startsWith(\"@\");\n\n/** Parse a hole reference, or undefined when it is not one. The on-disk form\n * stays a plain string, so the canonical serialiser and the shard merge need\n * no change at all: a hole is still one group name against one value. */\nexport const parseHoleRef = (value: string): HoleRef | undefined => {\n const m = HOLE_REF.exec(value);\n return m === null ? undefined : { scope: m[1] as HoleRefScope, name: m[2]! };\n};\n\n/** A declared kind of hand (schema 2.6): live-inherited, author-side only,\n * never called from game code. One condition governs every instance. */\nexport interface HandTemplate<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Authored display order (sparse; one without it falls back to its id\n * position). Merges as a per-item value, so id-sorted storage stays\n * merge-clean (Reboot 7.4). */\n order?: number;\n\n /** Fixed tag bindings: tag group id -> tag id. Literal tags only: what a\n * template FIXES is the same for every instance, and a hole that moves is\n * the instance's own business (`Hand.chosen`, 4.6). */\n bindings?: Record<string, string>;\n /** The holes: tag group ids each instance fills (one tag each, or one\n * property reference: 4.6). */\n chooses?: string[];\n /** Shared availability condition, ANDed in (schema 3.1); evaluated per\n * instance against that instance's composed @hand. */\n condition?: E;\n /** Default slot cap. */\n slots: number | \"unbounded\";\n /** Declared @hand state every instance carries. */\n properties: PropertyDecl[];\n}\n\n/** A standalone hand's inline rule (schema 2.6): owned by the hand. */\nexport interface HandRule<E> {\n /**\n * Tag group id -> tag id, or a PROPERTY REFERENCE (`\"@hand.zone\"`,\n * `\"@story.where\"`, `\"@world.place\"`) the runtime resolves at ask time\n * (design/engine-server.md 4.6, the hand that moves). Still a plain string\n * on disk, so the canonical serialiser and the merge are untouched; what\n * widened is the meaning, and `parseHoleRef` is where it is read.\n *\n * `place` is never fillable this way: it is the hand's own name, not an axis.\n */\n bindings?: Record<string, string>;\n condition?: E;\n slots: number | \"unbounded\";\n}\n\n/** A hand (schema 2.6): a template instance (template + chosen) or a\n * standalone hand (rule). Exactly one of template / rule. Fully concrete:\n * deal is name-only. */\nexport interface Hand<E> {\n id: string;\n /** The name deal() is called with; a rename is a breaking change\n * (Reboot 7.4). */\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Hand template id (not gameId). */\n template?: string;\n /**\n * Template instances: tag group id -> tag id, one per `chooses` hole.\n *\n * A value may instead be a PROPERTY REFERENCE (`\"@hand.zone\"`,\n * `\"@story.where\"`, `\"@world.place\"`), which makes the hole MOVABLE: the\n * runtime resolves the reference at ask time and binds the hole to the tag\n * the value names, so moving the Elder to the forest is `setProperty` and\n * nothing else (design/engine-server.md 4.6). Still a plain string on disk,\n * so the canonical serialiser and the shard merge need no change; read it\n * with `parseHoleRef`.\n */\n chosen?: Record<string, string>;\n /** Standalone hands: the inline rule. */\n rule?: HandRule<E>;\n /** Override; defaults to the template's / rule's slots. The ONLY template\n * field an instance may override (schema 2.6). */\n slots?: number;\n /** Standalone hands' own @hand state (template instances inherit the\n * template's declarations). */\n properties?: PropertyDecl[];\n /** Authored display order within the box (sparse; authoring-only, never\n * compiled into the bundle - the compiler's explicit field list drops it). */\n order?: number;\n /** Template-of-play extras (e.g. a spatial pin). Source only. */\n templates?: Record<string, unknown>;\n}\n\nexport interface Box<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** The only per-box ranking policy (Reboot 2.2). */\n ranking: { specificity: boolean };\n /**\n * A TIMED box: its clock counts real time, one turn every `seconds` of the\n * run (design/engine-server.md 4.8). Absent is the ordinary box, whose turn\n * is a play.\n *\n * Two things follow, and only two. In the ENGINE, a play in this box\n * defaults to advancing nothing: `settings.playAdvancesTurns` does not\n * apply, so a designer cannot declare the convention and then forget to\n * switch play-advance off. Everywhere else it is what the tools SAY: the\n * host ticks the box (the runtime has no clock and gains none here), and a\n * card's `redraw: N` reads as N x `seconds`, which the editors, the bundle\n * inspectors and the coverage report spell out rather than leaving a\n * designer to know that 30 meant minutes.\n *\n * The number itself is inert to the runtime, which never reads it.\n */\n turn?: { seconds: number };\n /** The card template: what every card in this box carries. */\n fields: FieldDecl[];\n /** What every outcome in this box may carry, declared the same way.\n * Absent when the box declares none, so a bundle without them is byte for\n * byte what it was. */\n outcomeFields?: FieldDecl[];\n properties: PropertyDecl[];\n tagGroups: TagGroup[];\n decks: Deck<E>[];\n handTemplates: HandTemplate<E>[];\n hands: Hand<E>[];\n}\n\n// --- the compiled bundle (.storyletsc) ---------------------------------------\n\nexport const BUNDLE_SCHEMA = \"storylets/bundle@0\";\n\n/** Binds bundles to shards (staleness gate) and saves to bundles. */\nexport interface BundleContent {\n project: string;\n version: string;\n /** hash32 over the canonical source shards (schema 2.8). */\n hash: string;\n}\n\nexport interface BundleSettings {\n playAdvancesTurns: number;\n}\n\n/**\n * The play ladder (design/engine-server.md 4.10): how much of itself\n * Storyletter shows this project, in one setting with three rungs rather than\n * a set of toggles, because the features nest.\n *\n * solo one player, one flow: no sharing, no durability, no venue features\n * shared several players over one world: sharing appears\n * venue a production: nothing is hidden\n *\n * EDITOR-SIDE ONLY. It stays in the project shard beside `coverage` and\n * `export` and is never compiled: a solo project plays on the same Engine as a\n * venue one. Hidden is hidden rather than disabled, so going DOWN a rung is\n * refused when the project already contains what the rung would hide, and a\n * hand-edited shard above its rung is a compile warning.\n */\nexport type PlayRung = \"solo\" | \"shared\" | \"venue\";\n\n/** The default rung: a project shard that says nothing is a solo game. */\nexport const DEFAULT_PLAY_RUNG: PlayRung = \"solo\";\n\n/** The project shard's settings block: what the bundle carries, plus the\n * authoring-side play rung that it does not. */\nexport interface ProjectSettings extends BundleSettings {\n /** The play ladder rung (see `PlayRung`). Absent = \"solo\". */\n play?: PlayRung;\n}\n\n/**\n * A map that a bundle was asked to carry: one spatial tag group's geometry,\n * flattened for a host to draw (design/graphical-views.md 2, \"The map MAY ship\").\n *\n * INERT PAYLOAD. Nothing in the engine reads this and nothing ever will: the\n * runtime deals in tag names. It is here so a host that wants an in-game map does\n * not have to invent its own export, and it is absent unless the project asked\n * for it (`export.map`), so a build that does not want a map carries no bytes.\n *\n * GAME IDS throughout, never internal ids. Internal ids are authoring identity\n * and mean nothing outside the project; a host matches these against the same\n * names it passes to `peek`. There is nothing here to strip either, which is why\n * `metadata: \"stripped\"` needs no special case: no titles, no purposes.\n *\n * SITES ARE HERE, which reverses a ruling. Until 2026-09-05 this comment said\n * they were deliberately not: a site was where an author parked a hand while\n * working, held in the view sidecar precisely because it was not content, and a\n * host that wanted to place a hand had its zone from the compiled binding. That\n * held for a game, where a hand's zone is its only real-world meaning. It does\n * not hold for a physical experience (design/engine-server.md 4.3), where the\n * position IS content: it is where the kiosk stands, and a producer's map is\n * simply wrong without it. The alternative was a second file beside the bundle,\n * which would cost a format the inspectors do not read and would put the view\n * sidecar in the shipping path by the back door.\n */\nexport interface BundleMap {\n /** The owning box, by gameId (tag groups are box-scoped). */\n box: string;\n /** The tag group this is a map of, by gameId. */\n group: string;\n /** One entry per zone that has been drawn; a tag with no polygon is not a\n * place yet and is left out rather than shipped as an empty shape. */\n zones: { tag: string; polygon: ViewPoint[] }[];\n /** Background pictures, back to front, as bundle-relative paths. Hidden ones\n * do not ship: what an author put away is not something to spring on a host. */\n backgrounds?: BundleBackground[];\n /** Where the placed hands stand on this map, by hand gameId, sorted by that\n * gameId so the bytes do not depend on authoring order. A hand nobody has\n * placed has no entry, and a map with no placed hand has no key at all. The\n * zone a site sits in is NOT repeated here: the hand's own binding is what\n * the runtime deals from, and a second copy could only go on to disagree. */\n sites?: { hand: string; x: number; y: number }[];\n}\n\n/** One shipped picture. `locked` and `hidden` are authoring state and do not\n * travel; the draw order is the array order. */\nexport interface BundleBackground {\n /** Where the file sits relative to the bundle (\"assets/<box>/<file>\"). */\n file: string;\n x: number;\n y: number;\n width: number;\n height: number;\n opacity?: number;\n}\n\nexport interface Bundle {\n schema: typeof BUNDLE_SCHEMA;\n content: BundleContent;\n metadata: \"full\" | \"stripped\";\n settings: BundleSettings;\n world: {\n properties: PropertyDecl[];\n /** ScopeRegistrySpec (@wildwinter/scoperegistry): the owned/foreign\n * split. Absent = engine-owned @world (standalone play). */\n registry?: unknown;\n };\n story: {\n properties: PropertyDecl[];\n };\n boxes: Box<Expression>[];\n /** Maps, when the project asked for them. Absent is the normal state. */\n maps?: BundleMap[];\n /** Other engines' game-wide scopes the content names (`patter`), sorted: the\n * family's shared vocabulary, let through unchecked by the compiler. The\n * engine reports when the game has not registered one. Absent when none. */\n externalScopes?: string[];\n}\n\n// --- the save envelope --------------------------------------------------------\n//\n// Version 2 (the one-registry model): property values are the game's\n// ScopeRegistry's, not the engine's. The envelope holds what is NOT a property\n// (boards, clocks, cooldowns, PRNGs, play logs, spent cards), plus, when the\n// engine made its own registry (a standalone game), that registry's values\n// under `registry`. A game that passed a registry saves it once itself.\n// Version 1 envelopes still load on every runtime: their property partitions\n// move into the registry under the keys below.\n//\n// Registry keys (identical on every runtime, since they are in the save):\n// `story` for the shared @story; `storylets/<kind>/<id>` for a shared box,\n// deck, hand, or value bag (kind is `box`, `deck`, `hand`, or `value`, id the\n// internal id); `storylets/flow/<flowId>/story` and\n// `storylets/flow/<flowId>/<kind>/<id>` for a flow's own. Ids escape `%` as\n// `%25` and `/` as `%2F`. A bag with no declared properties is not registered.\n// A self-backed @world (no resolver bound) is a stored property too, under\n// `world`.\n\nexport const SAVE_SCHEMA = \"storylets/save@2\";\n/** The version 1 envelope's schema tag, still read. */\nexport const SAVE_SCHEMA_V1 = \"storylets/save@1\";\n\nexport interface PlayRecord {\n /** Card and outcome by gameId (feeds the play-history functions). */\n card: string;\n /** \"\" for a card with no outcomes, played with none: the key is always\n * there, so a save's shape does not depend on the card. */\n outcome: string;\n turn: number;\n}\n\n/** A property bag: name -> value. */\nexport type PropertyBag = Record<string, ScalarValue>;\n\n/** The per-scope property partitions one side of the sharing flag holds:\n * a save carries one of these for the shared values and one per flow\n * (design/flows.md). NO world key, in either: @world is the game's own\n * state, resolved through the world resolver and saved by whoever owns\n * it - \"host saves its container once, each engine saves its own\n * envelope\" (engine-runtimes.md 3.1). */\nexport interface PropsPartition {\n story: PropertyBag;\n box: Record<string, PropertyBag>;\n deck: Record<string, PropertyBag>;\n hand: Record<string, PropertyBag>;\n /** Tag state, keyed by tag id. */\n value: Record<string, PropertyBag>;\n}\n\n/** One flow's snapshot inside the envelope (schema 4), and the blob\n * `saveFlow` parks. */\nexport interface FlowSave {\n /** The per-flow property partitions. Carried by `saveFlow`, which parks one\n * flow whole; absent from a version 2 envelope's flows, whose properties\n * are the registry's. Present in every flow of a version 1 envelope. */\n props?: PropsPartition;\n /** Per-box turn counters, keyed by box id (schema 3.4) - per flow: there\n * is deliberately no global turn. */\n turns: Record<string, number>;\n /** mulberry32 state, uint32 (schema 3.3), per flow. */\n prng: number;\n /** Absolute next-eligible turn (of the card's box's clock) per card id;\n * MAX_SAFE_INTEGER = never (deliberately not Infinity, which\n * JSON-serialises to null). */\n cooldowns: Record<string, number>;\n /** Hand contents (card ids, in dealt order), keyed by hand id. The claims\n * ledger is derived from this (schema 3.5). */\n board: Record<string, string[]>;\n playLog: PlayRecord[];\n}\n\n/** The whole engine, one envelope: the shared partitions once, then every\n * live flow keyed by its id - Patter's shape (one shared blob + N flow\n * blobs; multi-flow and save/load are the same feature). */\n/** The engine's half of a save: what every flow shares. Properties, and the\n * cards a shared `redraw: \"never\"` has taken out of the world for good\n * (design/shared-scarcity.md). Claims are NOT here: they are derived from the\n * live boards, and each flow's board rides its own blob. */\nexport interface SharedSave {\n /** The shared property partitions: version 1 only. */\n props?: PropsPartition;\n /** Card ids, sorted, so a save is byte-stable for a diff. */\n spent: string[];\n}\n\nexport interface SaveEnvelope {\n schema: typeof SAVE_SCHEMA;\n content: BundleContent;\n /** The engine's own registry's values, keyed by registry key: present only\n * when the engine made the registry itself (a standalone game). A game that\n * passed a registry saves it once, beside this envelope. */\n registry?: Record<string, PropertyBag>;\n shared: SharedSave;\n flows: Record<string, FlowSave>;\n}\n\n/** A version 1 envelope, from before the registry held the properties. Every\n * runtime still reads it: its partitions move into the registry as it loads. */\nexport interface SaveEnvelopeV1 {\n schema: typeof SAVE_SCHEMA_V1;\n content: BundleContent;\n shared: SharedSave & { props: PropsPartition };\n flows: Record<string, FlowSave & { props: PropsPartition }>;\n}\n\n// --- the load report (design/engine-server.md 4.9) ----------------------------\n//\n// `loadGame` is forgiving by design: a card the bundle no longer has drops off\n// the board, a property the save does not carry keeps its default, and a\n// version two builds newer loads without a word. That forgiveness is what makes\n// a save survive an edit, and it is also what hides the cost of a content\n// update from whoever is about to apply one. The report is the same walk,\n// itemised: `previewLoad` computes it and changes nothing, `loadGame` computes\n// it and applies it, and `previewFlowRestore` answers the same questions for\n// one flow (4.1's `openFlow(id, { restore })`).\n//\n// Card, hand and flow identities are GAME IDS: a report is host-facing and\n// internal ids mean nothing outside the project. The one exception is an\n// entity the edit DELETED - a vanished card, a vanished hand - which has no\n// gameId left to give, so the report carries the id the save itself carries.\n// There is nothing else to name it by.\n//\n// A property is named differently, and deliberately: by its ENGINE ADDRESS,\n// the string listProperties() prints and getProperty()/setProperty() accept.\n// A report entry is then something a host can act on rather than merely\n// print, and the runtimes have one property grammar instead of two.\n\n/** One card that a restore refused to put back on the board.\n *\n * `vanished` and `hand-vanished` are the edit's doing (the card, or the hand\n * it sat in, is no longer in the bundle). `claimed-elsewhere` is only ever a\n * single-flow restore into a LIVE engine: the card is shared, and the other\n * open flows already hold every copy the world has. */\nexport interface LoadEviction {\n flow: string;\n hand: string;\n card: string;\n reason: \"vanished\" | \"hand-vanished\" | \"claimed-elsewhere\";\n}\n\n/** One property the restore could not put back as it was. `flow` names the\n * flow whose half it belongs to; absent, it is the shared half.\n *\n * `path` is the engine's property address, spelled exactly as\n * `Flow.listProperties()` / `Engine.listProperties()` print it and exactly as\n * `getProperty` and `setProperty` accept it: `story.<name>` for the story\n * scope, `<scope>.<ownerGameId>.<name>` for the box, deck, hand and tag\n * scopes. No `@`, which belongs to the expression language and not to an\n * address.\n *\n * The owner segment is its GAMEID (design/engine-server.md 4.4), the name it\n * is called by everywhere else, so an operator reading a hot-swap report can\n * paste the address straight into `setProperty`. An owner the build no longer\n * has keeps the id the save carried: there is no gameId left to give it,\n * which is the rule the eviction list above has always used. */\nexport interface LoadProperty {\n flow?: string;\n path: string;\n}\n\n/** What a load or a flow restore would do that is not a plain restore\n * (design/engine-server.md 4.9). Arrays are sorted, so two runtimes given the\n * same save and bundle produce the same bytes; `flows` alone keeps the\n * envelope's own order, because a caller re-takes its handles in it. */\nexport interface LoadReport {\n /** No drift and nothing dropped, defaulted or retyped: the save goes back\n * exactly as it was. `flows` is not a divergence and does not count. */\n exact: boolean;\n project: string;\n /** Drift when the two differ; reported, never refused. */\n version: { saved: string; bundle: string };\n /** Drift when the two differ; reported, never refused. */\n hash: { saved: string; bundle: string };\n /** The flows this restores, in the order it restores them. */\n flows: string[];\n evicted: LoadEviction[];\n /** Cooldowns held for cards the bundle no longer has. */\n droppedCooldowns: { flow: string; card: string }[];\n /** Shared `redraw: \"never\"` entries for cards the bundle no longer has. */\n droppedSpent: string[];\n /** In the save, not declared any more. */\n droppedProperties: LoadProperty[];\n /** Declared, not in the save: it takes the declaration's default. */\n defaultedProperties: LoadProperty[];\n /** In the save, still declared, but the saved value no longer fits the\n * declaration (its type changed, or an enum value / quality stage was\n * edited away). It takes the declaration's default. */\n retypedProperties: LoadProperty[];\n}\n\n/** The .storyletsave FILE: the HOST's file, not the engine's - the engine's\n * envelope plus, when the host keeps one, its @world container. This is\n * \"host saves its container once, each engine saves its own envelope\"\n * folded into one file for the single-host case; the ENGINE never reads or\n * writes `world` (loadGame takes the envelope alone). */\nexport const SAVEFILE_SCHEMA = \"storylets/savefile@1\";\n\nexport interface SaveFile {\n schema: typeof SAVEFILE_SCHEMA;\n /** The engine's envelope: version 2 when written today, version 1 still read. */\n engine: SaveEnvelope | SaveEnvelopeV1;\n /** The host's @world values, saved and restored by the host. */\n world?: PropertyBag;\n}\n\n// --- source shards (design/storylets-source.md) --------------------------------\n\n/** The project folder: a macOS package, a plain folder elsewhere. */\nexport const PROJECT_FOLDER_EXTENSION = \".storylets\";\n/** The compiled bundle (strict JSON; generated, never hand-edited). */\nexport const BUNDLE_EXTENSION = \".storyletsc\";\n\n/**\n * Where a shipped background sits, relative to the bundle file.\n *\n * One function so the compiler (which writes the name into the bundle) and the\n * export op (which writes the bytes) cannot drift apart: a path agreed in two\n * places is a path that eventually disagrees. Per BOX, because two boxes may\n * each have their own `plan.png` and a build must not silently keep one of them.\n */\nexport const bundleAssetPath = (boxGameId: string, file: string): string =>\n `assets/${boxGameId}/${file}`;\n/** Per-type shard extensions, JSON5 inside (source doc section 2). */\nexport const SHARD_EXTENSIONS = {\n project: \".storyletproj\",\n box: \".storyletbox\",\n tags: \".storylettags\",\n hands: \".storylethands\",\n deck: \".storyletdeck\",\n /** The AUTHOR's arrangement layer: the canvases, where cards sit on a deck's\n * node canvas and the furniture drawn round them. Its own shard because\n * positions churn (an afternoon of tidying a canvas touches every card) and\n * content does not, so a designer arranging and a writer editing never\n * collide on one file (design/graphical-views.md section 1.2). */\n view: \".storyletview\",\n /** The DESIGNER's map: where a box's hands stand in space, and the furniture\n * round them. One per box, beside the view shard.\n *\n * Split out of the view shard on 2026-09-06 (design/engine-server.md 9.1\n * point 5) because the two halves stopped having one owner. A hand's\n * position ships in the bundle's `maps` block (4.3) and is where a venue's\n * kiosk stands, so it is SHAPE, which a server's author key may not change;\n * the canvases are the author's own working drawing and never leave the\n * project folder. One file could not be both. */\n map: \".storyletmap\",\n /** Threaded comments: content-ADJACENT, so neither in a content shard (a\n * writer's deck edit must not conflict with a reviewer's comment) nor in the\n * arrangement sidecar (this is not where anything sits). One per box,\n * id-keyed (design/annotation.md). Documentation NOTES used to share this\n * file and were retired: `purpose` already says why a thing exists, and\n * Patterpad's typed routing has no destination here. */\n notes: \".storyletnotes\",\n /** An installation contract: what a VENUE depends on, one file per\n * installation in `contracts/` at the project root\n * (design/engine-server.md 4.11). Its own shard, and its own folder, for the\n * walkthrough's reason (Reboot 7.5, S4): a different owner, a different\n * change rate, and a merge that must never collide with the author's edits,\n * since the server always wins its own file. */\n contract: \".storyletcontract\",\n} as const;\n\n/** Where the installation contracts live, relative to the project root. The\n * directory is the registry, as it is for a box's decks: a contract exists\n * because its file exists. */\nexport const CONTRACTS_DIR = \"contracts\";\n\nexport const PROJECT_SCHEMA = \"storylets/project@0\";\nexport const BOX_SCHEMA = \"storylets/box@0\";\nexport const TAGS_SCHEMA = \"storylets/tags@0\";\nexport const HANDS_SCHEMA = \"storylets/hands@0\";\nexport const DECK_SCHEMA = \"storylets/deck@0\";\nexport const VIEW_SCHEMA = \"storylets/view@0\";\nexport const MAP_SCHEMA = \"storylets/map@0\";\n/** The comment sidecar's schema. Still called \"notes\" on disk: the file already\n * held both, and renaming it would break every project for no gain. */\nexport const NOTES_SCHEMA = \"storylets/notes@0\";\nexport const CONTRACT_SCHEMA = \"storylets/contract@0\";\n\n/**\n * What one installation depends on, written by the venue's server and read by\n * `validate` (design/engine-server.md 4.11).\n *\n * NOT THE AUTHOR'S FILE. A venue is provisioned against names - the hands its\n * stations deal, the boxes its scheduler ticks, the properties its clocks drive,\n * the fields its crew read - and the server writes them out so the tools that\n * already gate a build can refuse a rename before it reaches the venue. A\n * project playing at two venues has two of these. The author never edits one,\n * and today, with no server built, a project either receives one or has none.\n *\n * NEVER COMPILED. It is project-side config like `coverage` and `export`: the\n * server does not need its own contract handed back, it needs the bundle to\n * still honour it.\n *\n * BY GAMEID throughout, because a gameId is the name that crosses the project's\n * border and an internal id is authoring identity.\n */\nexport interface ContractShard {\n schema: typeof CONTRACT_SCHEMA;\n /** The installation this contract speaks for. One file per installation, and\n * two files naming the same one is an error. */\n installation: string;\n /** Who wrote it, for a human reading the file (\"Storylet Server 0.1.0\"). */\n by?: string;\n /** The server's revision when it wrote this. */\n revision?: number;\n /** Hands a station is bound to, by gameId: they may not be renamed or\n * removed. */\n hands?: string[];\n /** Timed boxes the venue's scheduler ticks, by box gameId, with the turn unit\n * in SECONDS it was provisioned against. A box whose unit changed means every\n * rest on its cards changed meaning. */\n boxes?: Record<string, { turn: number }>;\n /** Property paths the venue reads or drives, in the engine's own address\n * grammar with no `@` (\"world.time_wall\", \"story.visits\"), which is how\n * `listProperties()` prints them. */\n properties?: ContractProperty[];\n /** Card-template field names the crew and the bridges read. */\n fields?: string[];\n /** Outcome field names they read, the same way: the after-line a station\n * shows when a press lands. What `fields` is to the card template, this is\n * to the box's `outcomeFields`. */\n outcomeFields?: string[];\n}\n\n/**\n * One contracted property.\n *\n * A bare path is the common form and the one the spec's example writes. The\n * object form adds the TYPE the venue was provisioned against, which is the only\n * way `validate` can catch the break that costs a producer most: a property that\n * still exists under the same name and now holds something else. A server that\n * knows the type should write the object form; a hand-written contract may say\n * only the path and get the existence check alone.\n */\nexport type ContractProperty = string | { path: string; type?: PropertyType };\n\n/** The path a contracted property names, whichever form it was written in. */\nexport const contractPropertyPath = (p: ContractProperty): string =>\n typeof p === \"string\" ? p : p.path;\n\n/** The type a contracted property was provisioned against, when it says. */\nexport const contractPropertyType = (p: ContractProperty): PropertyType | undefined =>\n typeof p === \"string\" ? undefined : p.type;\n\n/** A point in a canvas's own coordinates. */\nexport interface ViewPoint {\n x: number;\n y: number;\n}\n\n/**\n * Canvas furniture: what an author draws AROUND the content to make sense of it\n * (design/graphical-views.md 3, \"Frames and sites\").\n *\n * Both canvases carry the same thing, which is why they share a type: a node\n * canvas and a map are different views of different material, but \"put a box\n * round this lot and call it act two\" is the same thought on either.\n *\n * It lives in the view sidecar because it is ARRANGEMENT. Nothing here is\n * content: no runtime reads it, no bundle carries it, and deleting the sidecar\n * loses only the drawing. Threaded comments are the\n * other thing entirely - they attach to entities and they travel - but a canvas\n * DRAWS their markers, while owning none of them.\n */\nexport interface CanvasFurniture {\n /** Titled areas behind the content, back to front (see `stacked`).\n *\n * There was a second kind, a `stickies` list, retired on 2026-08-10\n * (design/annotation.md): a dropped comment marker does the same job in a\n * fraction of the space, and an annotation that takes as much room as the\n * thing it is about is a bad trade on a canvas. */\n frames?: Frame[];\n}\n\n/**\n * A titled area behind a group of things: Unreal's comment box.\n *\n * Deliberately dumb about what is inside it. It has no membership list and\n * computes none: a frame is a thing an author DREW, and the cards under it are\n * whatever happens to be under it now. That is what keeps it honest when content\n * moves, and it is the same reasoning that keeps a zone's sites out of the map's\n * sidecar.\n */\nexport interface Frame extends ViewPoint {\n id: string;\n w: number;\n h: number;\n /** Shown in the frame's bar, and the handle it is dragged by. */\n title?: string;\n /** One of the furniture palette's names (see `FURNITURE_COLOURS`); the theme\n * decides what that looks like, so a frame does not carry a hex value that\n * would fight the palette on the day somebody switches theme. */\n colour?: string;\n /** Place in the frame band (sparse, `stacked`). Frames can nest. */\n z?: number;\n}\n\n/** The furniture palette: names, not colours. The theme maps them, so the same\n * shard reads correctly on linen and on baize. */\nexport const FURNITURE_COLOURS = [\"paper\", \"amber\", \"sage\", \"sky\", \"rose\", \"slate\"] as const;\nexport type FurnitureColour = typeof FURNITURE_COLOURS[number];\n\n/** One deck's node canvas: where its cards sit, and the furniture around them.\n * Sparse throughout. A card with no entry lays out by default, and an entry for\n * a card that no longer exists is inert, so there is no referential integrity to\n * maintain against content that moves underneath. */\nexport interface DeckCanvas extends CanvasFurniture {\n /** Keyed by CARD id. */\n cards?: Record<string, ViewPoint>;\n}\n\n/** The box's map: where its hands sit in space, and the furniture around them.\n * Carried by the MAP shard since 2026-09-06; `ViewShard.map` is the old\n * address, read for one release and never written. */\nexport interface BoxMap extends CanvasFurniture {\n /** Keyed by HAND id. WHERE a site is, and nothing else.\n *\n * Which zone it is IN is not recorded here, and deliberately (2026-08-06,\n * with the rebinding drag): a hand that binds a zone already says so in its\n * own shard, as `chosen` or as a rule binding, and that is the truth the\n * runtime deals from. A copy here could only ever go on to disagree with it,\n * and a site whose recorded zone contradicts the hand it stands for would be\n * the most misleading thing on the map.\n *\n * Called `pins` until 2026-08-10 (design/annotation.md). No compatibility\n * branch: the only projects that exist are the examples in this repo, and they\n * were edited. */\n sites?: Record<string, ViewPoint>;\n}\n\n/** The AUTHOR's arrangement layer for one box: where cards sit on their decks'\n * canvases, and the furniture drawn round them.\n *\n * Its own shard on purpose (design/graphical-views.md section 1.2). Positions\n * churn, content does not: an afternoon of tidying a canvas touches every card,\n * and if that lived in the deck shard then a designer arranging and a writer\n * editing card text would collide on one file all day, while a content review\n * would be full of coordinates. Keyed by id throughout so the existing merge\n * engine handles two designers rearranging different things without a conflict.\n *\n * Source-only. It never reaches the compiled bundle, exactly as `order` does\n * not: the compiler reads the fields it names and this is not among them. */\nexport interface ViewShard {\n schema: typeof VIEW_SCHEMA;\n /** Keyed by DECK id: one node canvas each. */\n canvases?: Record<string, DeckCanvas>;\n /** @deprecated The box map's old address, kept for one release and READ ONLY.\n * A reader that meets it uses it when the box has no `MapShard`, and the\n * formatter moves it; nothing writes it any more. Removed after the next\n * release, at which point a map left here is simply lost. */\n map?: BoxMap;\n}\n\n/** The DESIGNER's map for one box: where its hands stand in space.\n *\n * Split out of the view shard on 2026-09-06 (design/engine-server.md 9.1 point\n * 5). The two halves had stopped sharing an owner: a hand's position ships in\n * the bundle's `maps` block (4.3), which makes it the thing a venue provisions\n * its kiosks against, while a deck's canvas is a working drawing that never\n * leaves the folder. A server's author key may change the canvases and not\n * this.\n *\n * The map is NESTED under `map` rather than flattened to the top level, and\n * deliberately: the block's bytes are then exactly what the view shard held, so\n * the migration is a move of a value rather than a reshaping of it, the merge\n * strategy carries over word for word, and a reader that has to look in both\n * places is one expression (`box.map?.map ?? box.view?.map`).\n *\n * Source-only in the sense the view shard is not: `compileMaps` reads the\n * positions for the bundle's `maps` block, under `export.map`. */\nexport interface MapShard {\n schema: typeof MAP_SCHEMA;\n map: BoxMap;\n}\n\n/** A coverage input driver: during a coverage run the harness feeds a\n * host-seam property (`@world.x`) values from `values`, so content gated on\n * external state gets exercised (Patter's coverageDrivers, carried whole). */\nexport interface CoverageDriver {\n /** \"initial\": set once as each playthrough starts. \"recurring\": re-rolled\n * per turn at the cadence, so one run passes through several states. */\n kind: \"initial\" | \"recurring\";\n /** For recurring drivers: how often to re-roll per turn (default \"sometimes\"). */\n cadence?: \"rarely\" | \"sometimes\" | \"often\";\n /** The pool the harness picks from (uniform). Empty = inert. */\n values: ScalarValue[];\n}\n\n/** Authoring-side coverage configuration (never compiled into the bundle). */\nexport interface CoverageConfig {\n /** Property drivers, keyed by ref (\"@world.danger\"). */\n drivers?: Record<string, CoverageDriver>;\n\n}\n\nexport interface ProjectShard {\n schema: typeof PROJECT_SCHEMA;\n project: {\n id: string;\n name: string;\n version: string;\n };\n settings: ProjectSettings;\n /** Coverage drivers + argument domains (authoring/testing config; stays\n * out of the compiled bundle). */\n coverage?: CoverageConfig;\n /** Validation switches (authoring config; never compiled). Off is written\n * as ABSENT, like `export.map`: a shard says what an author chose. */\n validation?: {\n /** Also warn when state is WRITTEN but nothing reads it. Off by default:\n * cards are routinely written ahead of the content that will read them,\n * so mid-development this warning is mostly noise. The read side (a gate\n * on state nothing writes) always warns, because that kills cards now. */\n warnUnreadWrites?: boolean;\n };\n /**\n * Where the game's shared scopes folder is, relative to the folder holding this file\n * (`\"../../shared/game-scopes\"`). Authoring config, never compiled, and usually absent: the\n * tools find a `game-scopes/` folder by walking up from the project on their own, stopping at\n * the version-control root, so this is only for a folder that walk would not reach. A path\n * that doesn't exist is a project error. (patterkit design/shared-scopes.md.)\n */\n gameScopes?: string;\n /**\n * The Patter project this one is paired with: a `.patter` folder, relative to the folder\n * holding this file (`\"../story/the-hamlet.patter\"`). Authoring config, never compiled: the\n * engines still know nothing of each other (Reboot 10). With it, `validate` checks each card\n * against the scene of the same name in the Patter project's published bundle, and\n * Storyletter can open Patterpad at a card's scene. A folder that isn't there is a warning,\n * since a writer may hold the cards without the dialogue.\n */\n patter?: string;\n /**\n * The boxes the game performs through the paired Patter project, by box id: every card in them\n * plays the scene named after it. Authoring config, never compiled, and only meaningful beside\n * `patter`. With it, `validate` checks only these boxes and also reports a card with no scene;\n * Storyletter's Board plays their cards' scenes, and the playable export carries them.\n * Absent, every card that has a scene is checked and none is required to have one.\n */\n patterBoxes?: string[];\n world: {\n properties: PropertyDecl[];\n registry?: unknown;\n };\n story: {\n properties: PropertyDecl[];\n };\n /** Templates of play: configuration bags keyed by template name. Core\n * validates only what it knows. */\n templates: Record<string, unknown>;\n export: {\n bundle: string;\n metadata: \"full\" | \"stripped\";\n /**\n * Does a `.storyletpack` carry the boxes' binary assets (background images)?\n *\n * Default false, and a project-level DEFAULT rather than a rule: a pack is a\n * delivery, so the caller can override it per pack (2026-08-07). Some\n * projects would benefit from sending their pictures in certain\n * circumstances and others never would, which is why neither \"always\" nor\n * \"never\" is the answer.\n *\n * Nothing to do with the compiled bundle, which has its own switch: `map`.\n */\n packAssets?: boolean;\n /**\n * Does the compiled bundle carry the maps (zone shapes and background\n * pictures)?\n *\n * Default false, and the default matters: geometry is authoring data, the\n * runtime deals in tag names, and a shipping build should carry nothing it\n * does not use. But a host that wants an in-game map should not have to\n * invent its own export, and it is most useful early - a prototype with a\n * real map beats a prototype with a list of zone names.\n *\n * It sits beside `metadata` on purpose: that is already the switch for\n * \"authoring data that may or may not ship\", and this is its sibling rather\n * than a new concept. With it on, `export` also writes the background files\n * next to the bundle, and `describeBundle` says what is in there.\n */\n map?: boolean;\n };\n}\n\nexport interface BoxShard {\n schema: typeof BOX_SCHEMA;\n box: {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Authored display order among boxes (sparse; absent falls back to the\n * folder-name position). Authoring-only, like a card's (never compiled\n * into the bundle); merges as a per-field value. */\n order?: number;\n ranking: { specificity: boolean };\n /** Declares a timed box (see `Box.turn`); compiled through unchanged. */\n turn?: { seconds: number };\n fields: FieldDecl[];\n /** The outcome fields (see `Box.outcomeFields`); a shard without the key\n * declares none. */\n outcomeFields?: FieldDecl[];\n properties: PropertyDecl[];\n };\n}\n\n/** The box's tag groups: how its cards are filed. */\nexport interface TagsShard {\n schema: typeof TAGS_SCHEMA;\n groups: TagGroup[];\n}\n\n/** The box's hand templates + hands (the writer/programmer contract). */\nexport interface HandsShard {\n schema: typeof HANDS_SCHEMA;\n templates: HandTemplate<string>[];\n hands: Hand<string>[];\n}\n\nexport interface DeckShard {\n schema: typeof DECK_SCHEMA;\n deck: {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n condition?: string;\n /** Scarce across flows: see Deck.shared. */\n shared?: boolean;\n /** Its `redraw: \"never\"` cards are spent past the run: see Deck.durable. */\n durable?: boolean;\n /** Authored display order within the box (sparse; see BoxShard). */\n order?: number;\n properties: PropertyDecl[];\n };\n cards: Card<string>[];\n}\n\n// --- templates of play --------------------------------------------------------\n// The spatial template's types, field access and geometry. Re-exported here so the\n// package has one entry point, and kept in its own module because core schema and\n// a template of play are different things (Reboot 6).\nexport * from \"./spatial.js\";\n\n// How a hand reaches a tag group, and whether that binding is the hand's own to\n// change. Core schema rather than a template of play, but the map is what needed\n// it said out loud.\nexport * from \"./hands.js\";\n\n// Frames: what an author draws around the content. Arrangement,\n// so it lives in the sidecar and reads forgivingly (furniture.ts says why).\nexport * from \"./furniture.js\";\n\n// Threaded comments: the conversation about a thing, in its own sidecar.\nexport * from \"./comments.js\";\n\n// Guessing a property's type from what an outcome writes: the quick fix's input.\nexport * from \"./infer.js\";\n","// ---------------------------------------------------------------------------\n// Cards against their scenes, on the two PUBLISHED bundles: the build-time\n// check a game runs so the naming convention is safe (lifted from the Hamlet's\n// scripts/pairing.mjs, whose messages it keeps). Nothing declares the links, so\n// nothing else validates them: the failure it catches is a card that performs\n// no dialogue, or a branch that ends without saying what happened, and both\n// look exactly like content somebody meant to write.\n//\n// A card finds its scene as Patter's runtime resolves a reference: an internal\n// id first, else an address (a pinned gameId, else the name's slug). Storyletter\n// runs the same analysis on the project as you edit (ops `patter-link.ts`, which\n// takes the two walkers below from here); this is the game's copy, on what ships.\n// ---------------------------------------------------------------------------\n\nimport { gameIdify } from \"@storylet-studio/model\";\n\n/** Every gameEvent outcome id anywhere under a compiled node, in document order. */\nexport function outcomesReported(node: unknown): string[] {\n const found: string[] = [];\n (function walk(n: unknown): void {\n if (Array.isArray(n)) { n.forEach(walk); return; }\n if (!n || typeof n !== \"object\") return;\n const o = n as { kind?: unknown; gameData?: { outcome?: unknown } };\n if (o.kind === \"gameEvent\" && typeof o.gameData?.outcome === \"string\") found.push(o.gameData.outcome);\n for (const v of Object.values(n)) walk(v);\n })(node);\n return found;\n}\n\n/** One choice option in a compiled scene, as the pairing reads it. */\nexport interface BundleOption {\n id: string;\n /** The string id of the words the player picks, when the option has any. */\n promptId?: string;\n /** The outcome the option labels itself with, if any. */\n outcome: string | null;\n /** The gameEvent outcomes its branch fires, which win over the label. */\n overrides: string[];\n}\n\n/** Every choice option in a compiled scene (a group carrying a prompt). */\nexport function optionsOf(scene: unknown): BundleOption[] {\n const found: BundleOption[] = [];\n (function walk(n: unknown): void {\n if (Array.isArray(n)) { n.forEach(walk); return; }\n if (!n || typeof n !== \"object\") return;\n const o = n as { type?: unknown; prompt?: { id?: unknown }; id?: unknown; gameData?: { outcome?: unknown }; children?: unknown };\n if (o.type === \"group\" && o.prompt !== undefined) {\n found.push({\n id: String(o.id),\n ...(typeof o.prompt?.id === \"string\" ? { promptId: o.prompt.id } : {}),\n outcome: typeof o.gameData?.outcome === \"string\" ? o.gameData.outcome : null,\n overrides: outcomesReported(o.children ?? []),\n });\n }\n // The prompt is text, never structure: walking it could mistake a nested group for an option.\n for (const [k, v] of Object.entries(n)) if (k !== \"prompt\") walk(v);\n })(scene);\n return found;\n}\n\ninterface BundleCard { id: string; gameId?: string; outcomes?: { gameId?: string; id: string }[] }\ninterface BundleScene { name?: string; gameId?: string; [key: string]: unknown }\n\n/**\n * Compare a compiled storylet bundle with a compiled Patter bundle. Returns the problems, one\n * readable line each; empty means they line up.\n *\n * `boxes` (box gameIds) limits the check to the boxes the game performs through Patter: in those,\n * a card with no scene is a problem, and so is a scene no card plays. Without it, every card that\n * has a scene is checked and none is required to have one.\n *\n * The rule enforced is the one the Performer plays by: a gameEvent wins, else the label on the\n * option taken, else the card's only outcome. So a scene whose card has one outcome need say\n * nothing, and one whose card has several must leave no path that says nothing.\n */\nexport function checkPairing(storyletBundle: unknown, patterBundle: unknown, boxes?: readonly string[]): string[] {\n const problems: string[] = [];\n const scenes = ((patterBundle as { scenes?: Record<string, BundleScene> }).scenes) ?? {};\n const address = (s: BundleScene): string => s.gameId?.trim() || gameIdify(s.name ?? \"\");\n const byAddress = new Map(Object.entries(scenes).map(([id, s]) => [address(s), id]));\n const sceneFor = (ref: string): string | undefined => (scenes[ref] ? ref : byAddress.get(ref));\n const played = new Set<string>();\n\n const allBoxes = ((storyletBundle as { boxes?: { gameId?: string; id: string; decks?: { cards?: BundleCard[] }[] }[] }).boxes) ?? [];\n for (const box of allBoxes) {\n if (boxes && !boxes.includes(box.gameId ?? box.id)) continue;\n for (const deck of box.decks ?? []) {\n for (const card of deck.cards ?? []) {\n const name = card.gameId ?? card.id;\n const id = sceneFor(name);\n if (id === undefined) {\n if (boxes) problems.push(`card \"${name}\" has no scene of that name`);\n continue;\n }\n played.add(id);\n const scene = scenes[id];\n const declared = (card.outcomes ?? []).map((o) => o.gameId ?? o.id);\n const options = optionsOf(scene);\n const events = outcomesReported(scene);\n const named = [...new Set([...events, ...options.flatMap((o) => (o.outcome ? [o.outcome] : []))])];\n\n for (const n of named) {\n if (!declared.includes(n)) {\n problems.push(`scene \"${name}\" names outcome \"${n}\", which that card does not declare (it declares: ${declared.join(\", \") || \"none\"})`);\n }\n }\n if (declared.length > 1) {\n if (options.length === 0 && events.length === 0) {\n problems.push(`scene \"${name}\" says nothing about its outcome, and its card declares ${declared.length} (${declared.join(\", \")}): label its options, or fire a gameEvent`);\n }\n for (const o of options) {\n if (!o.outcome && o.overrides.length === 0) {\n problems.push(`option \"${o.id}\" in scene \"${name}\" names no outcome and fires no gameEvent, so taking it leaves the host guessing between ${declared.join(\", \")}`);\n }\n }\n for (const d of declared) {\n if (!named.includes(d)) problems.push(`outcome \"${d}\" of card \"${name}\" is named by no option and no gameEvent`);\n }\n }\n }\n }\n }\n if (boxes) {\n for (const [id, scene] of Object.entries(scenes)) {\n if (!played.has(id)) problems.push(`scene \"${scene.gameId?.trim() || id}\" belongs to no card, so nothing can ever play it`);\n }\n }\n return problems;\n}\n"],"mappings":"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","names":["src_exports","__export","Performer","checkPairing","optionsOf","outcomesReported","sceneIdFor","sceneIdFor","engine","bundle","ref","id","MAX_STEPS","Performer","patter","boxes","sceneIdOf","boxGameId","card","outcomes","sceneId","p","flow","existing","e","saved","options","optionsFor","optionId","option","o","next","box","start","i","step","named","reached","outcome","shut","x","why","gameIdify","text","outcomesReported","node","found","walk","o","v","optionsOf","scene","k","checkPairing","storyletBundle","patterBundle","boxes","problems","scenes","address","s","gameIdify","byAddress","id","sceneFor","ref","played","allBoxes","box","deck","card","name","declared","options","events","named","n","d"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@storylet-studio/with-patter",
3
- "version": "0.1.0",
4
- "description": "Storylets with Patter: perform a dealt card as the Patter scene named after it, and take the outcome the scene reaches. DOM-free, for a game, the Board and the playable page alike.",
3
+ "version": "0.2.0",
4
+ "description": "Storylets with Patter: perform a dealt card as the Patter scene named after it, take the outcome the scene reaches, and check the cards and scenes line up. DOM-free, for a game, the Board and the playable page alike.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -13,6 +13,14 @@
13
13
  },
14
14
  "type": "module",
15
15
  "author": "Ian Thomas",
16
+ "keywords": [
17
+ "storylets",
18
+ "patter",
19
+ "storylet-studio",
20
+ "patterkit",
21
+ "interactive-fiction",
22
+ "dialogue"
23
+ ],
16
24
  "main": "./dist/index.js",
17
25
  "module": "./dist/index.js",
18
26
  "types": "./dist/index.d.ts",
@@ -21,7 +29,8 @@
21
29
  "types": "./dist/index.d.ts",
22
30
  "import": "./dist/index.js",
23
31
  "require": "./dist/index.cjs"
24
- }
32
+ },
33
+ "./with-patter.min.js": "./dist/with-patter.min.js"
25
34
  },
26
35
  "files": [
27
36
  "dist",
@@ -31,15 +40,10 @@
31
40
  "scripts": {
32
41
  "build": "tsup"
33
42
  },
43
+ "dependencies": {
44
+ "@storylet-studio/model": ">=0.1.0 <1.0.0"
45
+ },
34
46
  "peerDependencies": {
35
47
  "@patterkit/runtime": ">=0.14.0 <1.0.0"
36
- },
37
- "keywords": [
38
- "storylets",
39
- "patter",
40
- "storylet-studio",
41
- "patterkit",
42
- "interactive-fiction",
43
- "dialogue"
44
- ]
48
+ }
45
49
  }