@polycode-projects/the-mechanical-code-talker 2.7.23 → 2.7.25
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 +23 -0
- package/data/sprites/adventurer.toml +5 -0
- package/data/sprites/animal.toml +6 -0
- package/data/sprites/butler.toml +8 -0
- package/data/sprites/cabinet.toml +5 -0
- package/data/sprites/container.toml +5 -0
- package/data/sprites/cook.toml +7 -0
- package/data/sprites/desk.toml +4 -0
- package/data/sprites/dog-with-colour.toml +20 -0
- package/data/sprites/dog.toml +5 -0
- package/data/sprites/egg.toml +4 -0
- package/data/sprites/fly.toml +5 -0
- package/data/sprites/furniture.toml +5 -0
- package/data/sprites/gardener.toml +4 -0
- package/data/sprites/housekeeper.toml +6 -0
- package/data/sprites/key.toml +5 -0
- package/data/sprites/lamp.toml +4 -0
- package/data/sprites/letter.toml +5 -0
- package/data/sprites/person.toml +4 -0
- package/data/sprites/poodle.toml +5 -0
- package/data/sprites/portable.toml +6 -0
- package/data/sprites/portrait.toml +5 -0
- package/data/sprites/room.toml +5 -0
- package/data/sprites/spider.toml +5 -0
- package/package.json +1 -1
- package/src/adapters/corpus/sprite-template-files.mjs +52 -0
- package/src/adapters/toml-config.mjs +8 -0
- package/src/domain/game-config.mjs +90 -0
- package/src/domain/spider-fly-world.mjs +5 -2
- package/src/domain/sprite-map.mjs +25 -10
- package/src/domain/sprite-templates.mjs +131 -0
- package/src/services/adventure-viz.mjs +201 -18
- package/src/services/adventure.mjs +45 -3
- package/src/services/chat-session.mjs +9 -1
- package/src/services/chat.mjs +49 -27
- package/src/services/spider-fly-turn.mjs +11 -10
- package/src/services/spider-fly-viz.mjs +73 -8
- package/src/services/spider-fly.mjs +56 -36
- package/src/surfaces/web/adventure-browser-entry.mjs +57 -19
- package/src/surfaces/web/memory-ask-browser.bundle.js +42 -0
- package/src/surfaces/web/spider-fly-browser-entry.mjs +2 -1
|
@@ -22,6 +22,7 @@ import { foldSpiderFlyState, runSpiderFlyTick, startSpiderFlyGame } from "./spid
|
|
|
22
22
|
import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
|
|
23
23
|
import { getWorldsPackProvider } from "../adapters/corpus/worlds-pack.mjs";
|
|
24
24
|
import { appendFacts, appendRule, loadMemory, readFactRows } from "../adapters/memory/core.mjs";
|
|
25
|
+
import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
25
26
|
|
|
26
27
|
// ---- recognizers: the closed opening/stop/tick/address set -------------------
|
|
27
28
|
|
|
@@ -70,7 +71,7 @@ const WORLD_OPENING_FALLBACK =
|
|
|
70
71
|
|
|
71
72
|
// ---- the opening turn: load the shipped board through the worlds pack -------
|
|
72
73
|
|
|
73
|
-
async function openSpiderFlyGame({ planHolder, memoryDir, env, cache }) {
|
|
74
|
+
async function openSpiderFlyGame({ planHolder, memoryDir, env, cache, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
74
75
|
if (!memoryDir) {
|
|
75
76
|
return {
|
|
76
77
|
text: "the spider-and-fly game needs a session with a memory store to hold the board — start tmct inside a repo first.",
|
|
@@ -99,7 +100,7 @@ async function openSpiderFlyGame({ planHolder, memoryDir, env, cache }) {
|
|
|
99
100
|
}
|
|
100
101
|
if (cache) cache.rows = null; // the fact-rows cache predates these writes
|
|
101
102
|
|
|
102
|
-
const { started } = await startSpiderFlyGame(memoryDir, { flyCount: 1 });
|
|
103
|
+
const { started } = await startSpiderFlyGame(memoryDir, { flyCount: 1, config: gameConfig?.spiderFly });
|
|
103
104
|
planHolder.state = { spiderFly: { turn: 0 } };
|
|
104
105
|
const opener = started
|
|
105
106
|
? (payload.meta?.opening || WORLD_OPENING_FALLBACK)
|
|
@@ -212,8 +213,8 @@ function describeEcologyNote(eco) {
|
|
|
212
213
|
return bits.length ? `; ${bits.join(", ")}` : "";
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
async function runTickAndRender({ planHolder, memoryDir, cache, toldFacts = [], addressedNote = null }) {
|
|
216
|
-
const tick = await runSpiderFlyTick(memoryDir, { toldFacts });
|
|
216
|
+
async function runTickAndRender({ planHolder, memoryDir, cache, toldFacts = [], addressedNote = null, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
217
|
+
const tick = await runSpiderFlyTick(memoryDir, { toldFacts, config: gameConfig?.spiderFly });
|
|
217
218
|
if (cache) cache.rows = null;
|
|
218
219
|
planHolder.state = { spiderFly: { turn: tick.turn } };
|
|
219
220
|
return {
|
|
@@ -232,7 +233,7 @@ async function runTickAndRender({ planHolder, memoryDir, cache, toldFacts = [],
|
|
|
232
233
|
* current turn's told-facts"). This also matches how runSpiderFlyTick
|
|
233
234
|
* itself already works: it holds no standing plan or belief between calls,
|
|
234
235
|
* recomputing everything fresh from the folded fact rows every tick. */
|
|
235
|
-
async function runToldFactTurn(match, { planHolder, memoryDir, cache }) {
|
|
236
|
+
async function runToldFactTurn(match, { planHolder, memoryDir, cache, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
236
237
|
const [, addrKindRaw, addrNum, subjKindRaw, subjNum, direction, cellLiteral] = match;
|
|
237
238
|
const addrKind = addrKindRaw.toLowerCase();
|
|
238
239
|
const subjKind = subjKindRaw.toLowerCase();
|
|
@@ -258,7 +259,7 @@ async function runToldFactTurn(match, { planHolder, memoryDir, cache }) {
|
|
|
258
259
|
const targetCellId = cellId(targetCell.x, targetCell.y);
|
|
259
260
|
const toldFacts = [{ subject: subjectId, toAgent: addresseeId, cell: targetCellId, turn: state.turnCount + 1 }];
|
|
260
261
|
return runTickAndRender({
|
|
261
|
-
planHolder, memoryDir, cache, toldFacts,
|
|
262
|
+
planHolder, memoryDir, cache, toldFacts, gameConfig,
|
|
262
263
|
addressedNote: `told the ${addresseeId} the ${subjectId} is at ${targetCellId}`,
|
|
263
264
|
});
|
|
264
265
|
}
|
|
@@ -275,7 +276,7 @@ async function runToldFactTurn(match, { planHolder, memoryDir, cache }) {
|
|
|
275
276
|
* ordinary lanes unchanged, board untouched (§6.2 — no special-cased
|
|
276
277
|
* spider-fly code path for plain questions).
|
|
277
278
|
*/
|
|
278
|
-
export async function spiderFlyTurn(line, { planHolder, memoryDir, env, cache = null, isPlanFrameLine = () => false }) {
|
|
279
|
+
export async function spiderFlyTurn(line, { planHolder, memoryDir, env, cache = null, isPlanFrameLine = () => false, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
279
280
|
const slot = planHolder?.state ?? null;
|
|
280
281
|
const spiderFly = slot?.spiderFly ?? null;
|
|
281
282
|
const opening = SPIDER_FLY_OPEN_RE.test(line);
|
|
@@ -305,7 +306,7 @@ export async function spiderFlyTurn(line, { planHolder, memoryDir, env, cache =
|
|
|
305
306
|
note: "SPIDER-FLY — an opening arrived while a plan frame is active; the slot holds one thing at a time",
|
|
306
307
|
};
|
|
307
308
|
}
|
|
308
|
-
return openSpiderFlyGame({ planHolder, memoryDir, env, cache });
|
|
309
|
+
return openSpiderFlyGame({ planHolder, memoryDir, env, cache, gameConfig });
|
|
309
310
|
}
|
|
310
311
|
|
|
311
312
|
// A game is live.
|
|
@@ -343,11 +344,11 @@ export async function spiderFlyTurn(line, { planHolder, memoryDir, env, cache =
|
|
|
343
344
|
miss: true,
|
|
344
345
|
};
|
|
345
346
|
}
|
|
346
|
-
return runToldFactTurn(told, { planHolder, memoryDir, cache });
|
|
347
|
+
return runToldFactTurn(told, { planHolder, memoryDir, cache, gameConfig });
|
|
347
348
|
}
|
|
348
349
|
|
|
349
350
|
if (SPIDER_FLY_TICK_RE.test(line)) {
|
|
350
|
-
return runTickAndRender({ planHolder, memoryDir, cache, toldFacts: [] });
|
|
351
|
+
return runTickAndRender({ planHolder, memoryDir, cache, toldFacts: [], gameConfig });
|
|
351
352
|
}
|
|
352
353
|
|
|
353
354
|
return null; // an unaddressed aside — the ordinary lanes answer, board untouched
|
|
@@ -96,13 +96,18 @@ export function threadCellsForSpiderPlan(agents, geometry) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/** The self-contained spider-and-fly page. Pure — the same output for the
|
|
99
|
-
* same `title` every time; every other piece of state
|
|
100
|
-
* computed live in the browser once the sibling bundle
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
|
|
99
|
+
* same `title`/`spriteTemplates` every time; every other piece of state
|
|
100
|
+
* this page shows is computed live in the browser once the sibling bundle
|
|
101
|
+
* loads. `spriteTemplates` is the build step's own read of
|
|
102
|
+
* data/sprites/*.toml (sprite-template-files.mjs), embedded as page data
|
|
103
|
+
* the same reason adventure-viz.mjs's own worldPayload is — the browser
|
|
104
|
+
* bundle stays fs-free. Defaults to `[]` (every agent falls back to the
|
|
105
|
+
* flat SPRITE_REGISTRY, unchanged from before this module existed).
|
|
106
|
+
* `?preview=1` on the page's own URL switches it into the small, auto-
|
|
107
|
+
* playing, non-interactive mode the home page's hero iframe embeds (§11) —
|
|
108
|
+
* one file serves both the hero and the "open full-screen" link, matching
|
|
109
|
+
* how ledger.html/plan.html are each one file embedded two ways. */
|
|
110
|
+
export function renderSpiderFlyHtml({ title = DEFAULT_TITLE, spriteTemplates = [] } = {}) {
|
|
106
111
|
const gridData = embedJson({
|
|
107
112
|
gridSize: GRID_SIZE,
|
|
108
113
|
webCells: webCellIds(),
|
|
@@ -114,6 +119,7 @@ export function renderSpiderFlyHtml({ title = DEFAULT_TITLE } = {}) {
|
|
|
114
119
|
tickWaitMs: TICK_WAIT_MS,
|
|
115
120
|
maxFlyMass: FLY_INITIAL_MASS,
|
|
116
121
|
maxSpiderMass: SPIDER_INITIAL_MASS,
|
|
122
|
+
spriteTemplates,
|
|
117
123
|
});
|
|
118
124
|
|
|
119
125
|
return `<!doctype html>
|
|
@@ -171,6 +177,11 @@ ${THEME_TOKENS_CSS}
|
|
|
171
177
|
.chatask .prompt { color: var(--taught); font-size: .78rem; font-family: ${MONO_STACK}; }
|
|
172
178
|
.chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); padding: .32rem .55rem; min-width: 0; }
|
|
173
179
|
.chatask input:disabled { opacity: .5; }
|
|
180
|
+
.chatpills { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .5rem; }
|
|
181
|
+
.pill { font-family: ${MONO_STACK}; font-size: .68rem; padding: .2rem .6rem; border: 1px solid var(--line); border-radius: 99px; background: var(--bg); color: var(--ink); white-space: nowrap; }
|
|
182
|
+
.pill:hover:not(:disabled) { border-color: var(--taught); }
|
|
183
|
+
.pill:disabled { opacity: .45; cursor: default; }
|
|
184
|
+
.pill[data-role="addr"].active { border-color: var(--taught); color: var(--taught); }
|
|
174
185
|
.controls-row { display: flex; align-items: center; gap: .6rem; margin-top: 1rem; flex-wrap: wrap; }
|
|
175
186
|
.controls-row button { font-family: ${MONO_STACK}; font-size: .78rem; padding: .3rem .7rem; border: 1px solid var(--line); background: var(--card); color: var(--ink); }
|
|
176
187
|
.controls-row button:hover:not(:disabled) { border-color: var(--taught); }
|
|
@@ -206,6 +217,14 @@ ${THEME_TOKENS_CSS}
|
|
|
206
217
|
<span class="prompt mono">tmct></span>
|
|
207
218
|
<input id="chatq" type="text" placeholder="@spider the fly is east" aria-label="Address the spider or the fly" disabled>
|
|
208
219
|
</form>
|
|
220
|
+
<div class="chatpills" id="chatpills" role="group" aria-label="quick phrases to fill the chat input">
|
|
221
|
+
<button type="button" class="pill" data-role="addr" data-addressee="spider" disabled>@spider</button>
|
|
222
|
+
<button type="button" class="pill" data-role="addr" data-addressee="fly" disabled>@fly</button>
|
|
223
|
+
<button type="button" class="pill" data-role="dir" data-direction="north" disabled>the fly is north</button>
|
|
224
|
+
<button type="button" class="pill" data-role="dir" data-direction="south" disabled>the fly is south</button>
|
|
225
|
+
<button type="button" class="pill" data-role="dir" data-direction="east" disabled>the fly is east</button>
|
|
226
|
+
<button type="button" class="pill" data-role="dir" data-direction="west" disabled>the fly is west</button>
|
|
227
|
+
</div>
|
|
209
228
|
</div>
|
|
210
229
|
</aside>
|
|
211
230
|
</div>
|
|
@@ -238,6 +257,9 @@ const SPIDERFLY = ${gridData};
|
|
|
238
257
|
const chatlogEl = el("chatlog");
|
|
239
258
|
const chatformEl = el("chatform");
|
|
240
259
|
const chatqEl = el("chatq");
|
|
260
|
+
const chatpillsEl = el("chatpills");
|
|
261
|
+
const addressPillEls = [...chatpillsEl.querySelectorAll('[data-role="addr"]')];
|
|
262
|
+
const directionPillEls = [...chatpillsEl.querySelectorAll('[data-role="dir"]')];
|
|
241
263
|
const statusEl = el("status");
|
|
242
264
|
const turnLabelEl = el("turnLabel");
|
|
243
265
|
const resetBtn = el("resetBtn");
|
|
@@ -298,8 +320,14 @@ const SPIDERFLY = ${gridData};
|
|
|
298
320
|
node = document.createElement("div");
|
|
299
321
|
node.className = "sprite";
|
|
300
322
|
node.dataset.cls = cls;
|
|
323
|
+
// Property-aware resolution (sprite-templates.mjs's resolveSpriteAsset):
|
|
324
|
+
// no agent here carries an mgx:hasProperty fact today, so propertyFacts
|
|
325
|
+
// stays empty and every agent resolves through its plain class template
|
|
326
|
+
// (or the flat SPRITE_REGISTRY, for a class with none) — the same output
|
|
327
|
+
// as before this module existed, just wired for the day an agent does
|
|
328
|
+
// carry one.
|
|
301
329
|
const sprite = window.tmctSpiderFly
|
|
302
|
-
? tmctSpiderFly.
|
|
330
|
+
? tmctSpiderFly.resolveSpriteAsset(cls, (session && session.taxonomyRows) || [], [], SPIDERFLY.spriteTemplates, tmctSpiderFly.SPRITE_REGISTRY)
|
|
303
331
|
: "";
|
|
304
332
|
node.innerHTML = sprite;
|
|
305
333
|
if (!preview) {
|
|
@@ -465,6 +493,42 @@ const SPIDERFLY = ${gridData};
|
|
|
465
493
|
});
|
|
466
494
|
});
|
|
467
495
|
|
|
496
|
+
// ---- chat pills: click-to-fill shortcuts over the SAME #chatq input, never
|
|
497
|
+
// a second path into the engine — a pill only ever sets/appends text and
|
|
498
|
+
// focuses the field, exactly what typing the same characters would do, so
|
|
499
|
+
// free typing keeps working unchanged and every resulting phrase is one the
|
|
500
|
+
// addressed teach-frame grammar (SPIDER_FLY_TOLD_RE in spider-fly-turn.mjs)
|
|
501
|
+
// genuinely accepts.
|
|
502
|
+
function addresseeKindOf(value) {
|
|
503
|
+
const m = /^@(spider|fly)(?:-\\d+)?\\b/i.exec(String(value).trim());
|
|
504
|
+
return m ? m[1].toLowerCase() : null;
|
|
505
|
+
}
|
|
506
|
+
function refreshPills() {
|
|
507
|
+
const explicitKind = addresseeKindOf(chatqEl.value);
|
|
508
|
+
const subject = (explicitKind || "spider") === "spider" ? "fly" : "spider";
|
|
509
|
+
for (const btn of directionPillEls) btn.textContent = "the " + subject + " is " + btn.dataset.direction;
|
|
510
|
+
for (const btn of addressPillEls) btn.classList.toggle("active", btn.dataset.addressee === explicitKind);
|
|
511
|
+
}
|
|
512
|
+
for (const btn of addressPillEls) {
|
|
513
|
+
btn.addEventListener("click", () => {
|
|
514
|
+
chatqEl.value = "@" + btn.dataset.addressee + " ";
|
|
515
|
+
refreshPills();
|
|
516
|
+
chatqEl.focus();
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
for (const btn of directionPillEls) {
|
|
520
|
+
btn.addEventListener("click", () => {
|
|
521
|
+
const kind = addresseeKindOf(chatqEl.value) || "spider";
|
|
522
|
+
let value = chatqEl.value;
|
|
523
|
+
if (!addresseeKindOf(value)) value = "@" + kind + " " + value.trimStart();
|
|
524
|
+
chatqEl.value = value.replace(/\\s+$/, "") + " " + btn.textContent;
|
|
525
|
+
refreshPills();
|
|
526
|
+
chatqEl.focus();
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
chatqEl.addEventListener("input", refreshPills);
|
|
530
|
+
refreshPills();
|
|
531
|
+
|
|
468
532
|
// ---- serialize every engine-touching call: the ticker and the chat dock
|
|
469
533
|
// share one in-memory store, and an overlapping tick()/turn() pair could
|
|
470
534
|
// race against the same @turnN write.
|
|
@@ -481,6 +545,7 @@ const SPIDERFLY = ${gridData};
|
|
|
481
545
|
statusEl.textContent = session.opening;
|
|
482
546
|
chatqEl.disabled = false;
|
|
483
547
|
resetBtn.disabled = false; playBtn.disabled = false; stepBtn.disabled = false;
|
|
548
|
+
for (const btn of [...addressPillEls, ...directionPillEls]) btn.disabled = false;
|
|
484
549
|
}
|
|
485
550
|
|
|
486
551
|
let loopScheduled = false;
|
|
@@ -19,6 +19,7 @@ import { appendFacts, loadMemory, readFactRows } from "../adapters/memory/core.m
|
|
|
19
19
|
import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
|
|
20
20
|
import { mulberry32 } from "../domain/seeded-random.mjs";
|
|
21
21
|
import { fnv1a32 } from "../domain/hash.mjs";
|
|
22
|
+
import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
22
23
|
|
|
23
24
|
// ---- tunable constants (starting values, not fixed — the vision radius and
|
|
24
25
|
// mass economy all want checking against a real playable board) -------------
|
|
@@ -187,18 +188,20 @@ export function gridApplyActions(factRows) {
|
|
|
187
188
|
export const spiderPathStateKey = (state) => cellId(state.x, state.y);
|
|
188
189
|
|
|
189
190
|
/** Whether (x, y) is currently webbed — the static home zone (always active)
|
|
190
|
-
* OR a live spider-built web (mgx:web-built-at-turn +
|
|
191
|
+
* OR a live spider-built web (mgx:web-built-at-turn + webDurationTurns >
|
|
191
192
|
* turn). The one predicate every eat precondition and the fly's movement
|
|
192
193
|
* gate consult, so the static zone and dynamic webs are ONE concept. `state`
|
|
193
194
|
* may be omitted (or carry no `webs` map) — the static-zone check alone
|
|
194
195
|
* still answers correctly, just blind to dynamic webs; every real caller
|
|
195
|
-
* threads the folded state through.
|
|
196
|
-
|
|
196
|
+
* threads the folded state through. `webDurationTurns` defaults to the
|
|
197
|
+
* shipped WEB_DURATION_TURNS; a caller holding a resolved game config passes
|
|
198
|
+
* its own webDurationTurns instead. */
|
|
199
|
+
export function hasActiveWebAt(x, y, state, turn, webDurationTurns = WEB_DURATION_TURNS) {
|
|
197
200
|
if (isInWebBlock(x, y)) return true;
|
|
198
201
|
if (!state?.webs?.size) return false;
|
|
199
202
|
const target = cellId(x, y);
|
|
200
203
|
for (const { cell, builtAtTurn } of state.webs.values()) {
|
|
201
|
-
if (cell === target && builtAtTurn +
|
|
204
|
+
if (cell === target && builtAtTurn + webDurationTurns > turn) return true;
|
|
202
205
|
}
|
|
203
206
|
return false;
|
|
204
207
|
}
|
|
@@ -210,11 +213,12 @@ export function hasActiveWebAt(x, y, state, turn) {
|
|
|
210
213
|
* only on the fixed target), so this returns null — an honest "no path to
|
|
211
214
|
* an eat" rather than a path toward a cell that would never satisfy the eat
|
|
212
215
|
* condition. Null also covers "no believed target at all." `state`/`turn`
|
|
213
|
-
* are optional, defaulting to "static web zone only" (see hasActiveWebAt).
|
|
214
|
-
|
|
216
|
+
* are optional, defaulting to "static web zone only" (see hasActiveWebAt).
|
|
217
|
+
* `webDurationTurns` forwards to hasActiveWebAt unchanged. */
|
|
218
|
+
export function planSpiderPath(spiderCell, believedFlyCell, applyActions, state, turn, webDurationTurns = WEB_DURATION_TURNS) {
|
|
215
219
|
if (!believedFlyCell) return null;
|
|
216
220
|
const isGoal = (s) =>
|
|
217
|
-
s.x === believedFlyCell.x && s.y === believedFlyCell.y && hasActiveWebAt(s.x, s.y, state, turn);
|
|
221
|
+
s.x === believedFlyCell.x && s.y === believedFlyCell.y && hasActiveWebAt(s.x, s.y, state, turn, webDurationTurns);
|
|
218
222
|
return findActionPath(spiderCell, isGoal, applyActions, { stateKey: spiderPathStateKey });
|
|
219
223
|
}
|
|
220
224
|
|
|
@@ -362,10 +366,17 @@ function mostRecentEaterSpider(state, eatenDeltaBySpider) {
|
|
|
362
366
|
* never starve-checked, so callers that don't track spider mass, e.g. older
|
|
363
367
|
* tests, see no behavior change). `state` is the PRE-move fold (for history:
|
|
364
368
|
* prior flies-eaten counts, prior eggs, prior eaten turns, live webs).
|
|
369
|
+
* `config` (default DEFAULT_GAME_CONFIG.spiderFly) supplies every tunable
|
|
370
|
+
* this pass reads: the initial masses a fallback/hatch/spawn mints, the
|
|
371
|
+
* eggs-eaten lay threshold, the hatch delay, the spawn interval, and the web
|
|
372
|
+
* duration the eat precondition checks against.
|
|
365
373
|
* Returns `{ writes, events }` — writes to append alongside the turn's
|
|
366
374
|
* movement facts, events for the tick's own return payload. Pure.
|
|
367
375
|
*/
|
|
368
|
-
export function runEcologyPass({
|
|
376
|
+
export function runEcologyPass({
|
|
377
|
+
state, postMovePlacements, postMoveMassByFly, postMoveMassBySpider = new Map(), turn,
|
|
378
|
+
config = DEFAULT_GAME_CONFIG.spiderFly,
|
|
379
|
+
}) {
|
|
369
380
|
const k = turn;
|
|
370
381
|
const writes = [];
|
|
371
382
|
const events = { eaten: [], starved: [], laid: null, hatched: [], spawned: null, spawnedCell: null, massAfterEating: new Map() };
|
|
@@ -381,7 +392,7 @@ export function runEcologyPass({ state, postMovePlacements, postMoveMassByFly, p
|
|
|
381
392
|
const eatenMassBySpider = new Map();
|
|
382
393
|
for (const spiderId of spiders) {
|
|
383
394
|
const sCell = postMovePlacements.get(spiderId);
|
|
384
|
-
if (!hasActiveWebAt(sCell.x, sCell.y, state, k)) continue;
|
|
395
|
+
if (!hasActiveWebAt(sCell.x, sCell.y, state, k, config.webDurationTurns)) continue;
|
|
385
396
|
for (const flyId of flies) {
|
|
386
397
|
if (claimedFlies.has(flyId)) continue;
|
|
387
398
|
const fCell = postMovePlacements.get(flyId);
|
|
@@ -396,7 +407,7 @@ export function runEcologyPass({ state, postMovePlacements, postMoveMassByFly, p
|
|
|
396
407
|
for (const [spiderId, delta] of eatenDeltaBySpider) {
|
|
397
408
|
const newCount = (state.fliesEaten.get(spiderId)?.value ?? 0) + delta;
|
|
398
409
|
writes.push({ subject: `${spiderId}@turn${k}`, predicate: "mgx:flies-eaten", object: String(newCount) });
|
|
399
|
-
const priorSpiderMass = postMoveMassBySpider.get(spiderId) ?? (state.mass.get(spiderId)?.value ??
|
|
410
|
+
const priorSpiderMass = postMoveMassBySpider.get(spiderId) ?? (state.mass.get(spiderId)?.value ?? config.spiderInitialMass);
|
|
400
411
|
const newSpiderMass = priorSpiderMass + (eatenMassBySpider.get(spiderId) ?? 0);
|
|
401
412
|
writes.push({ subject: `${spiderId}@turn${k}`, predicate: "mgx:mass", object: String(newSpiderMass) });
|
|
402
413
|
events.massAfterEating.set(spiderId, newSpiderMass);
|
|
@@ -427,7 +438,7 @@ export function runEcologyPass({ state, postMovePlacements, postMoveMassByFly, p
|
|
|
427
438
|
const liveEggId = [...state.laidAtTurn.keys()].find((id) => !state.removed.has(id));
|
|
428
439
|
if (!liveEggId) {
|
|
429
440
|
const sinceTurn = mostRecentEggLaidTurn(state);
|
|
430
|
-
const threshold = sinceTurn === -1 ? 1 :
|
|
441
|
+
const threshold = sinceTurn === -1 ? 1 : config.eggsEatenThreshold;
|
|
431
442
|
let eatsSince = events.eaten.length;
|
|
432
443
|
for (const { turn: eatenTurn } of state.eatenBy.values()) if (eatenTurn > sinceTurn) eatsSince += 1;
|
|
433
444
|
if (eatsSince >= threshold) {
|
|
@@ -442,18 +453,18 @@ export function runEcologyPass({ state, postMovePlacements, postMoveMassByFly, p
|
|
|
442
453
|
}
|
|
443
454
|
}
|
|
444
455
|
|
|
445
|
-
// 4. Hatch — any live egg laid exactly
|
|
456
|
+
// 4. Hatch — any live egg laid exactly config.eggHatchDelayTurns turns ago.
|
|
446
457
|
const liveEggIds = [...state.laidAtTurn.keys()].filter((id) => !state.removed.has(id)).sort();
|
|
447
458
|
let nextSpiderNum = 1 + maxIdSuffix(state.placements.keys(), /^spider-(\d+)$/);
|
|
448
459
|
for (const eggId of liveEggIds) {
|
|
449
460
|
const laidTurn = state.laidAtTurn.get(eggId).value;
|
|
450
|
-
if (laidTurn +
|
|
461
|
+
if (laidTurn + config.eggHatchDelayTurns !== k) continue;
|
|
451
462
|
const eggCell = state.placements.get(eggId)?.cell;
|
|
452
463
|
if (!eggCell) continue;
|
|
453
464
|
const newSpiderId = `spider-${nextSpiderNum}`;
|
|
454
465
|
nextSpiderNum += 1;
|
|
455
466
|
writes.push({ subject: `${newSpiderId}@turn${k}`, predicate: "mgx:currently-in", object: eggCell });
|
|
456
|
-
writes.push({ subject: `${newSpiderId}@turn${k}`, predicate: "mgx:mass", object: String(
|
|
467
|
+
writes.push({ subject: `${newSpiderId}@turn${k}`, predicate: "mgx:mass", object: String(config.spiderInitialMass) });
|
|
457
468
|
writes.push({ subject: `${eggId}@turn${k}`, predicate: "mgx:hatched-into", object: newSpiderId });
|
|
458
469
|
events.hatched.push({ egg: eggId, spider: newSpiderId, cell: eggCell });
|
|
459
470
|
}
|
|
@@ -461,7 +472,7 @@ export function runEcologyPass({ state, postMovePlacements, postMoveMassByFly, p
|
|
|
461
472
|
// 5. Spawn — every third turn, a new fly at a seeded pick among the
|
|
462
473
|
// currently-uncontested perimeter cells (never Math.random — see
|
|
463
474
|
// seededPick's own header comment).
|
|
464
|
-
if (k %
|
|
475
|
+
if (k % config.flySpawnIntervalTurns === 0) {
|
|
465
476
|
const occupied = new Set();
|
|
466
477
|
for (const spiderId of spiders) { const c = postMovePlacements.get(spiderId); occupied.add(cellId(c.x, c.y)); }
|
|
467
478
|
for (const flyId of flies) {
|
|
@@ -479,7 +490,7 @@ export function runEcologyPass({ state, postMovePlacements, postMoveMassByFly, p
|
|
|
479
490
|
const newFlyId = `fly-${1 + maxIdSuffix(state.placements.keys(), /^fly-(\d+)$/)}`;
|
|
480
491
|
const cell = seededPick(uncontested, `${WORLD_NAME}:${k}:${newFlyId}:spawn`);
|
|
481
492
|
writes.push({ subject: `${newFlyId}@turn${k}`, predicate: "mgx:currently-in", object: cell });
|
|
482
|
-
writes.push({ subject: `${newFlyId}@turn${k}`, predicate: "mgx:mass", object: String(
|
|
493
|
+
writes.push({ subject: `${newFlyId}@turn${k}`, predicate: "mgx:mass", object: String(config.flyInitialMass) });
|
|
483
494
|
events.spawned = newFlyId;
|
|
484
495
|
events.spawnedCell = cell;
|
|
485
496
|
}
|
|
@@ -494,15 +505,16 @@ export function runEcologyPass({ state, postMovePlacements, postMoveMassByFly, p
|
|
|
494
505
|
* board perimeter — a fresh session's own starting state, never part of
|
|
495
506
|
* the shipped (reusable, static) world pack itself. A no-op when spider-1
|
|
496
507
|
* already exists (idempotent — safe to call from a caller unsure whether
|
|
497
|
-
* the game has already started).
|
|
498
|
-
|
|
508
|
+
* the game has already started). `config` (default
|
|
509
|
+
* DEFAULT_GAME_CONFIG.spiderFly) supplies the starting masses. */
|
|
510
|
+
export async function startSpiderFlyGame(memoryDir, { flyCount = 1, config = DEFAULT_GAME_CONFIG.spiderFly } = {}) {
|
|
499
511
|
const state = foldSpiderFlyState(readFactRows(await loadMemory(memoryDir)));
|
|
500
512
|
if (state.placements.has("spider-1")) return { started: false, facts: [] };
|
|
501
513
|
|
|
502
514
|
const perimeter = perimeterCells();
|
|
503
515
|
const facts = [
|
|
504
516
|
{ subject: "spider-1", predicate: "mgx:currently-in", object: cellId(WEB_HOME.x, WEB_HOME.y) },
|
|
505
|
-
{ subject: "spider-1", predicate: "mgx:mass", object: String(
|
|
517
|
+
{ subject: "spider-1", predicate: "mgx:mass", object: String(config.spiderInitialMass) },
|
|
506
518
|
];
|
|
507
519
|
const occupied = new Set([cellId(WEB_HOME.x, WEB_HOME.y)]);
|
|
508
520
|
for (let i = 0; i < flyCount; i += 1) {
|
|
@@ -511,7 +523,7 @@ export async function startSpiderFlyGame(memoryDir, { flyCount = 1 } = {}) {
|
|
|
511
523
|
const cell = seededPick(uncontested.length ? uncontested : perimeter, `${WORLD_NAME}:0:${flyId}:spawn`);
|
|
512
524
|
occupied.add(cell);
|
|
513
525
|
facts.push({ subject: flyId, predicate: "mgx:currently-in", object: cell });
|
|
514
|
-
facts.push({ subject: flyId, predicate: "mgx:mass", object: String(
|
|
526
|
+
facts.push({ subject: flyId, predicate: "mgx:mass", object: String(config.flyInitialMass) });
|
|
515
527
|
}
|
|
516
528
|
await appendFacts(memoryDir, facts.map((f) => ({ ...f, provenance: worldProvenanceTag(WORLD_NAME) })));
|
|
517
529
|
return { started: true, facts };
|
|
@@ -534,11 +546,13 @@ function goalLineFor(subject, believed, arrived, kind) {
|
|
|
534
546
|
* web(s) minted THIS tick before they've been written/read back), as a
|
|
535
547
|
* plain array of { id, cell, builtAtTurn, expiresAtTurn }. Excludes the
|
|
536
548
|
* always-on static home zone (that's WEB_HOME/WEB_RADIUS, drawn separately —
|
|
537
|
-
* this is only the spider-built kind), for a renderer to draw distinctly.
|
|
538
|
-
|
|
549
|
+
* this is only the spider-built kind), for a renderer to draw distinctly.
|
|
550
|
+
* `webDurationTurns` defaults to the shipped WEB_DURATION_TURNS; a caller
|
|
551
|
+
* holding a resolved game config passes its own webDurationTurns instead. */
|
|
552
|
+
export function liveWebs(websMap, turn, webDurationTurns = WEB_DURATION_TURNS) {
|
|
539
553
|
const out = [];
|
|
540
554
|
for (const [id, { cell, builtAtTurn }] of websMap) {
|
|
541
|
-
if (builtAtTurn +
|
|
555
|
+
if (builtAtTurn + webDurationTurns > turn) out.push({ id, cell, builtAtTurn, expiresAtTurn: builtAtTurn + webDurationTurns });
|
|
542
556
|
}
|
|
543
557
|
return out;
|
|
544
558
|
}
|
|
@@ -560,9 +574,15 @@ export function liveWebs(websMap, turn) {
|
|
|
560
574
|
* eaten/starved/laid/hatched/spawned event summary; `activeWebs` is every
|
|
561
575
|
* currently-live dynamic web (static home zone excluded — that's fixed grid
|
|
562
576
|
* geometry, not runtime state), for a renderer to draw distinctly.
|
|
577
|
+
*
|
|
578
|
+
* `opts.config` (default DEFAULT_GAME_CONFIG.spiderFly) supplies every
|
|
579
|
+
* tunable this tick reads: the vision radius, both agents' starting/decrement
|
|
580
|
+
* masses, and the web duration, and is forwarded unchanged into
|
|
581
|
+
* runEcologyPass.
|
|
563
582
|
*/
|
|
564
583
|
export async function runSpiderFlyTick(memoryDir, opts = {}) {
|
|
565
|
-
const {
|
|
584
|
+
const { toldFacts = [], config = DEFAULT_GAME_CONFIG.spiderFly } = opts;
|
|
585
|
+
const visionRadius = config.visionRadius;
|
|
566
586
|
const rows = readFactRows(await loadMemory(memoryDir));
|
|
567
587
|
const state = foldSpiderFlyState(rows);
|
|
568
588
|
const k = state.turnCount + 1;
|
|
@@ -581,8 +601,8 @@ export async function runSpiderFlyTick(memoryDir, opts = {}) {
|
|
|
581
601
|
|
|
582
602
|
for (const spiderId of spiders) {
|
|
583
603
|
const spiderCell = parseCellId(state.placements.get(spiderId).cell);
|
|
584
|
-
const priorMass = state.mass.get(spiderId)?.value ??
|
|
585
|
-
const newMass = Math.max(0, priorMass -
|
|
604
|
+
const priorMass = state.mass.get(spiderId)?.value ?? config.spiderInitialMass;
|
|
605
|
+
const newMass = Math.max(0, priorMass - config.spiderMassDecrementPerTurn);
|
|
586
606
|
postMoveMassBySpider.set(spiderId, newMass);
|
|
587
607
|
|
|
588
608
|
// Priority 1: avoid any OTHER live spider believed visible.
|
|
@@ -599,7 +619,7 @@ export async function runSpiderFlyTick(memoryDir, opts = {}) {
|
|
|
599
619
|
const target = nearestBelievedTarget(spiderId, spiderCell, flies, state, { visionRadius, toldFacts });
|
|
600
620
|
if (target) {
|
|
601
621
|
nextCell = spiderCell;
|
|
602
|
-
const path = planSpiderPath(spiderCell, target.cell, applyActions, state, k);
|
|
622
|
+
const path = planSpiderPath(spiderCell, target.cell, applyActions, state, k, config.webDurationTurns);
|
|
603
623
|
if (path) {
|
|
604
624
|
if (path.actions.length) { nextCell = path.states[1]; plan = path.actions; }
|
|
605
625
|
} else {
|
|
@@ -611,14 +631,14 @@ export async function runSpiderFlyTick(memoryDir, opts = {}) {
|
|
|
611
631
|
// already at its closest reachable cell but still a step away
|
|
612
632
|
// (Chebyshev-adjacent isn't co-located; has-exit-* edges have no
|
|
613
633
|
// diagonal hop).
|
|
614
|
-
const arrived = nextCell.x === target.cell.x && nextCell.y === target.cell.y && hasActiveWebAt(nextCell.x, nextCell.y, state, k);
|
|
634
|
+
const arrived = nextCell.x === target.cell.x && nextCell.y === target.cell.y && hasActiveWebAt(nextCell.x, nextCell.y, state, k, config.webDurationTurns);
|
|
615
635
|
goal = goalLineFor(spiderId, target, arrived, "spider");
|
|
616
636
|
} else {
|
|
617
637
|
// Priority 3: hold position, and build/refresh a web there unless an
|
|
618
638
|
// unexpired web already covers this exact cell.
|
|
619
639
|
nextCell = spiderCell;
|
|
620
640
|
const heldCellId = cellId(spiderCell.x, spiderCell.y);
|
|
621
|
-
if (!hasActiveWebAt(spiderCell.x, spiderCell.y, state, k)) {
|
|
641
|
+
if (!hasActiveWebAt(spiderCell.x, spiderCell.y, state, k, config.webDurationTurns)) {
|
|
622
642
|
const webId = `web-${nextWebNum}`;
|
|
623
643
|
nextWebNum += 1;
|
|
624
644
|
tickWebs.set(webId, { cell: heldCellId, builtAtTurn: k });
|
|
@@ -640,19 +660,19 @@ export async function runSpiderFlyTick(memoryDir, opts = {}) {
|
|
|
640
660
|
for (const flyId of flies) {
|
|
641
661
|
const flyCell = parseCellId(state.placements.get(flyId).cell);
|
|
642
662
|
const believedSpider = nearestBelievedTarget(flyId, flyCell, spiders, state, { visionRadius, toldFacts });
|
|
643
|
-
const webbed = hasActiveWebAt(flyCell.x, flyCell.y, state, k);
|
|
663
|
+
const webbed = hasActiveWebAt(flyCell.x, flyCell.y, state, k, config.webDurationTurns);
|
|
644
664
|
const nextCell = webbed ? flyCell : greedyFlyMove(flyCell, believedSpider?.cell ?? null, applyActions, k, flyId);
|
|
645
665
|
postMovePlacements.set(flyId, nextCell);
|
|
646
666
|
movementWrites.push({ subject: `${flyId}@turn${k}`, predicate: "mgx:currently-in", object: cellId(nextCell.x, nextCell.y) });
|
|
647
|
-
const priorMass = state.mass.get(flyId)?.value ??
|
|
648
|
-
const newMass = Math.max(0, priorMass -
|
|
667
|
+
const priorMass = state.mass.get(flyId)?.value ?? config.flyInitialMass;
|
|
668
|
+
const newMass = Math.max(0, priorMass - config.flyMassDecrementPerTurn);
|
|
649
669
|
postMoveMassByFly.set(flyId, newMass);
|
|
650
670
|
movementWrites.push({ subject: `${flyId}@turn${k}`, predicate: "mgx:mass", object: String(newMass) });
|
|
651
671
|
const goal = webbed ? "trapped in an active web — can't move." : goalLineFor(flyId, believedSpider, true, "fly");
|
|
652
672
|
agents[flyId] = { cell: cellId(nextCell.x, nextCell.y), goal, mass: newMass };
|
|
653
673
|
}
|
|
654
674
|
|
|
655
|
-
const ecology = runEcologyPass({ state, postMovePlacements, postMoveMassByFly, postMoveMassBySpider, turn: k });
|
|
675
|
+
const ecology = runEcologyPass({ state, postMovePlacements, postMoveMassByFly, postMoveMassBySpider, turn: k, config });
|
|
656
676
|
// Every agent's goal was assigned during movement, before this same tick's
|
|
657
677
|
// ecology pass resolves eating/starving — so a THIRD agent's goal can name
|
|
658
678
|
// a subject that dies in this exact tick just as easily as the dying
|
|
@@ -705,14 +725,14 @@ export async function runSpiderFlyTick(memoryDir, opts = {}) {
|
|
|
705
725
|
// though the very same tick's event text already announces it, only
|
|
706
726
|
// catching up the following tick once the fold picks it up naturally.
|
|
707
727
|
for (const h of ecology.events.hatched) {
|
|
708
|
-
agents[h.spider] = { cell: h.cell, goal: "just hatched — no goal yet.", plan: null, mass:
|
|
728
|
+
agents[h.spider] = { cell: h.cell, goal: "just hatched — no goal yet.", plan: null, mass: config.spiderInitialMass };
|
|
709
729
|
}
|
|
710
730
|
if (ecology.events.spawned && ecology.events.spawnedCell) {
|
|
711
|
-
agents[ecology.events.spawned] = { cell: ecology.events.spawnedCell, goal: "just arrived — no goal yet.", mass:
|
|
731
|
+
agents[ecology.events.spawned] = { cell: ecology.events.spawnedCell, goal: "just arrived — no goal yet.", mass: config.flyInitialMass };
|
|
712
732
|
}
|
|
713
733
|
const writes = [...movementWrites, ...ecology.writes];
|
|
714
734
|
const provenance = `${worldProvenanceTag(WORLD_NAME)}:turn${k}`;
|
|
715
735
|
await appendFacts(memoryDir, writes.map((f) => ({ ...f, provenance })));
|
|
716
736
|
|
|
717
|
-
return { turn: k, writes, agents, ecology: ecology.events, activeWebs: liveWebs(tickWebs, k) };
|
|
737
|
+
return { turn: k, writes, agents, ecology: ecology.events, activeWebs: liveWebs(tickWebs, k, config.webDurationTurns) };
|
|
718
738
|
}
|
|
@@ -13,30 +13,40 @@
|
|
|
13
13
|
// rationale) — the bootstrap below then just appends it, exactly the shape
|
|
14
14
|
// openAdventure() itself writes for a real chat session.
|
|
15
15
|
//
|
|
16
|
-
// This session exposes
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
16
|
+
// This session exposes a raw autoplay tick, a read-only snapshot, AND
|
|
17
|
+
// (mirroring createSpiderFlySession's own `turn(line)`) a full chat-dock
|
|
18
|
+
// entry point: `turn(line)` runs the exact same runTurn the CLI and every
|
|
19
|
+
// other viz page's chat dock run, over this session's own memoryDir/graph/
|
|
20
|
+
// lexicon, threading `focus`/`last`/`planState` across calls the same way a
|
|
21
|
+
// real chat session does. `planState` and `autoplayTick`'s own `planHolder`
|
|
22
|
+
// share ONE mutable holder here, so a manual chat command and an auto-play
|
|
23
|
+
// tick can never disagree about whether the adventure is still open, mid a
|
|
24
|
+
// number game, etc. — whichever ran last leaves the holder as the other's
|
|
25
|
+
// starting point. `planHolder.state` starts as adventureTurn's own opened-
|
|
26
|
+
// world shape, so BOTH entry points treat every call as a live, already-open
|
|
27
|
+
// world rather than a fresh opening line: ordinary in-game commands (look/
|
|
28
|
+
// go/take/open/talk/examine/...) dispatch through adventure.mjs's own
|
|
29
|
+
// adventureTurn exactly as autoplayTick's calls already do, and anything not
|
|
30
|
+
// game-shaped falls through to the ordinary conversational layer, exactly
|
|
31
|
+
// like a real CLI session.
|
|
32
|
+
import { runTurn } from "../../services/chat.mjs";
|
|
27
33
|
import {
|
|
28
34
|
createInMemoryStore, appendFacts, appendRule, loadMemory, readFactRows,
|
|
29
35
|
} from "../../adapters/memory/core.mjs";
|
|
30
|
-
import {
|
|
36
|
+
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
37
|
+
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
38
|
+
import { foldWorldState, worldDigestRows, roomAffordances } from "../../services/adventure.mjs";
|
|
31
39
|
import { runAdventureAutoplayTick } from "../../services/adventure-autoplay.mjs";
|
|
32
40
|
import { resolveSpriteForClass, SPRITE_REGISTRY } from "../../domain/sprite-map.mjs";
|
|
41
|
+
import { resolveSpriteAsset } from "../../domain/sprite-templates.mjs";
|
|
33
42
|
|
|
34
|
-
/** A live in-memory adventure this page's ticker
|
|
35
|
-
*
|
|
43
|
+
/** A live in-memory adventure this page's ticker AND chat dock can both
|
|
44
|
+
* drive. Returns `{ memoryDir, autoplayTick, turn, snapshot }`.
|
|
36
45
|
* `worldPayload.facts`/`.rules` seed the store exactly the way
|
|
37
46
|
* openAdventure() itself does for a real session; `planHolder.state` is set
|
|
38
|
-
* the same way, so adventureTurn treats every subsequent call
|
|
39
|
-
* already-open world rather than
|
|
47
|
+
* the same way, so adventureTurn treats every subsequent call — auto-play's
|
|
48
|
+
* own or a visitor's typed one — as a live, already-open world rather than
|
|
49
|
+
* a fresh opening line. */
|
|
40
50
|
export async function createAdventureSession(worldPayload) {
|
|
41
51
|
const memoryDir = createInMemoryStore();
|
|
42
52
|
const tag = `world:${worldPayload.name}`;
|
|
@@ -54,6 +64,11 @@ export async function createAdventureSession(worldPayload) {
|
|
|
54
64
|
const openingHere = foldWorldState(openingRows).placements.get("player")?.object ?? null;
|
|
55
65
|
if (openingHere) exposedRoomIds = new Set([openingHere]);
|
|
56
66
|
|
|
67
|
+
const graph = parseEntities({ individuals: [], objectProperties: [] });
|
|
68
|
+
const lexicon = loadLexicon();
|
|
69
|
+
let focus = null;
|
|
70
|
+
let last = null;
|
|
71
|
+
|
|
57
72
|
return {
|
|
58
73
|
memoryDir,
|
|
59
74
|
|
|
@@ -69,8 +84,30 @@ export async function createAdventureSession(worldPayload) {
|
|
|
69
84
|
return result;
|
|
70
85
|
},
|
|
71
86
|
|
|
87
|
+
/** One dispatched chat turn — the SAME runTurn the CLI and every other
|
|
88
|
+
* viz page's own chat dock run, over this session's own memoryDir. A
|
|
89
|
+
* throwing runTurn must never kill the session — the page has no other
|
|
90
|
+
* chance to show this turn's answer. */
|
|
91
|
+
async turn(line) {
|
|
92
|
+
let result;
|
|
93
|
+
try {
|
|
94
|
+
result = await runTurn(line, {
|
|
95
|
+
config: null, source: null, graph, focus, last, memoryDir, sessionId,
|
|
96
|
+
env: {}, lexicon, vocabHint: "", planState: planHolder.state,
|
|
97
|
+
});
|
|
98
|
+
} catch (e) {
|
|
99
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
100
|
+
return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, record: null, plan: null };
|
|
101
|
+
}
|
|
102
|
+
focus = result.focus;
|
|
103
|
+
last = result.last;
|
|
104
|
+
if ("planState" in result) planHolder.state = result.planState;
|
|
105
|
+
return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null, plan: result.plan ?? null };
|
|
106
|
+
},
|
|
107
|
+
|
|
72
108
|
/** A read-only fold of the current room — no engine advance — for the
|
|
73
|
-
* page's own redraw after boot
|
|
109
|
+
* page's own redraw after boot, after every tick, and after every
|
|
110
|
+
* manual chat turn. */
|
|
74
111
|
async snapshot() {
|
|
75
112
|
const rows = readFactRows(await loadMemory(memoryDir));
|
|
76
113
|
const state = foldWorldState(rows);
|
|
@@ -81,8 +118,9 @@ export async function createAdventureSession(worldPayload) {
|
|
|
81
118
|
}
|
|
82
119
|
|
|
83
120
|
// Re-exported so the page's own rendering script (adventure-viz.mjs) never
|
|
84
|
-
// has to duplicate sprite resolution
|
|
121
|
+
// has to duplicate sprite resolution, the digest reader, or the room
|
|
122
|
+
// affordances the chat dock's own pills read from — the same posture
|
|
85
123
|
// spider-fly-browser-entry.mjs's own globalThis.tmctSpiderFly re-export takes.
|
|
86
124
|
globalThis.tmctAdventure = {
|
|
87
|
-
createAdventureSession, resolveSpriteForClass, SPRITE_REGISTRY, worldDigestRows,
|
|
125
|
+
createAdventureSession, resolveSpriteForClass, SPRITE_REGISTRY, resolveSpriteAsset, worldDigestRows, roomAffordances,
|
|
88
126
|
};
|