@polycode-projects/the-mechanical-code-talker 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/corpus/sprites/src/sprite-facts.jsonl +375 -8
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +20 -0
- package/src/domain/ask-vocab.mjs +71 -0
- package/src/domain/ask.mjs +168 -0
- package/src/domain/game-config.mjs +11 -0
- package/src/domain/mud-facts.mjs +15 -0
- package/src/domain/router/drive.mjs +35 -9
- package/src/domain/router/registry.mjs +24 -4
- package/src/domain/router/resolver.mjs +102 -40
- package/src/domain/scene-compose.mjs +117 -0
- package/src/domain/spider-fly-world.mjs +36 -0
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +161 -14
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +90 -121
- package/src/services/adventure.mjs +97 -35
- package/src/services/chat-page-viz.mjs +32 -16
- package/src/services/chat.mjs +101 -33
- package/src/services/code-explorer-viz.mjs +51 -49
- package/src/services/ingest-viz.mjs +15 -57
- package/src/services/ledger-viz.mjs +47 -27
- package/src/services/memory-panel-viz.mjs +38 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +87 -198
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +4 -3
- package/src/services/research-viz.mjs +10 -52
- package/src/services/spider-fly-turn.mjs +14 -22
- package/src/services/spider-fly-viz.mjs +79 -118
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +271 -221
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +75 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +49 -33
- package/src/surfaces/web/chat-browser-entry.mjs +30 -105
- package/src/surfaces/web/code-explorer-browser-entry.mjs +168 -24
- package/src/surfaces/web/ingest-browser-entry.mjs +3 -13
- package/src/surfaces/web/ledger-browser-entry.mjs +7 -47
- package/src/surfaces/web/memory-ask-browser.bundle.js +128 -125
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +28 -28
- package/src/surfaces/web/plan-browser-entry.mjs +22 -40
- package/src/surfaces/web/research-browser-entry.mjs +26 -41
- package/src/surfaces/web/spider-fly-browser-entry.mjs +45 -28
- package/src/surfaces/web/sprites-browser-entry.mjs +14 -27
- package/src/surfaces/web/turn-session.mjs +120 -0
- package/src/tools/definitions.mjs +30 -0
- package/src/tools/handlers/index.mjs +6 -3
- package/src/tools/handlers/kit.mjs +19 -2
- package/src/tools/handlers/tmct-ask.mjs +11 -6
- package/src/tools/handlers/tmct-ingest.mjs +5 -1
- package/src/tools/handlers/tmct-related.mjs +4 -4
- package/src/tools/handlers/tmct-sprite.mjs +147 -0
- package/src/tools/memory-fallthrough.mjs +9 -2
- package/src/tools/server.mjs +25 -1
package/src/services/mud-viz.mjs
CHANGED
|
@@ -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
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
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,
|
|
70
|
-
//
|
|
71
|
-
// mud-browser-entry.mjs's own
|
|
72
|
-
// responsibility, and this is where that
|
|
73
|
-
// queue is also what makes the deck's
|
|
74
|
-
// increments in the exact order turns
|
|
75
|
-
// between panes.
|
|
76
|
-
import {
|
|
77
|
-
|
|
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
|
|
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.
|
|
154
|
-
|
|
155
|
-
|
|
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,62 +170,14 @@ 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
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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 = [];
|
|
@@ -1390,12 +1344,10 @@ function pageScript() {
|
|
|
1390
1344
|
}
|
|
1391
1345
|
|
|
1392
1346
|
// Whichever individual the WORLD marks dangerous, never a species this page
|
|
1393
|
-
// hardcodes — the same
|
|
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
|
-
|
|
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.
|
|
@@ -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
|
-
|
|
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) {
|
|
@@ -1616,9 +1552,7 @@ function pageScript() {
|
|
|
1616
1552
|
|
|
1617
1553
|
// ---- room-view rendering -------------------------------------------------
|
|
1618
1554
|
function hashOf(text) {
|
|
1619
|
-
|
|
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) {
|
|
@@ -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
|
-
|
|
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) {
|
|
@@ -2117,10 +1988,7 @@ function pageScript() {
|
|
|
2117
1988
|
let editing = false;
|
|
2118
1989
|
|
|
2119
1990
|
function worldOnlyRows(rows) {
|
|
2120
|
-
|
|
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() {
|
|
@@ -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
|
|
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
|
|
2895
|
-
//
|
|
2896
|
-
//
|
|
2897
|
-
//
|
|
2898
|
-
//
|
|
2899
|
-
|
|
2900
|
-
|
|
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
|
-
|
|
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.
|
|
@@ -2936,9 +2811,23 @@ function pageScript() {
|
|
|
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
|
+
const opened = await window.tmctMud.createMudSession(scenario().worldPayload, { characters: everyone(), epoch: nextEpoch });
|
|
2940
2815
|
if (seq !== bootSeq) return;
|
|
2941
2816
|
session = opened;
|
|
2817
|
+
if (liveRoom) {
|
|
2818
|
+
try {
|
|
2819
|
+
await liveRoom.rebind({ memoryDir: opened.memoryDir, worldName: worldName, myDisplayName: myDisplayName });
|
|
2820
|
+
renderWire();
|
|
2821
|
+
renderNodes();
|
|
2822
|
+
// After renderWire, the same way dropRoom writes its own reason: the
|
|
2823
|
+
// render restates the state's stock note, and WHY this recast kept
|
|
2824
|
+
// the link is the thing worth saying over it.
|
|
2825
|
+
el("wireStateNote").textContent = note || "the burrow recast \\u2014 still linked.";
|
|
2826
|
+
await claimMyAnimals();
|
|
2827
|
+
} catch (err) {
|
|
2828
|
+
dropRoom("the link didn't survive the recast \\u2014 share again to link up.");
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2942
2831
|
for (let i = 0; i < cast.length; i += 1) slotOf[cast[i]] = slots[i];
|
|
2943
2832
|
bindPanes();
|
|
2944
2833
|
for (const character of cast) {
|
|
@@ -164,6 +164,19 @@ export function createP2pRoom({
|
|
|
164
164
|
const seenProvenanceById = new Map();
|
|
165
165
|
let cachedRows = [];
|
|
166
166
|
|
|
167
|
+
// Store-touching work runs one job at a time, in arrival order. Every path
|
|
168
|
+
// that reads or writes memoryDir/seenProvenanceById/cachedRows crosses at
|
|
169
|
+
// least one await, so without this a rebind could swap the store while a
|
|
170
|
+
// merge is half-written into the old one — the merged rows would then
|
|
171
|
+
// baseline against the wrong store, or vanish. Jobs queued behind a rebind
|
|
172
|
+
// land on the store the rebind installed.
|
|
173
|
+
let storeChain = Promise.resolve();
|
|
174
|
+
function withStore(job) {
|
|
175
|
+
const run = storeChain.then(job);
|
|
176
|
+
storeChain = run.then(() => {}, () => {});
|
|
177
|
+
return run;
|
|
178
|
+
}
|
|
179
|
+
|
|
167
180
|
const factsListeners = new Set();
|
|
168
181
|
const stateListeners = new Set();
|
|
169
182
|
const peersListeners = new Set();
|
|
@@ -246,8 +259,8 @@ export function createP2pRoom({
|
|
|
246
259
|
|
|
247
260
|
/** Diff the store against what this room last saw, relabel each changed
|
|
248
261
|
* fact's provenance for the wire, and broadcast the batch. The page calls
|
|
249
|
-
* this once after every local turn or action. */
|
|
250
|
-
async function
|
|
262
|
+
* this (as `afterLocalChange`) once after every local turn or action. */
|
|
263
|
+
async function flushLocalChange() {
|
|
251
264
|
await ensureStarted();
|
|
252
265
|
await refreshRows();
|
|
253
266
|
const changed = [];
|
|
@@ -270,7 +283,7 @@ export function createP2pRoom({
|
|
|
270
283
|
if (!accepted.length) return { merged: 0 };
|
|
271
284
|
// Flush first: a local fact still waiting to be diffed would otherwise be
|
|
272
285
|
// recorded as merged below and never leave this browser.
|
|
273
|
-
await
|
|
286
|
+
await flushLocalChange();
|
|
274
287
|
const { ids } = await appendFacts(memoryDir, accepted.map((f) => ({
|
|
275
288
|
subject: f.subject,
|
|
276
289
|
predicate: f.predicate,
|
|
@@ -357,9 +370,11 @@ export function createP2pRoom({
|
|
|
357
370
|
return;
|
|
358
371
|
}
|
|
359
372
|
case "sync-request": {
|
|
360
|
-
await
|
|
361
|
-
|
|
362
|
-
|
|
373
|
+
const facts = await withStore(async () => {
|
|
374
|
+
await refreshRows();
|
|
375
|
+
const timestamp = now();
|
|
376
|
+
return syncableFacts(cachedRows).flatMap((row) => toWireFacts(row, displayName, timestamp));
|
|
377
|
+
});
|
|
363
378
|
send(transport, syncResponseMessage({ facts }));
|
|
364
379
|
return;
|
|
365
380
|
}
|
|
@@ -368,7 +383,7 @@ export function createP2pRoom({
|
|
|
368
383
|
// between them needs no sequencing.
|
|
369
384
|
case "sync-response":
|
|
370
385
|
case "op":
|
|
371
|
-
await mergeIncomingFacts(message.facts);
|
|
386
|
+
await withStore(() => mergeIncomingFacts(message.facts));
|
|
372
387
|
}
|
|
373
388
|
}
|
|
374
389
|
|
|
@@ -413,7 +428,7 @@ export function createP2pRoom({
|
|
|
413
428
|
/** Mint a fresh invite blob. One blob completes exactly one connection, so
|
|
414
429
|
* each call replaces whatever earlier invite was still waiting for a reply. */
|
|
415
430
|
async function startSharing() {
|
|
416
|
-
await ensureStarted
|
|
431
|
+
await withStore(ensureStarted);
|
|
417
432
|
const transport = attachTransport(transportFactory());
|
|
418
433
|
pendingShare = transport;
|
|
419
434
|
lastError = null;
|
|
@@ -425,7 +440,7 @@ export function createP2pRoom({
|
|
|
425
440
|
/** Decode someone's invite and answer it. Returns the reply blob to send
|
|
426
441
|
* back, or a named problem the page can show beside the box it came from. */
|
|
427
442
|
async function acceptInvite(blobString) {
|
|
428
|
-
await ensureStarted
|
|
443
|
+
await withStore(ensureStarted);
|
|
429
444
|
const decoded = decodeInviteBlob(blobString);
|
|
430
445
|
if (decoded.error) {
|
|
431
446
|
lastError = problem("invite", decoded.error);
|
|
@@ -453,7 +468,7 @@ export function createP2pRoom({
|
|
|
453
468
|
/** Feed the joiner's reply back into the invite it answers, completing the
|
|
454
469
|
* connection. */
|
|
455
470
|
async function completeInvite(replyBlob) {
|
|
456
|
-
await ensureStarted
|
|
471
|
+
await withStore(ensureStarted);
|
|
457
472
|
if (!pendingShare) {
|
|
458
473
|
lastError = problem("reply", "no-pending-invite");
|
|
459
474
|
return lastError;
|
|
@@ -482,20 +497,24 @@ export function createP2pRoom({
|
|
|
482
497
|
}
|
|
483
498
|
|
|
484
499
|
async function setMyDisplayName(name) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
500
|
+
return withStore(async () => {
|
|
501
|
+
await ensureStarted();
|
|
502
|
+
displayName = name;
|
|
503
|
+
await appendFacts(memoryDir, [nodeNameFact(myPeerId, name, now())]);
|
|
504
|
+
await sortFactIndividualsById();
|
|
505
|
+
return flushLocalChange();
|
|
506
|
+
});
|
|
490
507
|
}
|
|
491
508
|
|
|
492
509
|
/** Wave as `subjectId`, in `roomId` when there is one. chat.html's presence
|
|
493
510
|
* wave passes null and lands on PRESENCE_SCOPE instead. */
|
|
494
511
|
async function wave(subjectId, roomId = null) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
512
|
+
return withStore(async () => {
|
|
513
|
+
await ensureStarted();
|
|
514
|
+
await appendFacts(memoryDir, [waveFact(subjectId, roomId || PRESENCE_SCOPE, now())]);
|
|
515
|
+
await sortFactIndividualsById();
|
|
516
|
+
return flushLocalChange();
|
|
517
|
+
});
|
|
499
518
|
}
|
|
500
519
|
|
|
501
520
|
/** Whether `subjectId` is waving right now, read from the cached rows so a
|
|
@@ -517,6 +536,53 @@ export function createP2pRoom({
|
|
|
517
536
|
return peers.get(peerId)?.displayName || String(peerId).slice(0, 8);
|
|
518
537
|
}
|
|
519
538
|
|
|
539
|
+
/** Re-bind this room to a fresh store — the recast path. The peer
|
|
540
|
+
* connections are the point of keeping the room alive, so nothing about
|
|
541
|
+
* the transports or the peer map is touched. In order: flush whatever the
|
|
542
|
+
* OLD store still had undiffed (a turn landed just before the recast must
|
|
543
|
+
* not vanish silently), swap the store reference, rebuild the diff
|
|
544
|
+
* baseline against the new store, write the identity facts into it, then
|
|
545
|
+
* push the new store's syncable facts to every open channel as an
|
|
546
|
+
* ordinary op and ask each peer for its own view with an ordinary
|
|
547
|
+
* sync-request — the exact machinery a freshly opened channel uses, no
|
|
548
|
+
* new message type. Runs on the store chain, so a merge in flight when
|
|
549
|
+
* the recast happens finishes against the store it started on, and
|
|
550
|
+
* everything behind it lands on the new one. */
|
|
551
|
+
async function rebind({ memoryDir: nextMemoryDir, worldName: nextWorldName, myDisplayName: nextDisplayName } = {}) {
|
|
552
|
+
if (!nextMemoryDir) throw new Error("rebind needs the store to bind to");
|
|
553
|
+
return withStore(async () => {
|
|
554
|
+
if (closed) throw new Error("this room is closed");
|
|
555
|
+
if (started) await flushLocalChange();
|
|
556
|
+
memoryDir = nextMemoryDir;
|
|
557
|
+
if (nextWorldName) worldName = nextWorldName;
|
|
558
|
+
if (nextDisplayName) displayName = nextDisplayName;
|
|
559
|
+
started = true;
|
|
560
|
+
seenProvenanceById.clear();
|
|
561
|
+
cachedRows = [];
|
|
562
|
+
const timestamp = now();
|
|
563
|
+
const identity = [];
|
|
564
|
+
if (worldId && worldName) identity.push(worldNameFact(worldId, worldName, timestamp));
|
|
565
|
+
if (myPeerId && displayName) identity.push(nodeNameFact(myPeerId, displayName, timestamp));
|
|
566
|
+
if (identity.length) {
|
|
567
|
+
await appendFacts(memoryDir, identity);
|
|
568
|
+
await sortFactIndividualsById();
|
|
569
|
+
}
|
|
570
|
+
await refreshRows();
|
|
571
|
+
for (const row of cachedRows) seenProvenanceById.set(row.id, row.provenance);
|
|
572
|
+
const targets = connectedPeers();
|
|
573
|
+
let pushed = 0;
|
|
574
|
+
if (targets.length) {
|
|
575
|
+
const wireTimestamp = now();
|
|
576
|
+
const facts = syncableFacts(cachedRows).flatMap((row) => toWireFacts(row, displayName, wireTimestamp));
|
|
577
|
+
pushed = facts.length;
|
|
578
|
+
if (facts.length) broadcast(opMessage({ from: myPeerId, facts }));
|
|
579
|
+
broadcast(syncRequestMessage());
|
|
580
|
+
}
|
|
581
|
+
emit(factsListeners, { merged: 0, rows: cachedRows });
|
|
582
|
+
return { pushed, peers: targets.length };
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
520
586
|
function close() {
|
|
521
587
|
closed = true;
|
|
522
588
|
for (const peer of peers.values()) peer.transport.close();
|
|
@@ -534,23 +600,24 @@ export function createP2pRoom({
|
|
|
534
600
|
return {
|
|
535
601
|
peerId: myPeerId,
|
|
536
602
|
worldId,
|
|
537
|
-
worldName,
|
|
603
|
+
get worldName() { return worldName; },
|
|
538
604
|
get displayName() { return displayName; },
|
|
539
605
|
get state() { return state; },
|
|
540
606
|
get lastError() { return lastError; },
|
|
541
607
|
get droppedMessages() { return droppedMessages; },
|
|
542
608
|
peers: peerList,
|
|
543
609
|
displayNameFor,
|
|
544
|
-
start: ensureStarted,
|
|
610
|
+
start: () => withStore(ensureStarted),
|
|
545
611
|
startSharing,
|
|
546
612
|
acceptInvite,
|
|
547
613
|
completeInvite,
|
|
548
|
-
afterLocalChange,
|
|
614
|
+
afterLocalChange: () => withStore(flushLocalChange),
|
|
549
615
|
setMyDisplayName,
|
|
550
616
|
wave,
|
|
551
617
|
isWaving,
|
|
618
|
+
rebind,
|
|
552
619
|
factRows: () => cachedRows,
|
|
553
|
-
refresh: refreshRows,
|
|
620
|
+
refresh: () => withStore(refreshRows),
|
|
554
621
|
onFactsChanged: subscribe(factsListeners),
|
|
555
622
|
onStateChanged: subscribe(stateListeners),
|
|
556
623
|
onPeersChanged: subscribe(peersListeners),
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
// is itself a declared class name is a class-to-class edge; anything else is
|
|
26
26
|
// an individual-to-class edge), not a guess.
|
|
27
27
|
|
|
28
|
+
import { countLabel } from "./viz-theme.mjs";
|
|
29
|
+
|
|
28
30
|
const attachPrefix = (predicate) => {
|
|
29
31
|
const p = String(predicate ?? "").trim();
|
|
30
32
|
return p.includes(":") ? p : `mgx:${p}`;
|
|
@@ -208,7 +210,7 @@ export function planToPddl(plan, { problemName = "tmct-plan", domainName = "tmct
|
|
|
208
210
|
|
|
209
211
|
if (actions.length) {
|
|
210
212
|
lines.push("");
|
|
211
|
-
lines.push(`;; action sequence — findActionPath's own shortest path (${actions.length
|
|
213
|
+
lines.push(`;; action sequence — findActionPath's own shortest path (${countLabel(actions.length, "move")})`);
|
|
212
214
|
actions.forEach((action, i) => {
|
|
213
215
|
const before = states[i] || [];
|
|
214
216
|
const after = states[i + 1] || [];
|