@polycode-projects/the-mechanical-code-talker 4.1.0 → 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -18
- package/bin/tmct.mjs +3 -0
- package/data/templates/responses.jsonl +3 -0
- package/package.json +2 -1
- package/src/adapters/memory/core.mjs +1358 -196
- package/src/adapters/memory/inspect.mjs +11 -0
- package/src/adapters/memory/shacl.mjs +38 -0
- package/src/adapters/p2p/webrtc-transport.mjs +28 -5
- package/src/domain/ask-vocab.mjs +39 -0
- package/src/domain/ask.mjs +183 -34
- package/src/domain/grammar/assert.mjs +8 -2
- package/src/domain/hanoi-board.mjs +232 -0
- package/src/domain/ingest-facts.mjs +120 -0
- package/src/domain/interpret/normalize.mjs +49 -0
- package/src/domain/memory/compaction.mjs +284 -0
- package/src/domain/memory/resolution.mjs +171 -0
- package/src/domain/memory/trust.mjs +175 -5
- package/src/domain/memory-facts.mjs +139 -0
- package/src/domain/p2p/facts.mjs +21 -0
- package/src/domain/p2p/peer-id.mjs +15 -0
- package/src/domain/p2p/provenance-relabel.mjs +13 -2
- package/src/domain/p2p/sync-filter.mjs +5 -1
- package/src/domain/p2p/wire.mjs +7 -4
- package/src/domain/scene-compose.mjs +2 -2
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +1 -1
- package/src/services/adventure-viz.mjs +43 -39
- package/src/services/adventure.mjs +70 -44
- package/src/services/chat-page-viz.mjs +403 -332
- package/src/services/chat.mjs +274 -156
- package/src/services/code-explorer-viz.mjs +142 -55
- package/src/services/index.mjs +1 -1
- package/src/services/ingest-viz.mjs +153 -28
- package/src/services/ledger-viz.mjs +47 -47
- package/src/services/memory-panel-viz.mjs +8 -3
- package/src/services/mud-turn.mjs +11 -8
- package/src/services/mud-viz.mjs +474 -234
- package/src/services/p2p-room.mjs +110 -23
- package/src/services/plan-viz.mjs +72 -13
- package/src/services/research-viz.mjs +35 -24
- package/src/services/share-overlay-viz.mjs +623 -0
- package/src/services/spider-fly-viz.mjs +24 -24
- package/src/services/sprite-catalog-viz.mjs +307 -82
- package/src/surfaces/web/adventure-browser-entry.mjs +46 -25
- package/src/surfaces/web/chat-browser-entry.mjs +55 -9
- package/src/surfaces/web/code-explorer-browser-entry.mjs +30 -16
- package/src/surfaces/web/engine-surface.mjs +82 -0
- package/src/surfaces/web/ingest-browser-entry.mjs +81 -11
- package/src/surfaces/web/ledger-browser-entry.mjs +47 -14
- package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +149 -116
- package/src/surfaces/web/mud-browser-entry.mjs +77 -25
- package/src/surfaces/web/p2p-browser-entry.mjs +1 -1
- package/src/surfaces/web/plan-browser-entry.mjs +49 -11
- package/src/surfaces/web/research-browser-entry.mjs +33 -24
- package/src/surfaces/web/spider-fly-browser-entry.mjs +32 -13
- package/src/surfaces/web/sprites-browser-entry.mjs +51 -11
- package/src/surfaces/web/tmct-surface.mjs +159 -0
- package/src/surfaces/web/turn-session.mjs +16 -5
- package/src/tools/server.mjs +13 -6
|
@@ -42,17 +42,17 @@
|
|
|
42
42
|
//
|
|
43
43
|
// Four further pure helpers are exported for testing but NOT spliced,
|
|
44
44
|
// because each calls another module's export — the in-page script instead
|
|
45
|
-
// reaches through the browser bundle's own `
|
|
46
|
-
// how the inline script calls
|
|
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
47
|
// spider-fly-world.mjs): `roomCaptionText` (calls `worldDigestRows`; the
|
|
48
|
-
// in-page `captionFor` mirrors it against `
|
|
48
|
+
// in-page `captionFor` mirrors it against `tmct.page.worldDigestRows`),
|
|
49
49
|
// `pillsForRoom` (a thin wrapper over adventure.mjs's own exported
|
|
50
50
|
// `roomAffordances`, whose header explains why its list can never promise an
|
|
51
51
|
// action one of take/open/talk/examine would then refuse; the in-page
|
|
52
|
-
// `pillsFor` mirrors it against `
|
|
52
|
+
// `pillsFor` mirrors it against `tmct.page.roomAffordances`),
|
|
53
53
|
// `goalStatusLines` (calls `foldWorldState` and adventure-autoplay.mjs's own
|
|
54
54
|
// `exposedFacts`; the in-page `goalStatusLinesFor` mirrors both against the
|
|
55
|
-
// `
|
|
55
|
+
// `tmct` surface too), and `visitedRoomGraph` (a directions-only
|
|
56
56
|
// layout of the rooms a session has actually visited, now a thin wrapper over
|
|
57
57
|
// viz-room-graph.mjs's shared `directedGridLayout` — mud-viz.mjs's own
|
|
58
58
|
// burrowGraph wrote the same BFS-grid layout a second time under its own
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
// header for the algorithm and what `hints`/disconnected components mean).
|
|
61
61
|
// The in-page script never splices `directedGridLayout`/`roomGraphSvg`
|
|
62
62
|
// either, for the same not-self-contained reason: it calls
|
|
63
|
-
// `
|
|
63
|
+
// `tmct.page.directedGridLayout`/`tmct.page.roomGraphSvg` straight
|
|
64
64
|
// through the bundle instead of re-implementing the room map a second time.
|
|
65
65
|
//
|
|
66
66
|
// The chat dock (chatlog/chatform/chatq/pills, below) mirrors
|
|
@@ -479,8 +479,8 @@ export function groundedPlaceholder(actions, fallback) {
|
|
|
479
479
|
* row. `[]` on an honest miss (neither lookup returns anything) — never a
|
|
480
480
|
* fabricated suggestion. NOT .toString()-splice-safe (it calls two other
|
|
481
481
|
* modules' exports) — the in-page script mirrors this same combination
|
|
482
|
-
* against the browser bundle's own `
|
|
483
|
-
* `
|
|
482
|
+
* against the browser bundle's own `tmct.page.relatedForTerm`/
|
|
483
|
+
* `tmct.page.classAncestorChain`, the same reach-through-the-global
|
|
484
484
|
* pattern `pillsFor`/`captionFor` already use for their own adventure.mjs
|
|
485
485
|
* calls (see this module's header). */
|
|
486
486
|
export function suggestionsForTerm(rows, term) {
|
|
@@ -1199,7 +1199,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1199
1199
|
let allStoreRows = [];
|
|
1200
1200
|
|
|
1201
1201
|
// ---- persistence — the manor remembers your visit, on this device -------
|
|
1202
|
-
// Best-effort IndexedDB (
|
|
1202
|
+
// Best-effort IndexedDB (tmct.page.openPersistedStore): after any
|
|
1203
1203
|
// state-changing redraw, the whole Backend-B payload plus the visited-room
|
|
1204
1204
|
// exposure set snapshots under the "adventure" key, debounced. Restored on
|
|
1205
1205
|
// boot; reset clears it and starts the world over. The home page's preview
|
|
@@ -1208,6 +1208,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1208
1208
|
let persist = null;
|
|
1209
1209
|
let persistSaveTimer = null;
|
|
1210
1210
|
window.tmctAdventureLastSave = null;
|
|
1211
|
+
window.tmct.lastSave = null;
|
|
1211
1212
|
|
|
1212
1213
|
// The deploy's own version, read off the service worker file the build
|
|
1213
1214
|
// already stamps (its cache name embeds package.json's version) — the only
|
|
@@ -1238,7 +1239,10 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1238
1239
|
}
|
|
1239
1240
|
const visitedRoomIds = lastSnapshot ? lastSnapshot.visitedRoomIds : [];
|
|
1240
1241
|
persist.save({ memoryPayload: memoryPayload, visitedRoomIds: visitedRoomIds }).then((saved) => {
|
|
1241
|
-
if (saved)
|
|
1242
|
+
if (saved) {
|
|
1243
|
+
window.tmctAdventureLastSave = { at: Date.now(), ms: Math.round(performance.now() - started) };
|
|
1244
|
+
window.tmct.lastSave = window.tmctAdventureLastSave;
|
|
1245
|
+
}
|
|
1242
1246
|
});
|
|
1243
1247
|
}, 500);
|
|
1244
1248
|
}
|
|
@@ -1297,22 +1301,22 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1297
1301
|
|
|
1298
1302
|
function captionFor(rows, state, here) {
|
|
1299
1303
|
const hereCased = here.charAt(0).toUpperCase() + here.slice(1);
|
|
1300
|
-
const lines =
|
|
1304
|
+
const lines = tmct.page.worldDigestRows(rows, state)
|
|
1301
1305
|
.filter((row) => row.subject !== "Player" && (row.object === here || row.subject === hereCased))
|
|
1302
1306
|
.map((row) => row.subject + " " + row.predicate + " " + row.object + ".");
|
|
1303
1307
|
return lines.length ? lines.join(" ") : "Nothing more about the " + here + " is written down yet.";
|
|
1304
1308
|
}
|
|
1305
1309
|
|
|
1306
1310
|
// ---- goal status — mirrors adventure-viz.mjs's own goalStatusLines
|
|
1307
|
-
// against
|
|
1311
|
+
// against tmct.page.foldWorldState/tmct.page.exposedFacts (the
|
|
1308
1312
|
// same mirroring captionFor/pillsFor already do against their own
|
|
1309
1313
|
// adventure.mjs exports), so a location is never claimed before the room
|
|
1310
1314
|
// holding it has actually been visited this session.
|
|
1311
1315
|
function goalStatusLinesFor(rows, state, visitedRoomIds) {
|
|
1312
1316
|
const ids = Array.from(new Set(rows.filter((r) => r.predicate === "mgx:is-objective" && r.object === "true").map((r) => r.subject)));
|
|
1313
1317
|
const carriedIds = new Set(carriedItems(rows, state, ACTING_SUBJECT).map((o) => o.subject));
|
|
1314
|
-
const exposedRows =
|
|
1315
|
-
const exposedState =
|
|
1318
|
+
const exposedRows = tmct.page.exposedFacts(rows, visitedRoomIds);
|
|
1319
|
+
const exposedState = tmct.page.foldWorldState(exposedRows);
|
|
1316
1320
|
return ids.map((id) => {
|
|
1317
1321
|
if (carriedIds.has(id)) return { subject: id, status: "carried", text: "carrying the " + id + " \\u2014 the adventure is won." };
|
|
1318
1322
|
const room = visibleRoomOf(exposedRows, exposedState, id, ACTING_SUBJECT);
|
|
@@ -1340,21 +1344,21 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1340
1344
|
// cursor per node; play mode's own map stays purely informational.
|
|
1341
1345
|
// roomGraphSvgFor mirrors the OLD inline roomMapSvg's own board-game sizing
|
|
1342
1346
|
// (a 64px square cell, 56x26px room footprints) through the shared
|
|
1343
|
-
// viz-room-graph.mjs renderer, reached via the
|
|
1347
|
+
// viz-room-graph.mjs renderer, reached via the tmct surface the
|
|
1344
1348
|
// same way captionFor/pillsFor already reach their own adventure.mjs
|
|
1345
1349
|
// calls: roomGraphSvg needs escapeHtml and a module-level exit-delta table
|
|
1346
1350
|
// this splice-safe script carries neither of, so it runs through the
|
|
1347
1351
|
// bundle rather than being spliced as text (see this page's own module
|
|
1348
1352
|
// header). visitedRoomGraphFor is the same posture for the layout half.
|
|
1349
1353
|
function roomGraphSvgFor(graph, clickable) {
|
|
1350
|
-
return
|
|
1354
|
+
return tmct.page.roomGraphSvg(graph, {
|
|
1351
1355
|
cellX: 64, cellY: 64, roomW: 56, roomH: 26,
|
|
1352
1356
|
clickable, selectedRoomId,
|
|
1353
1357
|
label: clickable ? "the whole manor \\u2014 click a room to inspect it" : "the rooms visited so far",
|
|
1354
1358
|
});
|
|
1355
1359
|
}
|
|
1356
1360
|
function visitedRoomGraphFor(state, visitedIds) {
|
|
1357
|
-
return
|
|
1361
|
+
return tmct.page.directedGridLayout(state, visitedIds, { actingSubject: ACTING_SUBJECT });
|
|
1358
1362
|
}
|
|
1359
1363
|
function renderRoomMap(rows, state, visitedRoomIds) {
|
|
1360
1364
|
mapWrapEl.innerHTML = roomGraphSvgFor(visitedRoomGraphFor(state, visitedRoomIds), false) || '<span class="empty-note">nowhere yet</span>';
|
|
@@ -1417,7 +1421,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1417
1421
|
addChatLine("u", esc(line));
|
|
1418
1422
|
addObjDockLine("u", esc(line));
|
|
1419
1423
|
return withLock(async () => {
|
|
1420
|
-
const result = await
|
|
1424
|
+
const result = await tmct.turn(line);
|
|
1421
1425
|
addChatLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
|
|
1422
1426
|
addObjDockLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
|
|
1423
1427
|
const snap = await session.snapshot();
|
|
@@ -1442,7 +1446,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1442
1446
|
// nothing, so it needs no main-transcript echo — it IS the board's own
|
|
1443
1447
|
// content); pill/typed turns below do echo, since they can change state.
|
|
1444
1448
|
await withLock(async () => {
|
|
1445
|
-
const result = await
|
|
1449
|
+
const result = await tmct.turn("look " + subject);
|
|
1446
1450
|
objLookEl.textContent = result.answer;
|
|
1447
1451
|
});
|
|
1448
1452
|
}
|
|
@@ -1505,8 +1509,8 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1505
1509
|
|
|
1506
1510
|
// ---- contextual pills — refreshed every redraw from the CURRENT room's
|
|
1507
1511
|
// own roomAffordances-derived list (mirroring pillsForRoom against
|
|
1508
|
-
//
|
|
1509
|
-
// roomCaptionText against
|
|
1512
|
+
// tmct.page.roomAffordances, the same way captionFor above mirrors
|
|
1513
|
+
// roomCaptionText against tmct.page.worldDigestRows), so a pill for a
|
|
1510
1514
|
// room the player has left, or an object already taken, never lingers.
|
|
1511
1515
|
// Clicking one inserts its exact command text into the input and focuses
|
|
1512
1516
|
// it; it never auto-submits, so free typing still works and a clicked
|
|
@@ -1517,7 +1521,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1517
1521
|
// ever wants to edit before running, and drawing it in both places would
|
|
1518
1522
|
// make one control a suggestion and its twin an action.
|
|
1519
1523
|
function pillsFor(rows, state, here) {
|
|
1520
|
-
return
|
|
1524
|
+
return tmct.page.roomAffordances(rows, state, here);
|
|
1521
1525
|
}
|
|
1522
1526
|
function renderPills(rows, state, here) {
|
|
1523
1527
|
const actions = pillsFor(rows, state, here).filter((a) => a.indexOf("go ") !== 0);
|
|
@@ -1560,10 +1564,10 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1560
1564
|
const ROOT_FALLBACK_BY_CLASS = { adventurer: "person", person: "person", room: "room" };
|
|
1561
1565
|
const rootFallbackFor = (cls) => ROOT_FALLBACK_BY_CLASS[cls] || "portable";
|
|
1562
1566
|
function resolveObjectSprite(rows, s) {
|
|
1563
|
-
if (s.subject === "you") return
|
|
1564
|
-
return
|
|
1567
|
+
if (s.subject === "you") return tmct.page.resolveSpriteAsset("adventurer", [], [], activeSpriteTemplates, tmct.page.SPRITE_REGISTRY, { rootFallback: "person", instanceKey: "you" });
|
|
1568
|
+
return tmct.page.resolveSpriteAsset(
|
|
1565
1569
|
s.subject, spriteAncestryRows(rows, s.subject), factsForSubject(rows, s.subject),
|
|
1566
|
-
activeSpriteTemplates,
|
|
1570
|
+
activeSpriteTemplates, tmct.page.SPRITE_REGISTRY,
|
|
1567
1571
|
{ rootFallback: rootFallbackFor(s.spriteClass), instanceKey: s.subject },
|
|
1568
1572
|
);
|
|
1569
1573
|
}
|
|
@@ -1574,9 +1578,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1574
1578
|
// everything else falls back through room's own ancestor chain to the
|
|
1575
1579
|
// generic room icon.
|
|
1576
1580
|
function roomKindIconSvg(rows, here) {
|
|
1577
|
-
return
|
|
1581
|
+
return tmct.page.resolveSpriteAsset(
|
|
1578
1582
|
here, spriteAncestryRows(rows, here), factsForSubject(rows, here),
|
|
1579
|
-
activeSpriteTemplates,
|
|
1583
|
+
activeSpriteTemplates, tmct.page.SPRITE_REGISTRY,
|
|
1580
1584
|
{ rootFallback: "room", instanceKey: "roomkind-" + here },
|
|
1581
1585
|
);
|
|
1582
1586
|
}
|
|
@@ -1641,7 +1645,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1641
1645
|
ticker.pause();
|
|
1642
1646
|
addChatLine("u", esc(line));
|
|
1643
1647
|
return withLock(async () => {
|
|
1644
|
-
const result = await
|
|
1648
|
+
const result = await tmct.turn(line);
|
|
1645
1649
|
addChatLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
|
|
1646
1650
|
const snap = await session.snapshot();
|
|
1647
1651
|
redraw(snap);
|
|
@@ -1703,9 +1707,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1703
1707
|
const classes = new Set(["adventurer"]);
|
|
1704
1708
|
subjects.forEach((s) => { if (s !== ACTING_SUBJECT) classes.add(spriteClassForObject(rows, s)); });
|
|
1705
1709
|
legendListEl.innerHTML = Array.from(classes).sort().map((cls) => {
|
|
1706
|
-
const svg =
|
|
1710
|
+
const svg = tmct.page.resolveSpriteAsset(
|
|
1707
1711
|
cls, spriteAncestryRows(rows, cls), factsForSubject(rows, cls),
|
|
1708
|
-
activeSpriteTemplates,
|
|
1712
|
+
activeSpriteTemplates, tmct.page.SPRITE_REGISTRY,
|
|
1709
1713
|
{ rootFallback: rootFallbackFor(cls), instanceKey: "legend-" + cls },
|
|
1710
1714
|
);
|
|
1711
1715
|
return spriteCardHtml(cls, cls, svg);
|
|
@@ -1724,8 +1728,8 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1724
1728
|
}
|
|
1725
1729
|
|
|
1726
1730
|
// ---- cursor-driven suggestion pills — the lateral SKOS neighbourhood
|
|
1727
|
-
// (
|
|
1728
|
-
// (
|
|
1731
|
+
// (tmct.page.relatedForTerm) plus the vertical is-a ancestor chain
|
|
1732
|
+
// (tmct.page.classAncestorChain), mirroring adventure-viz.mjs's own
|
|
1729
1733
|
// suggestionsForTerm against the browser bundle's global (the same
|
|
1730
1734
|
// reach-through-the-global pattern captionFor/pillsFor already use).
|
|
1731
1735
|
// Reads the FULL store (allStoreRows), not worldOnlyRows — a term's
|
|
@@ -1734,8 +1738,8 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1734
1738
|
function renderSuggestionPills() {
|
|
1735
1739
|
const term = wordBeforeCursor(editorTextEl.value, editorTextEl.selectionStart);
|
|
1736
1740
|
if (!term) { editorPillsEl.innerHTML = ""; return; }
|
|
1737
|
-
const related =
|
|
1738
|
-
const chain =
|
|
1741
|
+
const related = tmct.page.relatedForTerm(allStoreRows, term);
|
|
1742
|
+
const chain = tmct.page.classAncestorChain(term, allStoreRows);
|
|
1739
1743
|
const seen = new Set([term]);
|
|
1740
1744
|
const out = [];
|
|
1741
1745
|
const push = (label) => { if (label && !seen.has(label)) { seen.add(label); out.push(label); } };
|
|
@@ -1766,7 +1770,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1766
1770
|
const snap = await session.snapshot();
|
|
1767
1771
|
allStoreRows = snap.rows;
|
|
1768
1772
|
editRows = worldOnlyRows(snap.rows);
|
|
1769
|
-
editState =
|
|
1773
|
+
editState = tmct.page.foldWorldState(editRows);
|
|
1770
1774
|
if (result.unrecognized.length) {
|
|
1771
1775
|
const lines = result.unrecognized.map((u) => u.line).join(", ");
|
|
1772
1776
|
editorStatusEl.className = "edit-status pending";
|
|
@@ -1797,7 +1801,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1797
1801
|
const snap = await session.snapshot();
|
|
1798
1802
|
allStoreRows = snap.rows;
|
|
1799
1803
|
editRows = worldOnlyRows(snap.rows);
|
|
1800
|
-
editState =
|
|
1804
|
+
editState = tmct.page.foldWorldState(editRows);
|
|
1801
1805
|
editorTextEl.value = renderWorldEditorText(editRows, editState);
|
|
1802
1806
|
editorStatusEl.className = "edit-status";
|
|
1803
1807
|
editorStatusEl.textContent = "";
|
|
@@ -1818,7 +1822,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1818
1822
|
|
|
1819
1823
|
async function boot({ fresh = false } = {}) {
|
|
1820
1824
|
const saved = !fresh && persist ? await persist.load() : null;
|
|
1821
|
-
session = await
|
|
1825
|
+
session = await tmct.open(
|
|
1822
1826
|
world(),
|
|
1823
1827
|
saved && saved.payload && saved.payload.memoryPayload
|
|
1824
1828
|
? { restoredPayload: saved.payload.memoryPayload, restoredVisitedRoomIds: saved.payload.visitedRoomIds }
|
|
@@ -1877,9 +1881,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1877
1881
|
// to. Without that, one world's snapshot would restore into another's graph.
|
|
1878
1882
|
let siteVersion = "";
|
|
1879
1883
|
async function openPersistFor(worldName) {
|
|
1880
|
-
if (preview || !
|
|
1884
|
+
if (preview || !tmct.page.openPersistedStore) return;
|
|
1881
1885
|
if (!siteVersion) siteVersion = await fetchSiteVersion();
|
|
1882
|
-
persist =
|
|
1886
|
+
persist = tmct.page.openPersistedStore({ storeKey: "adventure", stamp: siteVersion + ":" + worldName });
|
|
1883
1887
|
}
|
|
1884
1888
|
|
|
1885
1889
|
if (scenarioSelectEl) {
|
|
@@ -652,76 +652,101 @@ async function writeWorldTurn(memoryDir, world, k, facts, cache) {
|
|
|
652
652
|
|
|
653
653
|
const VOIDED_TESTIMONY_SUFFIX = ":gone";
|
|
654
654
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
655
|
+
/** The provenance tag one character's claim carries: who said it, which run of
|
|
656
|
+
* the world it was said in, the turn it was said on, and whether it says the
|
|
657
|
+
* thing has gone. The epoch segment follows snapshotSubject's convention —
|
|
658
|
+
* written only once a recast has moved the world past epoch 0, so an unrecast
|
|
659
|
+
* store and every tag written before epochs existed keep the bare form and
|
|
660
|
+
* read back as epoch 0. */
|
|
661
|
+
export function characterTestimonyTag(character, k, { epoch = 0, voided = false } = {}) {
|
|
662
|
+
const when = epoch > 0 ? `epoch${epoch}:turn${k}` : `turn${k}`;
|
|
663
|
+
return `mud:${character}:${when}${voided ? VOIDED_TESTIMONY_SUFFIX : ""}`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function appendTestimony(memoryDir, { knower, source, thing, k, epoch = 0, voided = false, cache }) {
|
|
659
667
|
await appendFacts(memoryDir, [{
|
|
660
668
|
subject: knower, predicate: KNOWS_ABOUT_PREDICATE, object: thing,
|
|
661
|
-
provenance: characterTestimonyTag(source, k, voided),
|
|
669
|
+
provenance: characterTestimonyTag(source, k, { epoch, voided }),
|
|
662
670
|
}]);
|
|
663
671
|
if (cache) cache.rows = null;
|
|
664
672
|
}
|
|
665
673
|
|
|
666
|
-
/** Record that `teller` told `asker` about `thing` on turn `k`. The
|
|
667
|
-
* the subject — it is the one who now knows — and the teller is named
|
|
668
|
-
* provenance, so the claim corroborates the teller's Source, not the
|
|
669
|
-
|
|
670
|
-
|
|
674
|
+
/** Record that `teller` told `asker` about `thing` on turn `k` of `epoch`. The
|
|
675
|
+
* asker is the subject — it is the one who now knows — and the teller is named
|
|
676
|
+
* in the provenance, so the claim corroborates the teller's Source, not the
|
|
677
|
+
* asker's. */
|
|
678
|
+
export async function recordTold(memoryDir, { asker, teller, thing, k, epoch = 0, cache = null }) {
|
|
679
|
+
return appendTestimony(memoryDir, { knower: asker, source: teller, thing, k, epoch, cache });
|
|
671
680
|
}
|
|
672
681
|
|
|
673
|
-
/** Record that `observer` examined `thing` on turn `k`. The observer
|
|
674
|
-
* the subject and the provenance's character: it learned this by
|
|
675
|
-
* it is its own source for it. */
|
|
676
|
-
export async function recordExamined(memoryDir, { observer, thing, k, cache = null }) {
|
|
677
|
-
return appendTestimony(memoryDir, { knower: observer, source: observer, thing, k, cache });
|
|
682
|
+
/** Record that `observer` examined `thing` on turn `k` of `epoch`. The observer
|
|
683
|
+
* is both the subject and the provenance's character: it learned this by
|
|
684
|
+
* looking, so it is its own source for it. */
|
|
685
|
+
export async function recordExamined(memoryDir, { observer, thing, k, epoch = 0, cache = null }) {
|
|
686
|
+
return appendTestimony(memoryDir, { knower: observer, source: observer, thing, k, epoch, cache });
|
|
678
687
|
}
|
|
679
688
|
|
|
680
|
-
/** Record that `observer` saw `thing` leave the world on turn `k`
|
|
681
|
-
* last of it. Written as a fresh claim on the SAME edge an older one
|
|
682
|
-
* sits on, so the older claim stands untouched and stops being the one
|
|
683
|
-
* rules. The observer is its own source, the way examining is. */
|
|
684
|
-
export async function recordGone(memoryDir, { observer, thing, k, cache = null }) {
|
|
685
|
-
return appendTestimony(memoryDir, { knower: observer, source: observer, thing, k, voided: true, cache });
|
|
689
|
+
/** Record that `observer` saw `thing` leave the world on turn `k` of `epoch` —
|
|
690
|
+
* it ate the last of it. Written as a fresh claim on the SAME edge an older one
|
|
691
|
+
* already sits on, so the older claim stands untouched and stops being the one
|
|
692
|
+
* that rules. The observer is its own source, the way examining is. */
|
|
693
|
+
export async function recordGone(memoryDir, { observer, thing, k, epoch = 0, cache = null }) {
|
|
694
|
+
return appendTestimony(memoryDir, { knower: observer, source: observer, thing, k, epoch, voided: true, cache });
|
|
686
695
|
}
|
|
687
696
|
|
|
688
|
-
const TESTIMONY_TAG_RE = /^mud:([^:\s]+):turn(\d+)(:gone)?$/;
|
|
697
|
+
const TESTIMONY_TAG_RE = /^mud:([^:\s]+):(?:epoch(\d+):)?turn(\d+)(:gone)?$/;
|
|
689
698
|
const TURN_STAMP_RE = /:turn(\d+)\b/;
|
|
690
699
|
|
|
691
700
|
/** How one provenance segment on a knows-about edge stands as a claim about
|
|
692
|
-
* what `knower` knows:
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
* hearsay stamped with whatever turn it
|
|
696
|
-
|
|
701
|
+
* what `knower` knows: which run of the world it was made in, whether the
|
|
702
|
+
* knower vouches for it itself, the turn it was asserted on, and whether it
|
|
703
|
+
* says the thing is gone. A tag that is no character's testimony — a world's
|
|
704
|
+
* own seed fact, a dig spawn — reads as hearsay stamped with whatever turn it
|
|
705
|
+
* carries.
|
|
706
|
+
*
|
|
707
|
+
* Epoch reads the same way foldWorldState reads a row's: a stamped tag names
|
|
708
|
+
* its own run, a tag stamped with a turn but no epoch was written on epoch 0
|
|
709
|
+
* (or before epochs existed), and a tag with no turn at all is a seed the
|
|
710
|
+
* world lays down fresh on every recast, so it belongs to the run being read
|
|
711
|
+
* now. */
|
|
712
|
+
function testimonyClaim(segment, knower, currentEpoch = 0) {
|
|
697
713
|
const mine = TESTIMONY_TAG_RE.exec(segment);
|
|
698
|
-
const
|
|
714
|
+
const turnStamp = mine ? mine[3] : TURN_STAMP_RE.exec(segment)?.[1];
|
|
715
|
+
const stamp = Number(turnStamp ?? 0);
|
|
716
|
+
const epochStamp = mine?.[2];
|
|
699
717
|
return {
|
|
718
|
+
epoch: epochStamp ? Number(epochStamp) : (turnStamp === undefined ? currentEpoch : 0),
|
|
700
719
|
firsthand: !!mine && mine[1] === knower,
|
|
701
720
|
turn: Number.isFinite(stamp) ? stamp : 0,
|
|
702
|
-
voided: !!(mine && mine[
|
|
721
|
+
voided: !!(mine && mine[4]),
|
|
703
722
|
};
|
|
704
723
|
}
|
|
705
724
|
|
|
706
|
-
/**
|
|
707
|
-
* takes the tie — an animal that examined a carrot
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
725
|
+
/** The later epoch beats everything, then firsthand beats hearsay, then the
|
|
726
|
+
* later turn wins, then "gone" takes the tie — an animal that examined a carrot
|
|
727
|
+
* and ate it on one turn ate it second.
|
|
728
|
+
*
|
|
729
|
+
* Epoch on top mirrors the world-state fold: a recast starts the world over, so
|
|
730
|
+
* nothing said in the run it replaced can rule over what a character has seen
|
|
731
|
+
* since, however late in that old run it was said or however firsthand. Within
|
|
732
|
+
* one run, firsthand sits above recency, and that is what stops an animal being
|
|
733
|
+
* talked back into a meal it ate itself: a room-mate can tell it about that
|
|
734
|
+
* carrot the turn after, and its own eyes still hold. */
|
|
711
735
|
const outranksClaim = (claim, best) => (
|
|
712
|
-
claim.
|
|
713
|
-
: claim.
|
|
714
|
-
: claim.
|
|
736
|
+
claim.epoch !== best.epoch ? claim.epoch > best.epoch
|
|
737
|
+
: claim.firsthand !== best.firsthand ? claim.firsthand
|
|
738
|
+
: claim.turn !== best.turn ? claim.turn > best.turn
|
|
739
|
+
: claim.voided && !best.voided
|
|
715
740
|
);
|
|
716
741
|
|
|
717
742
|
/** The claim that rules on one knows-about edge, across every segment its
|
|
718
743
|
* provenance carries. */
|
|
719
|
-
function rulingTestimonyClaim(provenance, knower) {
|
|
744
|
+
function rulingTestimonyClaim(provenance, knower, currentEpoch = 0) {
|
|
720
745
|
let best = null;
|
|
721
746
|
for (const segment of String(provenance || "").split(" | ")) {
|
|
722
747
|
const tag = segment.trim();
|
|
723
748
|
if (!tag) continue;
|
|
724
|
-
const claim = testimonyClaim(tag, knower);
|
|
749
|
+
const claim = testimonyClaim(tag, knower, currentEpoch);
|
|
725
750
|
if (!best || outranksClaim(claim, best)) best = claim;
|
|
726
751
|
}
|
|
727
752
|
return best;
|
|
@@ -730,12 +755,13 @@ function rulingTestimonyClaim(provenance, knower) {
|
|
|
730
755
|
/** What `person` knows about NOW: the object of every knows-about edge whose
|
|
731
756
|
* ruling claim still stands, in the order the edges were first written.
|
|
732
757
|
* Nothing is deleted — an edge whose newest claim says the thing is gone just
|
|
733
|
-
* stops reading back.
|
|
734
|
-
|
|
758
|
+
* stops reading back. `currentEpoch` is the run the reader is on, so a claim a
|
|
759
|
+
* recast has left behind cannot decide what a character knows today. */
|
|
760
|
+
function currentKnowsAboutTopics(rows, person, currentEpoch = 0) {
|
|
735
761
|
const topics = [];
|
|
736
762
|
for (const row of rows || []) {
|
|
737
763
|
if (row.subject !== person || row.predicate !== KNOWS_ABOUT_PREDICATE) continue;
|
|
738
|
-
if (rulingTestimonyClaim(row.provenance, person)?.voided) continue;
|
|
764
|
+
if (rulingTestimonyClaim(row.provenance, person, currentEpoch)?.voided) continue;
|
|
739
765
|
topics.push(row.object);
|
|
740
766
|
}
|
|
741
767
|
return topics;
|
|
@@ -1291,7 +1317,7 @@ export function personKnowledgeLines(rows, state, person) {
|
|
|
1291
1317
|
? `you'll find the ${thing} in the ${place.object}.`
|
|
1292
1318
|
: `the ${thing} is in the ${place.object}.`);
|
|
1293
1319
|
}
|
|
1294
|
-
return { lines, aboutTopics: currentKnowsAboutTopics(rows, person) };
|
|
1320
|
+
return { lines, aboutTopics: currentKnowsAboutTopics(rows, person, state?.epoch ?? 0) };
|
|
1295
1321
|
}
|
|
1296
1322
|
|
|
1297
1323
|
/** The FOOD_CLASS things `person` durably knows about — from being told, or
|
|
@@ -1302,7 +1328,7 @@ export function personKnowledgeLines(rows, state, person) {
|
|
|
1302
1328
|
* food query has no per-topic sub-digest to hand back, so this returns the
|
|
1303
1329
|
* plain list of known food things rather than a {lines, topics} pair. Pure. */
|
|
1304
1330
|
export function personKnownFoodLines(rows, state, person) {
|
|
1305
|
-
return currentKnowsAboutTopics(rows, person)
|
|
1331
|
+
return currentKnowsAboutTopics(rows, person, state?.epoch ?? 0)
|
|
1306
1332
|
.filter((thing) => objectClassChain(rows, thing).includes(FOOD_CLASS));
|
|
1307
1333
|
}
|
|
1308
1334
|
|