@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
@@ -57,33 +57,40 @@
57
57
  // The burrow survey is ONE svg routine (burrowSvg over burrowGraph) drawn
58
58
  // twice: the omniscient board in the top row, and each pane's own
59
59
  // visited-only "known ground". Same layout, same treatment, one code path —
60
- // adapted from adventure-viz.mjs's own roomMapSvg/visitedRoomGraph pair, with
61
- // a cell-collision nudge burrowGraph needs and that page does not (a mud
62
- // character can dig down AND south out of one room, and both land on the
63
- // same grid cell otherwise).
60
+ // burrowGraph/burrowSvg are thin wrappers over viz-room-graph.mjs's shared
61
+ // directedGridLayout/roomGraphSvg (the same pair adventure-viz.mjs draws its
62
+ // own manor board from), passing the `root`/`nudgeCollisions`/`turf` options a
63
+ // burrow needs and a manor does not (a mud character can dig down AND south
64
+ // out of one room, and both land on the same grid cell otherwise).
64
65
  //
65
66
  // ONE ticker instance per pane (viz-ticker.mjs's createTicker), so
66
67
  // each can play/step independently, plus the deck's own "play" control that
67
68
  // calls .play()/.pause() on both pane tickers at once. Nothing plays until
68
69
  // that control is clicked. Every tick, from ANY pane, is funneled through one
69
- // shared async queue (serializeTick, spliced below) so two characters' turns
70
- // can never interleave their reads/writes of the one shared memoryDir —
71
- // mud-browser-entry.mjs's own header names this as the caller's
72
- // responsibility, and this is where that responsibility is discharged. The
73
- // queue is also what makes the deck's GLOBAL turn counter well-defined: it
74
- // increments in the exact order turns actually executed, never a race
75
- // between panes.
76
- import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, scenarioLabel } from "./viz-theme.mjs";
77
- import { createTicker } from "./viz-ticker.mjs";
70
+ // shared async queue (serializeTick, wrapping viz-ticker.mjs's own
71
+ // createSerialQueue) so two characters' turns can never interleave their
72
+ // reads/writes of the one shared memoryDir — mud-browser-entry.mjs's own
73
+ // header names this as the caller's responsibility, and this is where that
74
+ // responsibility is discharged. The queue is also what makes the deck's
75
+ // GLOBAL turn counter well-defined: it increments in the exact order turns
76
+ // actually executed, never a race between panes.
77
+ import {
78
+ THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, scenarioLabel,
79
+ wordBeforeCursor, rowsForWorld, appendLogLine,
80
+ } from "./viz-theme.mjs";
81
+ import { createTicker, createSerialQueue } from "./viz-ticker.mjs";
82
+ import { directedGridLayout, roomGraphSvg, levelsOf, EXIT_DELTA } from "./viz-room-graph.mjs";
78
83
  import {
79
84
  roomSceneObjects, scenePlacement, spriteClassForObject, spriteAncestryRows, factsForSubject,
80
85
  visibleRoomOf, roomKindForRoom, allRoomIds,
81
86
  } from "./adventure-viz.mjs";
82
- import { renderMudEditorText, wordBeforeCursor } from "./mud-editor.mjs";
87
+ import { renderMudEditorText } from "./mud-editor.mjs";
83
88
  import {
84
89
  wireStateLabel, nodeRowsFor, nodeInitials, inviteLinkFor, inviteParamsFrom,
85
90
  } from "./chat-page-viz.mjs";
86
91
  import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
92
+ import { fnv1a32 } from "../domain/hash.mjs";
93
+ import { predatorSubjects } from "../domain/mud-facts.mjs";
87
94
 
88
95
  const DEFAULT_TITLE = "tmct — the mud";
89
96
  // No display-face embedding pipeline exists anywhere in this project yet
@@ -150,22 +157,9 @@ export function carriedItemsFor(rows, state, character) {
150
157
  * keeps the level unchanged — what tells the survey where the turf line
151
158
  * falls. A room unreachable from `root` (should not happen — every dug room
152
159
  * writes a two-way exit back) is simply absent from the map rather than
153
- * guessed at. Pure, self-contained. */
154
- export function levelsOf(state, root = "garden") {
155
- const levels = new Map([[root, 0]]);
156
- const queue = [root];
157
- while (queue.length) {
158
- const room = queue.shift();
159
- const level = levels.get(room);
160
- for (const [direction, target] of state.exits.get(room) ?? []) {
161
- if (levels.has(target)) continue;
162
- const delta = direction === "down" ? -1 : direction === "up" ? 1 : 0;
163
- levels.set(target, level + delta);
164
- queue.push(target);
165
- }
166
- }
167
- return levels;
168
- }
160
+ * guessed at. Re-exported from viz-room-graph.mjs's shared implementation,
161
+ * which this module also splices into its own inline page script. */
162
+ export { levelsOf };
169
163
 
170
164
  /** Which characters currently stand in `room` — the survey's own per-room
171
165
  * roster, both for the omniscient board and a pane's own visited-only one.
@@ -176,69 +170,21 @@ export function charactersInRoom(state, room, characters) {
176
170
 
177
171
  /** The rooms in `roomIds` laid out on an integer grid FROM the world's own
178
172
  * has-exit-* directions — never a force-directed guess, so a room north of
179
- * another sits one row above it and a room dug DOWN sits one row below.
180
- * Adapted from adventure-viz.mjs's `visitedRoomGraph`, with two differences
181
- * a burrow needs and a manor does not: a cell already taken nudges right
182
- * rather than stacking two rooms on one square (dig down and dig south from
183
- * the same room otherwise collide), and every node carries its own `level`
184
- * so the renderer can draw the turf line between the surface and the soil.
185
- * An edge is drawn only between two rooms BOTH in `roomIds`; `hints` names
186
- * every exit from an included room toward one that is not — the direction
187
- * only, never the excluded room's own name, so a fog-of-war caller can draw
188
- * "there's a way on" and nothing more. Pure. */
173
+ * another sits one row above it and a room dug DOWN sits one row below. A
174
+ * thin wrapper over viz-room-graph.mjs's shared `directedGridLayout`, always
175
+ * passing `nudgeCollisions: true` the one thing a burrow needs that a
176
+ * manor does not: a cell already taken nudges right rather than stacking two
177
+ * rooms on one square (dig down and dig south from the same room otherwise
178
+ * collide). Pure. */
189
179
  export function burrowGraph(state, roomIds, root = "garden") {
190
- const DELTA = { north: [0, -1], south: [0, 1], east: [1, 0], west: [-1, 0], up: [0, -1], down: [0, 1] };
191
- const known = new Set(roomIds || []);
192
- const positions = new Map();
193
- const taken = new Set();
194
- const edges = [];
195
- const edgeKeys = new Set();
196
- const hints = [];
197
- const place = (room, x, y) => {
198
- let px = x;
199
- for (let guard = 0; taken.has(px + "," + y) && guard < 64; guard += 1) px += 1;
200
- positions.set(room, { x: px, y });
201
- taken.add(px + "," + y);
202
- };
203
- let offsetX = 0;
204
- for (const start of [root, ...[...known].sort()]) {
205
- if (!known.has(start) || positions.has(start)) continue;
206
- place(start, offsetX, 0);
207
- const queue = [start];
208
- while (queue.length) {
209
- const room = queue.shift();
210
- const pos = positions.get(room);
211
- const dirs = state.exits.get(room);
212
- for (const direction of [...(dirs?.keys() ?? [])].sort()) {
213
- const target = dirs.get(direction);
214
- if (!known.has(target)) { hints.push({ from: room, direction }); continue; }
215
- const key = [room, target].sort().join("\0");
216
- if (!edgeKeys.has(key)) { edgeKeys.add(key); edges.push({ from: room, to: target, direction }); }
217
- if (positions.has(target)) continue;
218
- const d = DELTA[direction] ?? [0, 0];
219
- place(target, pos.x + d[0], pos.y + d[1]);
220
- queue.push(target);
221
- }
222
- }
223
- offsetX = Math.max(...[...positions.values()].map((p) => p.x)) + 2;
224
- }
225
- const levels = levelsOf(state, root);
226
- const xs = [...positions.values()].map((p) => p.x);
227
- const ys = [...positions.values()].map((p) => p.y);
228
- const minX = Math.min(0, ...xs);
229
- const minY = Math.min(0, ...ys);
230
- const nodes = [...known].filter((room) => positions.has(room)).sort().map((room) => {
231
- const p = positions.get(room);
232
- return { id: room, x: p.x - minX, y: p.y - minY, level: levels.has(room) ? levels.get(room) : null };
233
- });
234
- return { nodes, edges, hints };
180
+ return directedGridLayout(state, roomIds, { root, nudgeCollisions: true });
235
181
  }
236
182
 
237
183
  /** The directions `here` can still be dug in: every direction the room's kind
238
184
  * allows that has no written exit yet. Above ground there is nothing to
239
185
  * tunnel sideways THROUGH, so a garden digs straight down and no other way;
240
186
  * underground, all six are open soil. The engine is the authority on this —
241
- * the page prefers `tmctMud.diggableDirections` whenever the bundle exposes
187
+ * the page prefers `tmct.page.diggableDirections` whenever the bundle exposes
242
188
  * one and only falls back to the rule below. Pure, self-contained
243
189
  * (roomKindForRoom is spliced alongside it). */
244
190
  export function diggableDirections(rows, state, here) {
@@ -1217,12 +1163,24 @@ function pageScript() {
1217
1163
  "use strict";
1218
1164
  const DATA = MUD_PAGE_DATA;
1219
1165
  const createTicker = ${createTicker.toString()};
1166
+ const createSerialQueue = ${createSerialQueue.toString()};
1220
1167
  const escapeHtml = ${escapeHtml.toString()};
1221
1168
  const esc = escapeHtml;
1169
+ const rowsForWorld = ${rowsForWorld.toString()};
1170
+ const appendLogLine = ${appendLogLine.toString()};
1171
+ const fnv1a32 = ${fnv1a32.toString()};
1172
+ const predatorSubjects = ${predatorSubjects.toString()};
1222
1173
  const paneMarkup = ${paneMarkup.toString()};
1223
1174
  const speciesOfCharacter = ${speciesOfCharacter.toString()};
1224
1175
  const mudRoomSceneObjects = ${mudRoomSceneObjects.toString()};
1225
1176
  const carriedItemsFor = ${carriedItemsFor.toString()};
1177
+ // EXIT_DELTA is data, not a function — directedGridLayout/roomGraphSvg both
1178
+ // close over it as a module-level const, which a \`.toString()\` splice never
1179
+ // carries (a spliced function's source text is its own body only), so it
1180
+ // travels here as JSON instead.
1181
+ const EXIT_DELTA = ${JSON.stringify(EXIT_DELTA)};
1182
+ const directedGridLayout = ${directedGridLayout.toString()};
1183
+ const roomGraphSvg = ${roomGraphSvg.toString()};
1226
1184
  const levelsOf = ${levelsOf.toString()};
1227
1185
  const charactersInRoom = ${charactersInRoom.toString()};
1228
1186
  const burrowGraph = ${burrowGraph.toString()};
@@ -1282,12 +1240,8 @@ function pageScript() {
1282
1240
  // directly, so two characters' turns can never interleave their reads and
1283
1241
  // writes of the one shared memoryDir, and the global turn counter always
1284
1242
  // increments in real execution order.
1285
- let tickChain = Promise.resolve();
1286
- function serializeTick(fn) {
1287
- const run = tickChain.then(fn, fn);
1288
- tickChain = run.catch(function () {});
1289
- return run;
1290
- }
1243
+ let tickQueue = createSerialQueue();
1244
+ function serializeTick(fn) { return tickQueue.run(fn); }
1291
1245
 
1292
1246
  let session = null;
1293
1247
  let cast = [];
@@ -1385,17 +1339,15 @@ function pageScript() {
1385
1339
  // world's ending sentence comes from the engine either way, never a second
1386
1340
  // wording of the same event.
1387
1341
  appendChat(character, "a", note
1388
- || (window.tmctMud.outOfPlayPhrase(character, fate) + ". It takes no more turns."));
1342
+ || (window.tmct.page.outOfPlayPhrase(character, fate) + ". It takes no more turns."));
1389
1343
  playFateScene(character, fate);
1390
1344
  }
1391
1345
 
1392
1346
  // Whichever individual the WORLD marks dangerous, never a species this page
1393
- // hardcodes — the same fact adventure.mjs's own predator check reads.
1347
+ // hardcodes — the same domain/mud-facts.mjs reader mud-turn.mjs's own
1348
+ // predator check reads server-side.
1394
1349
  function predatorInRows(rows) {
1395
- for (const row of rows) {
1396
- if (row.predicate === "mgx:is-predator" && row.object === "true") return row.subject;
1397
- }
1398
- return null;
1350
+ return predatorSubjects(rows)[0] || null;
1399
1351
  }
1400
1352
 
1401
1353
  // The pane holds on the den for the length of one pounce before it grays out.
@@ -1420,7 +1372,7 @@ function pageScript() {
1420
1372
  const rows = lastSnapshot.rows;
1421
1373
  const state = lastSnapshot.state;
1422
1374
  renderRoomView(character, rows, state, den, allRoomIds(rows),
1423
- window.tmctMud.castInRoom(rows, state, den, character));
1375
+ window.tmct.page.castInRoom(rows, state, den, character));
1424
1376
 
1425
1377
  const room = el(w + "-room");
1426
1378
  const lunger = room.querySelector('.floor-others .sprite-card[data-subject="' + predator + '"]');
@@ -1478,23 +1430,7 @@ function pageScript() {
1478
1430
  // thing stays readable through the popup, and stays in the DOM for anything
1479
1431
  // reading the log.
1480
1432
  function appendChat(character, cls, text) {
1481
- const log = el(paneIdFor(character) + "-chatlog");
1482
- const d = document.createElement("div");
1483
- d.className = cls;
1484
- d.textContent = text;
1485
- log.appendChild(d);
1486
- // A line-clamped box reports scrollHeight EQUAL to its clamped height, so
1487
- // the overflow has to be read against the same box with the clamp lifted.
1488
- d.classList.add("unclamped");
1489
- const wholeHeight = d.scrollHeight;
1490
- d.classList.remove("unclamped");
1491
- if (wholeHeight - d.clientHeight > 2) {
1492
- d.classList.add("clipped");
1493
- d.setAttribute("role", "button");
1494
- d.setAttribute("tabindex", "0");
1495
- d.setAttribute("title", "read the whole line");
1496
- }
1497
- log.scrollTop = log.scrollHeight;
1433
+ appendLogLine(el(paneIdFor(character) + "-chatlog"), cls, text, { clip: true });
1498
1434
  }
1499
1435
 
1500
1436
  function openLogPopup(slot, text) {
@@ -1526,7 +1462,7 @@ function pageScript() {
1526
1462
  return { answer: "", end: false };
1527
1463
  });
1528
1464
  }
1529
- return serializeTick(function () { return session.windows[character].turn(line); }).then(async function (res) {
1465
+ return serializeTick(function () { return tmct.turn(line, { as: character }); }).then(async function (res) {
1530
1466
  appendChat(character, "a", res.answer);
1531
1467
  await broadcastLocalChange();
1532
1468
  renderSoon();
@@ -1616,14 +1552,12 @@ function pageScript() {
1616
1552
 
1617
1553
  // ---- room-view rendering -------------------------------------------------
1618
1554
  function hashOf(text) {
1619
- let h = 0;
1620
- for (let i = 0; i < String(text).length; i += 1) h = (h * 31 + String(text).charCodeAt(i)) % 100000;
1621
- return h;
1555
+ return fnv1a32(String(text));
1622
1556
  }
1623
1557
 
1624
1558
  function spriteSvgFor(species, rows, instanceKey) {
1625
- if (window.tmctMud && window.tmctMud.resolveSpriteAsset) {
1626
- return window.tmctMud.resolveSpriteAsset(species, rows, [], DATA.spriteTemplates, window.tmctMud.SPRITE_REGISTRY, { instanceKey: instanceKey });
1559
+ if (window.tmct && window.tmct.page.resolveSpriteAsset) {
1560
+ return window.tmct.page.resolveSpriteAsset(species, rows, [], DATA.spriteTemplates, window.tmct.page.SPRITE_REGISTRY, { instanceKey: instanceKey });
1627
1561
  }
1628
1562
  return "";
1629
1563
  }
@@ -1636,10 +1570,10 @@ function pageScript() {
1636
1570
  // "animal": everything hung on this wall is a thing, and a thing with no
1637
1571
  // sprite should read as a plain parcel rather than a creature.
1638
1572
  function objectSvgFor(subject, rows) {
1639
- if (window.tmctMud && window.tmctMud.resolveSpriteAsset) {
1640
- return window.tmctMud.resolveSpriteAsset(
1573
+ if (window.tmct && window.tmct.page.resolveSpriteAsset) {
1574
+ return window.tmct.page.resolveSpriteAsset(
1641
1575
  subject, spriteAncestryRows(rows, subject), factsForSubject(rows, subject),
1642
- DATA.spriteTemplates, window.tmctMud.SPRITE_REGISTRY,
1576
+ DATA.spriteTemplates, window.tmct.page.SPRITE_REGISTRY,
1643
1577
  { rootFallback: "portable", instanceKey: subject },
1644
1578
  );
1645
1579
  }
@@ -1802,7 +1736,7 @@ function pageScript() {
1802
1736
  // than the pane offers to talk to.
1803
1737
  function roomCaptionFor(rows, state, here, character, roomMates) {
1804
1738
  const parts = [];
1805
- const view = window.tmctMud.worldDigestRows(rows, state, character);
1739
+ const view = window.tmct.page.worldDigestRows(rows, state, character);
1806
1740
  const lines = view.filter(function (r) { return r.subject.toLowerCase() === here.toLowerCase(); })
1807
1741
  .map(function (r) { return r.subject + " " + r.predicate + " " + r.object + "."; });
1808
1742
  parts.push(lines.length ? lines.join(" ") : "You are in the " + here + ".");
@@ -1821,7 +1755,7 @@ function pageScript() {
1821
1755
  // direction with neither draws nothing, so the ring never offers a command
1822
1756
  // the world would refuse.
1823
1757
  function diggableFor(rows, state, here) {
1824
- const fromEngine = window.tmctMud && window.tmctMud.diggableDirections;
1758
+ const fromEngine = window.tmct && window.tmct.page.diggableDirections;
1825
1759
  return fromEngine ? fromEngine(rows, state, here) : diggableDirections(rows, state, here);
1826
1760
  }
1827
1761
 
@@ -1848,7 +1782,7 @@ function pageScript() {
1848
1782
  // it. Exits appear in both the row and the compass ring on purpose: the ring
1849
1783
  // says WHERE a way out points, the row says what it is called.
1850
1784
  function affordancesFor(rows, state, here, character) {
1851
- const fromEngine = window.tmctMud && window.tmctMud.roomAffordances;
1785
+ const fromEngine = window.tmct && window.tmct.page.roomAffordances;
1852
1786
  return fromEngine ? fromEngine(rows, state, here, character) : [];
1853
1787
  }
1854
1788
 
@@ -1904,7 +1838,7 @@ function pageScript() {
1904
1838
  // can say "carrot" while every verb still resolves the distinct id
1905
1839
  // ("carrot-1"). itemLabel is the fallback for an id carrying no such fact.
1906
1840
  function labelForItem(rows, subject, roomIds) {
1907
- const fromEngine = window.tmctMud && window.tmctMud.displayNameOf;
1841
+ const fromEngine = window.tmct && window.tmct.page.displayNameOf;
1908
1842
  const name = fromEngine ? fromEngine(rows, subject) : null;
1909
1843
  return name && name !== subject ? name : itemLabel(subject, roomIds);
1910
1844
  }
@@ -1912,75 +1846,12 @@ function pageScript() {
1912
1846
  // ---- the burrow survey ---------------------------------------------------
1913
1847
  // One routine, drawn twice: the omniscient board in the top row (every room
1914
1848
  // the world has) and each pane's own known ground (only what that character
1915
- // has dug or walked, with a fading stub where it knows a way carries on).
1849
+ // has dug or walked, with a fading stub where it knows a way carries on). A
1850
+ // thin wrapper over the shared roomGraphSvg, fixed to the burrow's own
1851
+ // "turf" treatment (ground line, tunnels/shafts, per-room occupant dots)
1852
+ // and its own "burrow" wrapper class.
1916
1853
  function burrowSvg(graph, options) {
1917
- if (!graph.nodes.length) return "";
1918
- const opts = options || {};
1919
- const compact = !!opts.compact;
1920
- const cellX = compact ? 44 : 76, cellY = compact ? 26 : 54;
1921
- const roomW = compact ? 34 : 62, roomH = compact ? 14 : 26;
1922
- const maxX = Math.max.apply(null, graph.nodes.map(function (n) { return n.x; }));
1923
- const maxY = Math.max.apply(null, graph.nodes.map(function (n) { return n.y; }));
1924
- const w = (maxX + 1) * cellX, h = (maxY + 1) * cellY;
1925
- const byRoom = {};
1926
- for (const n of graph.nodes) byRoom[n.id] = n;
1927
- const cx = function (n) { return (n.x + 0.5) * cellX; };
1928
- const cy = function (n) { return (n.y + 0.5) * cellY; };
1929
-
1930
- // The turf band is only drawn when the layout genuinely separates the
1931
- // surface from the soil — a lateral dig can push a surface room onto a
1932
- // lower row, and a ground line above it would say something false. The
1933
- // room fills carry the same information either way, which is all the
1934
- // thumbnail-sized copy has room for.
1935
- const surfaceRows = graph.nodes.filter(function (n) { return n.level === 0; }).map(function (n) { return n.y; });
1936
- const soilRows = graph.nodes.filter(function (n) { return n.level !== 0; }).map(function (n) { return n.y; });
1937
- let turf = "";
1938
- if (!compact && surfaceRows.length && soilRows.length && Math.max.apply(null, surfaceRows) < Math.min.apply(null, soilRows)) {
1939
- const groundY = (Math.max.apply(null, surfaceRows) + 0.5) * cellY + roomH / 2 + (compact ? 3 : 7);
1940
- turf = '<rect class="turf" x="0" y="0" width="' + w + '" height="' + groundY + '"></rect>'
1941
- + '<line class="ground-line" x1="0" y1="' + groundY + '" x2="' + w + '" y2="' + groundY + '"></line>';
1942
- }
1943
-
1944
- const tunnels = graph.edges.map(function (e) {
1945
- const a = byRoom[e.from], b = byRoom[e.to];
1946
- if (!a || !b) return "";
1947
- const shaft = e.direction === "up" || e.direction === "down" ? " shaft" : "";
1948
- return '<line class="tunnel' + shaft + '" x1="' + cx(a) + '" y1="' + cy(a) + '" x2="' + cx(b) + '" y2="' + cy(b) + '"></line>';
1949
- }).join("");
1950
-
1951
- const HINT_DELTA = { north: [0, -1], south: [0, 1], east: [1, 0], west: [-1, 0], up: [0, -1], down: [0, 1] };
1952
- const hints = graph.hints.map(function (hi) {
1953
- const from = byRoom[hi.from];
1954
- if (!from) return "";
1955
- const d = HINT_DELTA[hi.direction] || [0, 0];
1956
- return '<circle class="hint" cx="' + (cx(from) + d[0] * cellX * 0.36) + '" cy="' + (cy(from) + d[1] * cellY * 0.36) + '" r="' + (compact ? 2 : 3.2) + '"></circle>';
1957
- }).join("");
1958
-
1959
- const occupants = opts.occupants || {};
1960
- const rooms = graph.nodes.map(function (n) {
1961
- const here = opts.here === n.id ? " here" : "";
1962
- const fresh = opts.fresh === n.id ? " freshly-dug" : "";
1963
- const surface = n.level === 0 ? " surface" : "";
1964
- const label = esc(n.id);
1965
- const fit = label.length * (compact ? 3.6 : 4.3) > roomW - 6
1966
- ? ' textLength="' + (roomW - 6) + '" lengthAdjust="spacingAndGlyphs"' : "";
1967
- const cast = occupants[n.id] || [];
1968
- // The room's own width is the budget: a dozen animals down one hole must
1969
- // still read as a dozen dots inside that room rather than a line running
1970
- // out of it, so the gap closes as the crowd grows.
1971
- const step = Math.min(compact ? 6 : 9, (roomW - 6) / Math.max(1, cast.length));
1972
- const dots = cast.map(function (c, i) {
1973
- const spread = (i - (cast.length - 1) / 2) * step;
1974
- return '<circle class="occupant" cx="' + (cx(n) + spread) + '" cy="' + (cy(n) + roomH / 2) + '" r="' + (compact ? 2.4 : 3.6)
1975
- + '" fill="' + c.color + '"><title>' + esc(c.character) + "</title></circle>";
1976
- }).join("");
1977
- return '<g class="room' + surface + here + fresh + '" data-room="' + label + '"><rect x="' + (cx(n) - roomW / 2) + '" y="' + (cy(n) - roomH / 2)
1978
- + '" width="' + roomW + '" height="' + roomH + '" rx="3"></rect><text x="' + cx(n) + '" y="' + (cy(n) + (compact ? 2 : 2.5))
1979
- + '"' + fit + ">" + label + "</text>" + dots + "</g>";
1980
- }).join("");
1981
-
1982
- return '<div class="burrow"><svg viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="xMidYMid meet" role="img" aria-label="'
1983
- + esc(opts.label || "the burrow") + '">' + turf + tunnels + hints + rooms + "</svg></div>";
1854
+ return roomGraphSvg(graph, Object.assign({ turf: true, wrapClass: "burrow" }, options || {}));
1984
1855
  }
1985
1856
 
1986
1857
  function renderMinimap(character, rows, state, here) {
@@ -2007,7 +1878,7 @@ function pageScript() {
2007
1878
  // the survey they annotate, and each one's own dot already carries its name.
2008
1879
  const keys = cast.map(function (c) {
2009
1880
  const room = state.placements.get(c) ? state.placements.get(c).object : "?";
2010
- const fate = window.tmctMud.outOfPlayReasonOf(state, c);
1881
+ const fate = window.tmct.page.outOfPlayReasonOf(state, c);
2011
1882
  const where = fate ? fate : "in " + esc(room);
2012
1883
  return '<span class="key-actor"><span class="key-dot" style="background:' + colorFor(c) + '"></span>'
2013
1884
  + esc(speciesOfCharacter(c)) + " " + where + "</span>";
@@ -2068,7 +1939,7 @@ function pageScript() {
2068
1939
  }
2069
1940
  }
2070
1941
  for (const character of everyone()) {
2071
- const reason = window.tmctMud.outOfPlayReasonOf(snap.state, character);
1942
+ const reason = window.tmct.page.outOfPlayReasonOf(snap.state, character);
2072
1943
  if (reason) markOutOfPlay(character, reason, null);
2073
1944
  }
2074
1945
  renderWorldMap(snap.rows, snap.state, roomIds);
@@ -2083,7 +1954,7 @@ function pageScript() {
2083
1954
  const place = snap.state.placements.get(character);
2084
1955
  const here = place ? place.object : null;
2085
1956
  if (!here) continue;
2086
- const roomMates = window.tmctMud.castInRoom(snap.rows, snap.state, here, character);
1957
+ const roomMates = window.tmct.page.castInRoom(snap.rows, snap.state, here, character);
2087
1958
  renderRoomView(character, snap.rows, snap.state, here, roomIds, roomMates);
2088
1959
  renderDirections(character, snap.rows, snap.state, here);
2089
1960
  renderChatPills(character, snap.rows, snap.state, here, roomIds);
@@ -2117,10 +1988,7 @@ function pageScript() {
2117
1988
  let editing = false;
2118
1989
 
2119
1990
  function worldOnlyRows(rows) {
2120
- const prefix = "world:" + scenario().worldPayload.name;
2121
- return (rows || []).filter(function (r) {
2122
- return typeof r.provenance === "string" && r.provenance.indexOf(prefix) === 0;
2123
- });
1991
+ return rowsForWorld(rows, scenario().worldPayload.name);
2124
1992
  }
2125
1993
 
2126
1994
  function renderEditMap() {
@@ -2204,9 +2072,9 @@ function pageScript() {
2204
2072
  function renderSuggestionPills() {
2205
2073
  const box = el("editorPills");
2206
2074
  const term = wordBeforeCursor(el("editorText").value, el("editorText").selectionStart);
2207
- if (!term || !window.tmctMud) { box.innerHTML = ""; return; }
2208
- const related = window.tmctMud.relatedForTerm ? window.tmctMud.relatedForTerm(allStoreRows, term) : null;
2209
- const chain = window.tmctMud.classAncestorChain ? window.tmctMud.classAncestorChain(term, allStoreRows) : [];
2075
+ if (!term || !window.tmct) { box.innerHTML = ""; return; }
2076
+ const related = window.tmct.page.relatedForTerm ? window.tmct.page.relatedForTerm(allStoreRows, term) : null;
2077
+ const chain = window.tmct.page.classAncestorChain ? window.tmct.page.classAncestorChain(term, allStoreRows) : [];
2210
2078
  const seen = { };
2211
2079
  seen[term] = true;
2212
2080
  const out = [];
@@ -2245,7 +2113,7 @@ function pageScript() {
2245
2113
  const snap = await session.snapshot();
2246
2114
  allStoreRows = snap.rows;
2247
2115
  editRows = worldOnlyRows(snap.rows);
2248
- editState = window.tmctMud.foldWorldState(window.tmctMud.worldActionRows(editRows));
2116
+ editState = window.tmct.page.foldWorldState(window.tmct.page.worldActionRows(editRows));
2249
2117
  if (result.unrecognized.length) {
2250
2118
  const lines = result.unrecognized.map(function (u) { return u.line; }).join(", ");
2251
2119
  status.className = "edit-status pending";
@@ -2301,7 +2169,7 @@ function pageScript() {
2301
2169
  const snap = await session.snapshot();
2302
2170
  allStoreRows = snap.rows;
2303
2171
  editRows = worldOnlyRows(snap.rows);
2304
- editState = window.tmctMud.foldWorldState(window.tmctMud.worldActionRows(editRows));
2172
+ editState = window.tmct.page.foldWorldState(window.tmct.page.worldActionRows(editRows));
2305
2173
  el("editorText").value = renderMudEditorText(editRows, editState);
2306
2174
  el("editorStatus").className = "edit-status";
2307
2175
  el("editorStatus").textContent = "";
@@ -2381,8 +2249,8 @@ function pageScript() {
2381
2249
  // engine calls live world state, plus the P2P layer's own four predicates.
2382
2250
  // The check is handed over rather than imported, so the sync filter never
2383
2251
  // learns the world engine's private predicate names.
2384
- const extraPredicates = (window.tmctMud.P2P_PREDICATES || []).slice();
2385
- const isState = window.tmctMud.isMudStatePredicate;
2252
+ const extraPredicates = (window.tmct.page.P2P_PREDICATES || []).slice();
2253
+ const isState = window.tmct.page.isMudStatePredicate;
2386
2254
  room = mod.createP2pRoom({
2387
2255
  memoryDir: session.memoryDir,
2388
2256
  myPeerId: myPeerId,
@@ -2824,7 +2692,7 @@ function pageScript() {
2824
2692
  const wasEditing = editing;
2825
2693
  if (wasEditing) await exitEditMode();
2826
2694
  scenarioIndex = picked;
2827
- await boot("a different burrow opened \\u2014 share again to link up.");
2695
+ await boot("a different burrow opened \\u2014 still linked.");
2828
2696
  if (wasEditing) await enterEditMode();
2829
2697
  });
2830
2698
  }
@@ -2891,13 +2759,20 @@ function pageScript() {
2891
2759
  async function openWorld(note) {
2892
2760
  const seq = bootSeq += 1;
2893
2761
  autoOn = false;
2894
- // A room is bound to the store it was opened over, and recasting mints a
2895
- // brand new one so a burrow that restarts leaves the link behind rather
2896
- // than quietly syncing a world nobody else is in. Saying so is the whole
2897
- // handling; sharing again opens a fresh link over the new world. Picking a
2898
- // different burrow is the same drop for the same reason, and says so in
2899
- // its own words.
2900
- dropRoom(note || "the burrow restarted \\u2014 share again to link up.");
2762
+ // A live link survives a recast: once the new burrow's store is open the
2763
+ // room re-binds to it (below), pushing the fresh world to every connected
2764
+ // node instead of quietly abandoning them. The recast moves the world one
2765
+ // epoch forward, read from the store the peers have all converged on, so
2766
+ // nobody's leftover snapshots from the old run can outrank the new one.
2767
+ const liveRoom = room;
2768
+ let nextEpoch = 0;
2769
+ if (liveRoom && session) {
2770
+ try {
2771
+ nextEpoch = (await session.snapshot()).state.epoch + 1;
2772
+ } catch (err) {
2773
+ nextEpoch = 1;
2774
+ }
2775
+ }
2901
2776
  claimedElsewhere = {};
2902
2777
  wavingNow = [];
2903
2778
  const playBtn = el("autoToggle");
@@ -2909,7 +2784,7 @@ function pageScript() {
2909
2784
  freshlyDugRoom = null;
2910
2785
  lastSnapshot = null;
2911
2786
  speechBubbles.clear();
2912
- tickChain = Promise.resolve();
2787
+ tickQueue = createSerialQueue();
2913
2788
  // Rebuilt rather than pruned: a character that had a pane last boot can be
2914
2789
  // an npc in this one, and a stale seat would send its ending to a pane now
2915
2790
  // showing somebody else.
@@ -2928,18 +2803,37 @@ function pageScript() {
2928
2803
  // so the roster belongs to the scenario rather than to the page.
2929
2804
  roster = rosterOf(scenario());
2930
2805
  rootRoom = rootRoomOf(scenario());
2931
- cast = window.tmctMud.pickMudRoster(roster, { count: chosenPlayerCount() });
2932
- const pool = window.tmctMud.expandMudRoster(roster, cast.length + npcCount)
2806
+ cast = window.tmct.page.pickMudRoster(roster, { count: chosenPlayerCount() });
2807
+ const pool = window.tmct.page.expandMudRoster(roster, cast.length + npcCount)
2933
2808
  .filter(function (id) { return cast.indexOf(id) === -1; });
2934
- npcs = window.tmctMud.pickMudRoster(pool, { count: npcCount });
2809
+ npcs = window.tmct.page.pickMudRoster(pool, { count: npcCount });
2935
2810
  slots = DATA.paneSlots.slice(0, cast.length);
2936
2811
  showPlayerCount(cast.length);
2937
2812
  showNpcCount(npcs.length);
2938
2813
  renderStage();
2939
- const opened = await window.tmctMud.createMudSession(scenario().worldPayload, { characters: everyone() });
2814
+ // slotOf has to match cast from the same synchronous stretch that just
2815
+ // rebuilt the DOM for it \\u2014 a render triggered anywhere in the awaits
2816
+ // below (rebind's own re-sync can trigger one) must never see this
2817
+ // scenario's cast paired with the LAST scenario's pane ids, which is what
2818
+ // a null pane-element lookup in renderAll means when it happens.
2819
+ for (let i = 0; i < cast.length; i += 1) slotOf[cast[i]] = slots[i];
2820
+ const opened = await window.tmct.open(scenario().worldPayload, { characters: everyone(), epoch: nextEpoch });
2940
2821
  if (seq !== bootSeq) return;
2941
2822
  session = opened;
2942
- for (let i = 0; i < cast.length; i += 1) slotOf[cast[i]] = slots[i];
2823
+ if (liveRoom) {
2824
+ try {
2825
+ await liveRoom.rebind({ memoryDir: opened.memoryDir, worldName: worldName, myDisplayName: myDisplayName });
2826
+ renderWire();
2827
+ renderNodes();
2828
+ // After renderWire, the same way dropRoom writes its own reason: the
2829
+ // render restates the state's stock note, and WHY this recast kept
2830
+ // the link is the thing worth saying over it.
2831
+ el("wireStateNote").textContent = note || "the burrow recast \\u2014 still linked.";
2832
+ await claimMyAnimals();
2833
+ } catch (err) {
2834
+ dropRoom("the link didn't survive the recast \\u2014 share again to link up.");
2835
+ }
2836
+ }
2943
2837
  bindPanes();
2944
2838
  for (const character of cast) {
2945
2839
  const w = paneIdFor(character);