@polycode-projects/the-mechanical-code-talker 4.0.1 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +2 -1
  2. package/corpus/sprites/src/sprite-facts.jsonl +375 -8
  3. package/package.json +1 -1
  4. package/src/adapters/memory/core.mjs +20 -0
  5. package/src/domain/ask-vocab.mjs +71 -0
  6. package/src/domain/ask.mjs +168 -0
  7. package/src/domain/game-config.mjs +11 -0
  8. package/src/domain/mud-facts.mjs +15 -0
  9. package/src/domain/router/drive.mjs +35 -9
  10. package/src/domain/router/registry.mjs +24 -4
  11. package/src/domain/router/resolver.mjs +102 -40
  12. package/src/domain/scene-compose.mjs +117 -0
  13. package/src/domain/spider-fly-world.mjs +36 -0
  14. package/src/domain/sprite-facts.mjs +0 -0
  15. package/src/domain/sprite-request.mjs +156 -0
  16. package/src/domain/sprite-templates.mjs +161 -14
  17. package/src/services/adventure-editor.mjs +8 -14
  18. package/src/services/adventure-viz.mjs +119 -150
  19. package/src/services/adventure.mjs +97 -35
  20. package/src/services/chat-page-viz.mjs +64 -48
  21. package/src/services/chat.mjs +102 -34
  22. package/src/services/code-explorer-viz.mjs +52 -50
  23. package/src/services/ingest-viz.mjs +32 -74
  24. package/src/services/ledger-viz.mjs +87 -70
  25. package/src/services/memory-panel-viz.mjs +38 -0
  26. package/src/services/mud-editor.mjs +10 -15
  27. package/src/services/mud-turn.mjs +6 -6
  28. package/src/services/mud-viz.mjs +119 -225
  29. package/src/services/p2p-room.mjs +90 -23
  30. package/src/services/plan-pddl.mjs +3 -1
  31. package/src/services/plan-viz.mjs +13 -12
  32. package/src/services/research-viz.mjs +25 -67
  33. package/src/services/spider-fly-turn.mjs +14 -22
  34. package/src/services/spider-fly-viz.mjs +97 -136
  35. package/src/services/spider-fly.mjs +69 -11
  36. package/src/services/sprite-catalog-viz.mjs +274 -224
  37. package/src/services/viz-boot.mjs +71 -0
  38. package/src/services/viz-room-graph.mjs +203 -0
  39. package/src/services/viz-theme.mjs +75 -1
  40. package/src/services/viz-ticker.mjs +22 -0
  41. package/src/surfaces/web/adventure-browser-entry.mjs +62 -47
  42. package/src/surfaces/web/chat-browser-entry.mjs +51 -107
  43. package/src/surfaces/web/code-explorer-browser-entry.mjs +192 -35
  44. package/src/surfaces/web/engine-surface.mjs +82 -0
  45. package/src/surfaces/web/ingest-browser-entry.mjs +16 -17
  46. package/src/surfaces/web/ledger-browser-entry.mjs +24 -56
  47. package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
  48. package/src/surfaces/web/memory-ask-browser.bundle.js +128 -125
  49. package/src/surfaces/web/memory-stats.mjs +11 -0
  50. package/src/surfaces/web/mud-browser-entry.mjs +70 -49
  51. package/src/surfaces/web/plan-browser-entry.mjs +39 -50
  52. package/src/surfaces/web/research-browser-entry.mjs +48 -46
  53. package/src/surfaces/web/spider-fly-browser-entry.mjs +76 -40
  54. package/src/surfaces/web/sprites-browser-entry.mjs +28 -32
  55. package/src/surfaces/web/tmct-surface.mjs +147 -0
  56. package/src/surfaces/web/turn-session.mjs +124 -0
  57. package/src/tools/definitions.mjs +30 -0
  58. package/src/tools/handlers/index.mjs +6 -3
  59. package/src/tools/handlers/kit.mjs +19 -2
  60. package/src/tools/handlers/tmct-ask.mjs +11 -6
  61. package/src/tools/handlers/tmct-ingest.mjs +5 -1
  62. package/src/tools/handlers/tmct-related.mjs +4 -4
  63. package/src/tools/handlers/tmct-sprite.mjs +147 -0
  64. package/src/tools/memory-fallthrough.mjs +9 -2
  65. package/src/tools/server.mjs +37 -6
@@ -19,7 +19,7 @@
19
19
  // already takes with its own precomputed memory payload, just applied to a
20
20
  // second kind of build-time data.
21
21
  //
22
- // Eight pure, `.toString()`-splice-safe pieces are exported as real functions
22
+ // Seven pure, `.toString()`-splice-safe pieces are exported as real functions
23
23
  // (not raw inline-script text) so they can be pinned directly by tests, the
24
24
  // same discipline spider-fly-viz.mjs holds classOfAgentId/
25
25
  // threadCellsForSpiderPlan to: `spriteClassForObject` (an object's sprite
@@ -33,27 +33,35 @@
33
33
  // ancestor walk, then floor), `roomSceneLayout` (the room split into a wall
34
34
  // band and floor stacks, over `roomSceneObjects` and `scenePlacement`),
35
35
  // `roomKindForRoom` (a room's border treatment, from its own rdf:type),
36
- // `carriedItems` (every object placed with the player), `exitDoorways` (the
37
- // written ways out of one room, in compass order, each marked with whether
38
- // this session has walked it — what the room view's own door plates are
39
- // drawn from), and `visitedRoomGraph`
40
- // (a directions-only layout of the rooms a session has actually visited
41
- // see its own header for the exposure discipline). None of these import
42
- // anything beyond this module's own exports, which is what keeps a raw
43
- // `.toString()` splice safe. Three further pure helpers are exported for
44
- // testing but NOT spliced, because each calls another module's export the
45
- // in-page script instead reaches through the browser bundle's own
46
- // `tmctAdventure` global (mirroring how the inline script calls
47
- // `tmctSpiderFly.*` rather than re-importing spider-fly-world.mjs):
48
- // `roomCaptionText` (calls `worldDigestRows`; the in-page `captionFor`
49
- // mirrors it against `tmctAdventure.worldDigestRows`), `pillsForRoom` (a thin
50
- // wrapper over adventure.mjs's own exported `roomAffordances`, whose header
51
- // explains why its list can never promise an action one of take/open/talk/
52
- // examine would then refuse; the in-page `pillsFor` mirrors it against
53
- // `tmctAdventure.roomAffordances`), and `goalStatusLines` (calls
54
- // `foldWorldState` and adventure-autoplay.mjs's own `exposedFacts`; the
55
- // in-page `goalStatusLinesFor` mirrors both against the `tmctAdventure`
56
- // global too).
36
+ // `carriedItems` (every object currently placed with a `holder`, "player" by
37
+ // default), and `exitDoorways` (the written ways out of one room, in compass
38
+ // order, each marked with whether this session has walked it — what the room
39
+ // view's own door plates are drawn from). None of these import anything
40
+ // beyond this module's own exports, which is what keeps a raw `.toString()`
41
+ // splice safe.
42
+ //
43
+ // Four further pure helpers are exported for testing but NOT spliced,
44
+ // because each calls another module's export the in-page script instead
45
+ // reaches through the browser bundle's own `tmct` surface (mirroring
46
+ // how the spider-fly page's inline script calls it rather than re-importing
47
+ // spider-fly-world.mjs): `roomCaptionText` (calls `worldDigestRows`; the
48
+ // in-page `captionFor` mirrors it against `tmct.page.worldDigestRows`),
49
+ // `pillsForRoom` (a thin wrapper over adventure.mjs's own exported
50
+ // `roomAffordances`, whose header explains why its list can never promise an
51
+ // action one of take/open/talk/examine would then refuse; the in-page
52
+ // `pillsFor` mirrors it against `tmct.page.roomAffordances`),
53
+ // `goalStatusLines` (calls `foldWorldState` and adventure-autoplay.mjs's own
54
+ // `exposedFacts`; the in-page `goalStatusLinesFor` mirrors both against the
55
+ // `tmct` surface too), and `visitedRoomGraph` (a directions-only
56
+ // layout of the rooms a session has actually visited, now a thin wrapper over
57
+ // viz-room-graph.mjs's shared `directedGridLayout` — mud-viz.mjs's own
58
+ // burrowGraph wrote the same BFS-grid layout a second time under its own
59
+ // name, and that shared module is where the layout lives now; see its own
60
+ // header for the algorithm and what `hints`/disconnected components mean).
61
+ // The in-page script never splices `directedGridLayout`/`roomGraphSvg`
62
+ // either, for the same not-self-contained reason: it calls
63
+ // `tmct.page.directedGridLayout`/`tmct.page.roomGraphSvg` straight
64
+ // through the bundle instead of re-implementing the room map a second time.
57
65
  //
58
66
  // The chat dock (chatlog/chatform/chatq/pills, below) mirrors
59
67
  // spider-fly-viz.mjs's own side panel: every manual exchange (via
@@ -84,13 +92,14 @@
84
92
  // an edit implies run through the browser bundle's own `session.applyEdit`
85
93
  // (adventure-browser-entry.mjs), never here — this module only renders and
86
94
  // reads.
87
- import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, scenarioLabel } from "./viz-theme.mjs";
88
- import { createTicker } from "./viz-ticker.mjs";
95
+ import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, scenarioLabel, rowsForWorld, wordBeforeCursor } from "./viz-theme.mjs";
96
+ import { createTicker, createSerialQueue } from "./viz-ticker.mjs";
97
+ import { directedGridLayout } from "./viz-room-graph.mjs";
89
98
  import { worldDigestRows, roomAffordances, foldWorldState } from "./adventure.mjs";
90
99
  import { exposedFacts } from "./adventure-autoplay.mjs";
91
100
  import { relatedForTerm } from "../domain/skos-view.mjs";
92
101
  import { classAncestorChain } from "../domain/sprite-map.mjs";
93
- import { renderWorldEditorText, wordBeforeCursor } from "./adventure-editor.mjs";
102
+ import { renderWorldEditorText } from "./adventure-editor.mjs";
94
103
 
95
104
  const DEFAULT_TITLE = "tmct — the adventure";
96
105
  const PREVIEW_MAX_TICKS = 30;
@@ -336,13 +345,16 @@ export function roomKindForRoom(rows, roomId) {
336
345
  return "indoor";
337
346
  }
338
347
 
339
- /** Every object currently `mgx:located-in` `actingSubject` (default
340
- * "player"), sorted, each with its sprite class — the exact placement
348
+ /** Every object currently `mgx:located-in` `holder` (default "player"),
349
+ * sorted, each with its sprite class — the exact placement
341
350
  * `worldDigestRows`'/`inventoryAnswer`'s own "carries the" branch already
342
- * reads, just returned as a plain list instead of prose. Pure. */
343
- export function carriedItems(rows, state, actingSubject = "player") {
351
+ * reads, just returned as a plain list instead of prose. `holder` is a real
352
+ * parameter, not a name baked into the function — mud-viz.mjs's own
353
+ * `carriedItemsFor` used to hardcode "player" instead, so a differently-
354
+ * named character's own satchel never read correctly there. Pure. */
355
+ export function carriedItems(rows, state, holder = "player") {
344
356
  return [...state.placements]
345
- .filter(([, p]) => p.predicate === "mgx:located-in" && p.object === actingSubject)
357
+ .filter(([, p]) => p.predicate === "mgx:located-in" && p.object === holder)
346
358
  .map(([subject]) => ({ subject, spriteClass: spriteClassForObject(rows, subject) }))
347
359
  .sort((a, b) => a.subject.localeCompare(b.subject));
348
360
  }
@@ -361,47 +373,13 @@ export function carriedItems(rows, state, actingSubject = "player") {
361
373
  * caller can draw at most "there's an exit that way" and nothing more.
362
374
  * Disconnected visited rooms (not reachable from each other by traveled
363
375
  * edges) lay out as separate side-by-side blocks rather than overlapping.
364
- * Pure. */
376
+ *
377
+ * A thin wrapper over viz-room-graph.mjs's shared `directedGridLayout`: no
378
+ * `root` (Ashcombe Hall is never dug two ways into one cell, so there is no
379
+ * level/turf to track) and no collision nudging (a manor fixed at authoring
380
+ * time never collides). Pure. */
365
381
  export function visitedRoomGraph(state, visitedRoomIds, actingSubject = "player") {
366
- const DELTA = { north: [0, -1], south: [0, 1], east: [1, 0], west: [-1, 0], up: [0, -1], down: [0, 1] };
367
- const visited = new Set(visitedRoomIds || []);
368
- const here = state.placements.get(actingSubject)?.object ?? null;
369
- const positions = new Map();
370
- const edges = [];
371
- const edgeKeys = new Set();
372
- const hints = [];
373
- let offsetX = 0;
374
- for (const start of [...visited].sort()) {
375
- if (positions.has(start)) continue;
376
- positions.set(start, { x: offsetX, y: 0 });
377
- const queue = [start];
378
- const component = [start];
379
- while (queue.length) {
380
- const room = queue.shift();
381
- const pos = positions.get(room);
382
- const dirs = state.exits.get(room);
383
- for (const direction of [...(dirs?.keys() ?? [])].sort()) {
384
- const target = dirs.get(direction);
385
- if (!visited.has(target)) { hints.push({ from: room, direction }); continue; }
386
- const key = [room, target].sort().join("\0");
387
- if (!edgeKeys.has(key)) { edgeKeys.add(key); edges.push({ from: room, to: target, direction }); }
388
- if (!positions.has(target)) {
389
- const [dx, dy] = DELTA[direction] ?? [0, 0];
390
- positions.set(target, { x: pos.x + dx, y: pos.y + dy });
391
- component.push(target);
392
- queue.push(target);
393
- }
394
- }
395
- }
396
- offsetX = Math.max(...component.map((r) => positions.get(r).x)) + 2;
397
- }
398
- const minX = Math.min(0, ...[...positions.values()].map((p) => p.x));
399
- const minY = Math.min(0, ...[...positions.values()].map((p) => p.y));
400
- const nodes = [...visited].sort().map((room) => {
401
- const p = positions.get(room) || { x: 0, y: 0 };
402
- return { id: room, x: p.x - minX, y: p.y - minY, current: room === here };
403
- });
404
- return { nodes, edges, hints };
382
+ return directedGridLayout(state, visitedRoomIds, { actingSubject });
405
383
  }
406
384
 
407
385
  /** Every room the world DEFINES at all (every subject the fact rows type as
@@ -501,8 +479,8 @@ export function groundedPlaceholder(actions, fallback) {
501
479
  * row. `[]` on an honest miss (neither lookup returns anything) — never a
502
480
  * fabricated suggestion. NOT .toString()-splice-safe (it calls two other
503
481
  * modules' exports) — the in-page script mirrors this same combination
504
- * against the browser bundle's own `tmctAdventure.relatedForTerm`/
505
- * `tmctAdventure.classAncestorChain`, the same reach-through-the-global
482
+ * against the browser bundle's own `tmct.page.relatedForTerm`/
483
+ * `tmct.page.classAncestorChain`, the same reach-through-the-global
506
484
  * pattern `pillsFor`/`captionFor` already use for their own adventure.mjs
507
485
  * calls (see this module's header). */
508
486
  export function suggestionsForTerm(rows, term) {
@@ -528,11 +506,19 @@ export function suggestionsForTerm(rows, term) {
528
506
  * itself (its own exits) or about something placed IN it — the same
529
507
  * "visible here" boundary `roomSceneObjects` draws from. The player's own
530
508
  * "is in the" row is excluded: the room frame already IS the current room,
531
- * so restating "you are here" is redundant, never informative. */
532
- export function roomCaptionText(rows, state, here) {
509
+ * so restating "you are here" is redundant, never informative.
510
+ *
511
+ * `{ caseInsensitive }` folds the room-id match to lowercase before
512
+ * comparing. Ashcombe Hall's own room ids are already lowercase, so this is
513
+ * a no-op here by default — it exists so mud-viz.mjs's own case-insensitive
514
+ * `roomCaptionFor` variant can share this one function instead of keeping a
515
+ * near-duplicate. */
516
+ export function roomCaptionText(rows, state, here, { caseInsensitive = false } = {}) {
533
517
  const hereCased = here.charAt(0).toUpperCase() + here.slice(1);
518
+ const objectMatches = (value) => (caseInsensitive ? String(value).toLowerCase() === here.toLowerCase() : value === here);
519
+ const subjectMatches = (value) => (caseInsensitive ? String(value).toLowerCase() === here.toLowerCase() : value === hereCased);
534
520
  const lines = worldDigestRows(rows, state)
535
- .filter((row) => row.subject !== "Player" && (row.object === here || row.subject === hereCased))
521
+ .filter((row) => row.subject !== "Player" && (objectMatches(row.object) || subjectMatches(row.subject)))
536
522
  .map((row) => `${row.subject} ${row.predicate} ${row.object}.`);
537
523
  return lines.length ? lines.join(" ") : `Nothing more about the ${here} is written down yet.`;
538
524
  }
@@ -886,7 +872,7 @@ ${THEME_TOKENS_CSS}
886
872
  this just keeps the board's own footprint stable and click-to-enlarge
887
873
  honest about what it's enlarging. */
888
874
  .map-viewport-fixed { width: 190px; margin: 0 auto; cursor: zoom-in; }
889
- /* the lights-down map lightbox — the same board, the same roomMapSvg
875
+ /* the lights-down map lightbox — the same board, the same room-graph svg
890
876
  output, just drawn bigger over a dimmed backdrop. Closes on a click
891
877
  anywhere outside the enlarged board, or Escape. */
892
878
  .map-lightbox { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding: 2.4rem; background: rgba(10, 8, 4, .74); }
@@ -1123,6 +1109,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1123
1109
  (function () {
1124
1110
  "use strict";
1125
1111
  const createTicker = ${createTicker.toString()};
1112
+ const createSerialQueue = ${createSerialQueue.toString()};
1126
1113
  const spriteClassForObject = ${spriteClassForObject.toString()};
1127
1114
  const visibleRoomOf = ${visibleRoomOf.toString()};
1128
1115
  const roomSceneObjects = ${roomSceneObjects.toString()};
@@ -1130,7 +1117,6 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1130
1117
  const roomSceneLayout = ${roomSceneLayout.toString()};
1131
1118
  const roomKindForRoom = ${roomKindForRoom.toString()};
1132
1119
  const carriedItems = ${carriedItems.toString()};
1133
- const visitedRoomGraph = ${visitedRoomGraph.toString()};
1134
1120
  const allRoomIds = ${allRoomIds.toString()};
1135
1121
  const groundedPlaceholder = ${groundedPlaceholder.toString()};
1136
1122
  const exitDoorways = ${exitDoorways.toString()};
@@ -1138,6 +1124,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1138
1124
  const factsForSubject = ${factsForSubject.toString()};
1139
1125
  const renderWorldEditorText = ${renderWorldEditorText.toString()};
1140
1126
  const wordBeforeCursor = ${wordBeforeCursor.toString()};
1127
+ const rowsForWorld = ${rowsForWorld.toString()};
1141
1128
  const esc = ${escapeHtml.toString()};
1142
1129
  // The identity Ashcombe Hall's own world facts place as the carrier/viewer
1143
1130
  // — one named constant standing in for what was six separate "player"
@@ -1212,7 +1199,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1212
1199
  let allStoreRows = [];
1213
1200
 
1214
1201
  // ---- persistence — the manor remembers your visit, on this device -------
1215
- // Best-effort IndexedDB (tmctAdventure.openPersistedStore): after any
1202
+ // Best-effort IndexedDB (tmct.page.openPersistedStore): after any
1216
1203
  // state-changing redraw, the whole Backend-B payload plus the visited-room
1217
1204
  // exposure set snapshots under the "adventure" key, debounced. Restored on
1218
1205
  // boot; reset clears it and starts the world over. The home page's preview
@@ -1258,13 +1245,10 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1258
1245
 
1259
1246
  // ---- serialize every engine-touching call: the ticker, the chat dock and
1260
1247
  // the editor sync all share one in-memory store, and any overlapping pair
1261
- // could race against the same write.
1262
- let lock = Promise.resolve();
1263
- function withLock(fn) {
1264
- const run = lock.then(fn, fn);
1265
- lock = run.catch(() => {});
1266
- return run;
1267
- }
1248
+ // could race against the same write. createSerialQueue is the shared
1249
+ // primitive mud-viz.mjs's own tickChain/serializeTick and spider-fly-viz.mjs's
1250
+ // own inlined withLock each duplicated under a different name.
1251
+ const { run: withLock } = createSerialQueue();
1268
1252
 
1269
1253
  // ---- large-sprite-tier wiring — the gradient-shaded 400px tier
1270
1254
  // (data/sprites-large/*.toml) arrives embedded at build time as
@@ -1313,22 +1297,22 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1313
1297
 
1314
1298
  function captionFor(rows, state, here) {
1315
1299
  const hereCased = here.charAt(0).toUpperCase() + here.slice(1);
1316
- const lines = tmctAdventure.worldDigestRows(rows, state)
1300
+ const lines = tmct.page.worldDigestRows(rows, state)
1317
1301
  .filter((row) => row.subject !== "Player" && (row.object === here || row.subject === hereCased))
1318
1302
  .map((row) => row.subject + " " + row.predicate + " " + row.object + ".");
1319
1303
  return lines.length ? lines.join(" ") : "Nothing more about the " + here + " is written down yet.";
1320
1304
  }
1321
1305
 
1322
1306
  // ---- goal status — mirrors adventure-viz.mjs's own goalStatusLines
1323
- // against tmctAdventure.foldWorldState/tmctAdventure.exposedFacts (the
1307
+ // against tmct.page.foldWorldState/tmct.page.exposedFacts (the
1324
1308
  // same mirroring captionFor/pillsFor already do against their own
1325
1309
  // adventure.mjs exports), so a location is never claimed before the room
1326
1310
  // holding it has actually been visited this session.
1327
1311
  function goalStatusLinesFor(rows, state, visitedRoomIds) {
1328
1312
  const ids = Array.from(new Set(rows.filter((r) => r.predicate === "mgx:is-objective" && r.object === "true").map((r) => r.subject)));
1329
1313
  const carriedIds = new Set(carriedItems(rows, state, ACTING_SUBJECT).map((o) => o.subject));
1330
- const exposedRows = tmctAdventure.exposedFacts(rows, visitedRoomIds);
1331
- const exposedState = tmctAdventure.foldWorldState(exposedRows);
1314
+ const exposedRows = tmct.page.exposedFacts(rows, visitedRoomIds);
1315
+ const exposedState = tmct.page.foldWorldState(exposedRows);
1332
1316
  return ids.map((id) => {
1333
1317
  if (carriedIds.has(id)) return { subject: id, status: "carried", text: "carrying the " + id + " \\u2014 the adventure is won." };
1334
1318
  const room = visibleRoomOf(exposedRows, exposedState, id, ACTING_SUBJECT);
@@ -1349,53 +1333,39 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1349
1333
  }
1350
1334
 
1351
1335
  // ---- room map — ONE svg-building routine shared by play mode's visited-
1352
- // only map and edit mode's whole-map (visitedRoomGraph fed allRoomIds
1336
+ // only map and edit mode's whole-map (visitedRoomGraphFor fed allRoomIds
1353
1337
  // instead of the exposure set — a parameter, not a second layout), so the
1354
1338
  // fixed-size-viewport CSS treatment and the node layout can never drift
1355
1339
  // between the two. clickable adds a data-room attribute and a pointer
1356
1340
  // cursor per node; play mode's own map stays purely informational.
1357
- function roomMapSvg(graph, clickable) {
1358
- if (!graph.nodes.length) return null;
1359
- // Board-game footprints: each room a named rectangle (the name INSIDE
1360
- // it, the way a Cluedo board prints its rooms), corridors as wide paths
1361
- // between the footprints, direction-only hints as path stubs fading
1362
- // toward rooms not yet visited.
1363
- const cell = 64, roomW = 56, roomH = 26;
1364
- const maxX = Math.max.apply(null, graph.nodes.map((n) => n.x));
1365
- const maxY = Math.max.apply(null, graph.nodes.map((n) => n.y));
1366
- const w = (maxX + 1) * cell, h = (maxY + 1) * cell;
1367
- const cx = (n) => (n.x + 0.5) * cell;
1368
- const cy = (n) => (n.y + 0.5) * cell;
1369
- const byRoom = new Map(graph.nodes.map((n) => [n.id, n]));
1370
- const edgesSvg = graph.edges.map((e) => {
1371
- const a = byRoom.get(e.from), b = byRoom.get(e.to);
1372
- return '<line class="room-edge" x1="' + cx(a) + '" y1="' + cy(a) + '" x2="' + cx(b) + '" y2="' + cy(b) + '"></line>';
1373
- }).join("");
1374
- const HINT_DELTA = { north: [0, -1], south: [0, 1], east: [1, 0], west: [-1, 0], up: [0, -1], down: [0, 1] };
1375
- const hintsSvg = graph.hints.map((hi) => {
1376
- const from = byRoom.get(hi.from);
1377
- const d = HINT_DELTA[hi.direction] || [0, 0];
1378
- return '<circle class="room-hint" cx="' + (cx(from) + d[0] * cell * 0.42) + '" cy="' + (cy(from) + d[1] * cell * 0.42) + '" r="3.5"></circle>';
1379
- }).join("");
1380
- const nodesSvg = graph.nodes.map((n) => {
1381
- const cls = "room-node" + (n.current ? " current" : "") + (clickable ? " clickable" : "") + (clickable && n.id === selectedRoomId ? " selected" : "");
1382
- const attr = clickable ? ' data-room="' + esc(n.id) + '"' : "";
1383
- return '<g class="' + cls + '"' + attr + '><rect x="' + (cx(n) - roomW / 2) + '" y="' + (cy(n) - roomH / 2) + '" width="' + roomW + '" height="' + roomH + '" rx="3"></rect>'
1384
- + '<text x="' + cx(n) + '" y="' + (cy(n) + 2.5) + '">' + esc(n.id) + "</text></g>";
1385
- }).join("");
1386
- return '<svg viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="xMidYMid meet" role="img" aria-label="' + (clickable ? "the whole manor \\u2014 click a room to inspect it" : "the rooms visited so far") + '">'
1387
- + edgesSvg + hintsSvg + nodesSvg + "</svg>";
1341
+ // roomGraphSvgFor mirrors the OLD inline roomMapSvg's own board-game sizing
1342
+ // (a 64px square cell, 56x26px room footprints) through the shared
1343
+ // viz-room-graph.mjs renderer, reached via the tmct surface the
1344
+ // same way captionFor/pillsFor already reach their own adventure.mjs
1345
+ // calls: roomGraphSvg needs escapeHtml and a module-level exit-delta table
1346
+ // this splice-safe script carries neither of, so it runs through the
1347
+ // bundle rather than being spliced as text (see this page's own module
1348
+ // header). visitedRoomGraphFor is the same posture for the layout half.
1349
+ function roomGraphSvgFor(graph, clickable) {
1350
+ return tmct.page.roomGraphSvg(graph, {
1351
+ cellX: 64, cellY: 64, roomW: 56, roomH: 26,
1352
+ clickable, selectedRoomId,
1353
+ label: clickable ? "the whole manor \\u2014 click a room to inspect it" : "the rooms visited so far",
1354
+ });
1355
+ }
1356
+ function visitedRoomGraphFor(state, visitedIds) {
1357
+ return tmct.page.directedGridLayout(state, visitedIds, { actingSubject: ACTING_SUBJECT });
1388
1358
  }
1389
1359
  function renderRoomMap(rows, state, visitedRoomIds) {
1390
- mapWrapEl.innerHTML = roomMapSvg(visitedRoomGraph(state, visitedRoomIds, ACTING_SUBJECT), false) || '<span class="empty-note">nowhere yet</span>';
1360
+ mapWrapEl.innerHTML = roomGraphSvgFor(visitedRoomGraphFor(state, visitedRoomIds), false) || '<span class="empty-note">nowhere yet</span>';
1391
1361
  }
1392
1362
 
1393
1363
  // ---- the map lightbox — clicking the fixed-square play-mode map redraws
1394
- // the SAME roomMapSvg output larger, over a dimmed backdrop; clicking the
1395
- // backdrop (not the board itself) or pressing Escape closes it.
1364
+ // the SAME roomGraphSvgFor output larger, over a dimmed backdrop; clicking
1365
+ // the backdrop (not the board itself) or pressing Escape closes it.
1396
1366
  function openMapLightbox() {
1397
1367
  if (!lastSnapshot) return;
1398
- const svg = roomMapSvg(visitedRoomGraph(lastSnapshot.state, lastSnapshot.visitedRoomIds, ACTING_SUBJECT), false);
1368
+ const svg = roomGraphSvgFor(visitedRoomGraphFor(lastSnapshot.state, lastSnapshot.visitedRoomIds), false);
1399
1369
  if (!svg) return;
1400
1370
  mapLightboxInnerEl.innerHTML = svg;
1401
1371
  mapLightboxEl.hidden = false;
@@ -1447,7 +1417,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1447
1417
  addChatLine("u", esc(line));
1448
1418
  addObjDockLine("u", esc(line));
1449
1419
  return withLock(async () => {
1450
- const result = await session.turn(line);
1420
+ const result = await tmct.turn(line);
1451
1421
  addChatLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
1452
1422
  addObjDockLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
1453
1423
  const snap = await session.snapshot();
@@ -1472,7 +1442,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1472
1442
  // nothing, so it needs no main-transcript echo — it IS the board's own
1473
1443
  // content); pill/typed turns below do echo, since they can change state.
1474
1444
  await withLock(async () => {
1475
- const result = await session.turn("look " + subject);
1445
+ const result = await tmct.turn("look " + subject);
1476
1446
  objLookEl.textContent = result.answer;
1477
1447
  });
1478
1448
  }
@@ -1503,7 +1473,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1503
1473
  document.addEventListener("keydown", (e) => { if (e.key === "Escape" && !objLightboxEl.hidden) closeObjectLightbox(); });
1504
1474
 
1505
1475
  function renderEditMap(rows, state) {
1506
- editMapWrapEl.innerHTML = roomMapSvg(visitedRoomGraph(state, allRoomIds(rows), ACTING_SUBJECT), true) || '<span class="empty-note">this world defines no rooms</span>';
1476
+ editMapWrapEl.innerHTML = roomGraphSvgFor(visitedRoomGraphFor(state, allRoomIds(rows)), true) || '<span class="empty-note">this world defines no rooms</span>';
1507
1477
  }
1508
1478
  editMapWrapEl.addEventListener("click", (e) => {
1509
1479
  const g = e.target.closest("[data-room]");
@@ -1535,8 +1505,8 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1535
1505
 
1536
1506
  // ---- contextual pills — refreshed every redraw from the CURRENT room's
1537
1507
  // own roomAffordances-derived list (mirroring pillsForRoom against
1538
- // tmctAdventure.roomAffordances, the same way captionFor above mirrors
1539
- // roomCaptionText against tmctAdventure.worldDigestRows), so a pill for a
1508
+ // tmct.page.roomAffordances, the same way captionFor above mirrors
1509
+ // roomCaptionText against tmct.page.worldDigestRows), so a pill for a
1540
1510
  // room the player has left, or an object already taken, never lingers.
1541
1511
  // Clicking one inserts its exact command text into the input and focuses
1542
1512
  // it; it never auto-submits, so free typing still works and a clicked
@@ -1547,7 +1517,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1547
1517
  // ever wants to edit before running, and drawing it in both places would
1548
1518
  // make one control a suggestion and its twin an action.
1549
1519
  function pillsFor(rows, state, here) {
1550
- return tmctAdventure.roomAffordances(rows, state, here);
1520
+ return tmct.page.roomAffordances(rows, state, here);
1551
1521
  }
1552
1522
  function renderPills(rows, state, here) {
1553
1523
  const actions = pillsFor(rows, state, here).filter((a) => a.indexOf("go ") !== 0);
@@ -1590,10 +1560,10 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1590
1560
  const ROOT_FALLBACK_BY_CLASS = { adventurer: "person", person: "person", room: "room" };
1591
1561
  const rootFallbackFor = (cls) => ROOT_FALLBACK_BY_CLASS[cls] || "portable";
1592
1562
  function resolveObjectSprite(rows, s) {
1593
- if (s.subject === "you") return tmctAdventure.resolveSpriteAsset("adventurer", [], [], activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY, { rootFallback: "person", instanceKey: "you" });
1594
- return tmctAdventure.resolveSpriteAsset(
1563
+ if (s.subject === "you") return tmct.page.resolveSpriteAsset("adventurer", [], [], activeSpriteTemplates, tmct.page.SPRITE_REGISTRY, { rootFallback: "person", instanceKey: "you" });
1564
+ return tmct.page.resolveSpriteAsset(
1595
1565
  s.subject, spriteAncestryRows(rows, s.subject), factsForSubject(rows, s.subject),
1596
- activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY,
1566
+ activeSpriteTemplates, tmct.page.SPRITE_REGISTRY,
1597
1567
  { rootFallback: rootFallbackFor(s.spriteClass), instanceKey: s.subject },
1598
1568
  );
1599
1569
  }
@@ -1604,9 +1574,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1604
1574
  // everything else falls back through room's own ancestor chain to the
1605
1575
  // generic room icon.
1606
1576
  function roomKindIconSvg(rows, here) {
1607
- return tmctAdventure.resolveSpriteAsset(
1577
+ return tmct.page.resolveSpriteAsset(
1608
1578
  here, spriteAncestryRows(rows, here), factsForSubject(rows, here),
1609
- activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY,
1579
+ activeSpriteTemplates, tmct.page.SPRITE_REGISTRY,
1610
1580
  { rootFallback: "room", instanceKey: "roomkind-" + here },
1611
1581
  );
1612
1582
  }
@@ -1671,7 +1641,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1671
1641
  ticker.pause();
1672
1642
  addChatLine("u", esc(line));
1673
1643
  return withLock(async () => {
1674
- const result = await session.turn(line);
1644
+ const result = await tmct.turn(line);
1675
1645
  addChatLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
1676
1646
  const snap = await session.snapshot();
1677
1647
  redraw(snap);
@@ -1701,8 +1671,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1701
1671
  // diff + write; this page only ever reads its result back.
1702
1672
 
1703
1673
  function worldOnlyRows(rows) {
1704
- const prefix = "world:" + world().name;
1705
- return (rows || []).filter((r) => typeof r.provenance === "string" && r.provenance.indexOf(prefix) === 0);
1674
+ return rowsForWorld(rows, world().name);
1706
1675
  }
1707
1676
 
1708
1677
  function renderRoomDetail() {
@@ -1734,9 +1703,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1734
1703
  const classes = new Set(["adventurer"]);
1735
1704
  subjects.forEach((s) => { if (s !== ACTING_SUBJECT) classes.add(spriteClassForObject(rows, s)); });
1736
1705
  legendListEl.innerHTML = Array.from(classes).sort().map((cls) => {
1737
- const svg = tmctAdventure.resolveSpriteAsset(
1706
+ const svg = tmct.page.resolveSpriteAsset(
1738
1707
  cls, spriteAncestryRows(rows, cls), factsForSubject(rows, cls),
1739
- activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY,
1708
+ activeSpriteTemplates, tmct.page.SPRITE_REGISTRY,
1740
1709
  { rootFallback: rootFallbackFor(cls), instanceKey: "legend-" + cls },
1741
1710
  );
1742
1711
  return spriteCardHtml(cls, cls, svg);
@@ -1755,8 +1724,8 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1755
1724
  }
1756
1725
 
1757
1726
  // ---- cursor-driven suggestion pills — the lateral SKOS neighbourhood
1758
- // (tmctAdventure.relatedForTerm) plus the vertical is-a ancestor chain
1759
- // (tmctAdventure.classAncestorChain), mirroring adventure-viz.mjs's own
1727
+ // (tmct.page.relatedForTerm) plus the vertical is-a ancestor chain
1728
+ // (tmct.page.classAncestorChain), mirroring adventure-viz.mjs's own
1760
1729
  // suggestionsForTerm against the browser bundle's global (the same
1761
1730
  // reach-through-the-global pattern captionFor/pillsFor already use).
1762
1731
  // Reads the FULL store (allStoreRows), not worldOnlyRows — a term's
@@ -1765,8 +1734,8 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1765
1734
  function renderSuggestionPills() {
1766
1735
  const term = wordBeforeCursor(editorTextEl.value, editorTextEl.selectionStart);
1767
1736
  if (!term) { editorPillsEl.innerHTML = ""; return; }
1768
- const related = tmctAdventure.relatedForTerm(allStoreRows, term);
1769
- const chain = tmctAdventure.classAncestorChain(term, allStoreRows);
1737
+ const related = tmct.page.relatedForTerm(allStoreRows, term);
1738
+ const chain = tmct.page.classAncestorChain(term, allStoreRows);
1770
1739
  const seen = new Set([term]);
1771
1740
  const out = [];
1772
1741
  const push = (label) => { if (label && !seen.has(label)) { seen.add(label); out.push(label); } };
@@ -1797,7 +1766,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1797
1766
  const snap = await session.snapshot();
1798
1767
  allStoreRows = snap.rows;
1799
1768
  editRows = worldOnlyRows(snap.rows);
1800
- editState = tmctAdventure.foldWorldState(editRows);
1769
+ editState = tmct.page.foldWorldState(editRows);
1801
1770
  if (result.unrecognized.length) {
1802
1771
  const lines = result.unrecognized.map((u) => u.line).join(", ");
1803
1772
  editorStatusEl.className = "edit-status pending";
@@ -1828,7 +1797,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1828
1797
  const snap = await session.snapshot();
1829
1798
  allStoreRows = snap.rows;
1830
1799
  editRows = worldOnlyRows(snap.rows);
1831
- editState = tmctAdventure.foldWorldState(editRows);
1800
+ editState = tmct.page.foldWorldState(editRows);
1832
1801
  editorTextEl.value = renderWorldEditorText(editRows, editState);
1833
1802
  editorStatusEl.className = "edit-status";
1834
1803
  editorStatusEl.textContent = "";
@@ -1849,7 +1818,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1849
1818
 
1850
1819
  async function boot({ fresh = false } = {}) {
1851
1820
  const saved = !fresh && persist ? await persist.load() : null;
1852
- session = await tmctAdventure.createAdventureSession(
1821
+ session = await tmct.open(
1853
1822
  world(),
1854
1823
  saved && saved.payload && saved.payload.memoryPayload
1855
1824
  ? { restoredPayload: saved.payload.memoryPayload, restoredVisitedRoomIds: saved.payload.visitedRoomIds }
@@ -1908,9 +1877,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1908
1877
  // to. Without that, one world's snapshot would restore into another's graph.
1909
1878
  let siteVersion = "";
1910
1879
  async function openPersistFor(worldName) {
1911
- if (preview || !tmctAdventure.openPersistedStore) return;
1880
+ if (preview || !tmct.page.openPersistedStore) return;
1912
1881
  if (!siteVersion) siteVersion = await fetchSiteVersion();
1913
- persist = tmctAdventure.openPersistedStore({ storeKey: "adventure", stamp: siteVersion + ":" + worldName });
1882
+ persist = tmct.page.openPersistedStore({ storeKey: "adventure", stamp: siteVersion + ":" + worldName });
1914
1883
  }
1915
1884
 
1916
1885
  if (scenarioSelectEl) {