@polycode-projects/the-mechanical-code-talker 5.0.2 → 5.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/corpus/worlds/manifest.json +9 -9
  2. package/corpus/worlds/shards/town-square-chapel.jsonl.gz +0 -0
  3. package/corpus/worlds/shards/town-square-market.jsonl.gz +0 -0
  4. package/corpus/worlds/shards/town-square.jsonl.gz +0 -0
  5. package/corpus/worlds/src/town-square-chapel.jsonl +1 -1
  6. package/corpus/worlds/src/town-square-market.jsonl +1 -1
  7. package/corpus/worlds/src/town-square.jsonl +1 -1
  8. package/data/mudiii-assets.json +35 -5
  9. package/package.json +1 -1
  10. package/src/adapters/memory/core.mjs +236 -18
  11. package/src/domain/ask-vocab.mjs +1 -1
  12. package/src/domain/ask.mjs +20 -10
  13. package/src/domain/game-config.mjs +10 -1
  14. package/src/domain/interpret/normalize.mjs +4 -0
  15. package/src/domain/memory/retraction.mjs +232 -0
  16. package/src/domain/p2p/sync-filter.mjs +9 -1
  17. package/src/domain/spider-fly-world.mjs +80 -35
  18. package/src/domain/syllogise.mjs +128 -75
  19. package/src/services/adventure-autoplay.mjs +2 -2
  20. package/src/services/adventure-viz.mjs +12 -7
  21. package/src/services/chat.mjs +42 -14
  22. package/src/services/mud-turn.mjs +1 -1
  23. package/src/services/mud-viz.mjs +11 -2
  24. package/src/services/mudiii-scene.mjs +199 -55
  25. package/src/services/mudiii-turn.mjs +82 -1
  26. package/src/services/mudiii-viz.mjs +17 -7
  27. package/src/services/p2p-room.mjs +130 -9
  28. package/src/services/predator-prey.mjs +524 -69
  29. package/src/services/spider-fly-turn.mjs +128 -56
  30. package/src/services/spider-fly-viz.mjs +65 -71
  31. package/src/services/world-teach.mjs +3 -3
  32. package/src/surfaces/web/adventure-browser-entry.mjs +12 -3
  33. package/src/surfaces/web/memory-ask-browser.bundle.js +118 -118
  34. package/src/surfaces/web/mud-browser-entry.mjs +10 -3
  35. package/src/surfaces/web/mudiii-browser-entry.mjs +3 -3
  36. package/src/surfaces/web/spider-fly-browser-entry.mjs +26 -22
  37. package/src/services/spider-fly.mjs +0 -943
@@ -1,30 +1,85 @@
1
- // spider-fly-turn.mjs — the chat lane for the headless spider-and-fly game
2
- // (PLAN_SPIDER_FLY.md §6): loading the shipped board into the session's
3
- // memory store, the stop command, the addressed spatial teach-frame that
4
- // feeds a told-fact into the next tick, and the bare "tick" command this
5
- // game's no-player-controlled-entity posture (§1 — both agents move on
6
- // their own, every turn) needs for CLI use. Mirrors adventure.mjs's own
7
- // shape exactly: closed-regex openers/stop, a slot-tagged one-at-a-time
8
- // coexistence check against the other two lanes, a lane function returning
9
- // { text, goal?, lane, note, miss? } or null when the turn is not this
10
- // lane's to answer. The fourth of the four lanes sharing planState's slot.
1
+ // spider-fly-turn.mjs — the spider-and-fly cast: its bindings onto the shared
2
+ // predator/prey engine, and its chat lane.
11
3
  //
12
- // This module never plans a path or scores a move itself every bit of
13
- // game logic (fold, pathfinding, belief, ecology) lives in spider-fly.mjs;
14
- // this file only recognizes chat shapes, resolves them to the shapes
15
- // runSpiderFlyTick's own interface accepts, and renders its return value as
16
- // chat text.
4
+ // The bindings are the first section below, and they are the whole of what this
5
+ // game adds to predator-prey.mjs. Every piece of game logic the (epoch, turn)
6
+ // fold, the pathfinding, belief, the decision chains, the ecology pass — is the
7
+ // shared engine's, run with this board's layout, this board's roles and this
8
+ // board's knobs. Carrying a catch to a web, spinning webs and laying eggs are
9
+ // three opt-in engine features spiderFlyEngineConfig switches on; nothing about
10
+ // them is written twice.
11
+ //
12
+ // The lane is everything after that: loading the shipped board into the
13
+ // session's memory store, the stop command, the addressed spatial teach-frame
14
+ // that feeds a told-fact into the next tick, and the bare "tick" command this
15
+ // game's no-player-controlled-entity posture (both agents move on their own,
16
+ // every turn) needs for CLI use. It mirrors adventure.mjs's own shape exactly:
17
+ // closed-regex openers/stop, a slot-tagged one-at-a-time coexistence check
18
+ // against the other lanes, a lane function returning { text, goal?, lane, note,
19
+ // miss? } or null when the turn is not this lane's to answer.
17
20
 
18
21
  import {
19
- DIRECTION_DELTA, WORLD_NAME, cellId, parseCellId, inBounds, chebyshevDistance, oneStepDirectionBetween,
22
+ DIRECTION_DELTA, WORLD_NAME, SPIDER_FLY_LAYOUT, SPIDER_FLY_ROLES, spiderFlyEngineConfig,
23
+ cellId, parseCellId, inBounds, chebyshevDistance, oneStepDirectionBetween,
20
24
  agentKindOf, liveIdsOfKind,
21
25
  } from "../domain/spider-fly-world.mjs";
22
- import { foldSpiderFlyState, runSpiderFlyTick, startSpiderFlyGame, beliefSnapshotFor } from "./spider-fly.mjs";
26
+ import { perimeterCells } from "../domain/town-square-world.mjs";
27
+ import {
28
+ beliefSnapshotFor, foldTownSquareState, liveWebs, runTownSquareTick, seededSpawnCell,
29
+ startTownSquareGame, townSquareBoard,
30
+ } from "./predator-prey.mjs";
23
31
  import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
24
32
  import { getWorldsPackProvider } from "../adapters/corpus/worlds-pack.mjs";
25
33
  import { appendFacts, appendRule, loadMemory, readFactRows } from "../adapters/memory/core.mjs";
26
34
  import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
27
35
 
36
+ // ---- the cast's bindings onto the shared engine ------------------------------
37
+
38
+ /** Fold fact rows into the current board state — the engine's own fold, named
39
+ * for the game whose readers ask for it. */
40
+ export const foldSpiderFlyState = foldTownSquareState;
41
+
42
+ export { beliefSnapshotFor, liveWebs };
43
+
44
+ const engineOpts = (config) => ({
45
+ layout: SPIDER_FLY_LAYOUT,
46
+ roles: SPIDER_FLY_ROLES,
47
+ config: spiderFlyEngineConfig(config),
48
+ });
49
+
50
+ /** Mint spider-1 in its web and a spread of flies around the board edge — a
51
+ * fresh session's own starting state, never part of the shipped (reusable,
52
+ * static) world pack. A no-op once this epoch already holds a roster, so it is
53
+ * safe to call from a caller unsure whether the game has started. The flies'
54
+ * cells are seeded picks over the same perimeter list, and off the same
55
+ * keyspace, a mid-game arrival draws from. */
56
+ export async function startSpiderFlyGame(memoryDir, { flyCount = 1, config = DEFAULT_GAME_CONFIG.spiderFly } = {}) {
57
+ const homeCell = SPIDER_FLY_LAYOUT.webHomeCell;
58
+ const agents = { "spider-1": { role: "predator", cell: homeCell } };
59
+ const edge = perimeterCells(SPIDER_FLY_LAYOUT);
60
+ const taken = new Set([homeCell]);
61
+ for (let i = 1; i <= flyCount; i += 1) {
62
+ const flyId = `fly-${i}`;
63
+ const free = edge.filter((c) => !taken.has(c));
64
+ const cell = seededSpawnCell(free.length ? free : edge, { layoutName: SPIDER_FLY_LAYOUT.name, id: flyId });
65
+ taken.add(cell);
66
+ agents[flyId] = { role: "prey", cell };
67
+ }
68
+ return startTownSquareGame(memoryDir, { ...engineOpts(config), agents });
69
+ }
70
+
71
+ /** One full turn on this board. `toldFacts` is the belief layer's chat channel:
72
+ * `{ subject, toAgent, cell, turn }` rows, empty for an unaddressed tick. */
73
+ export function runSpiderFlyTick(memoryDir, { toldFacts = [], config = DEFAULT_GAME_CONFIG.spiderFly } = {}) {
74
+ return runTownSquareTick(memoryDir, { ...engineOpts(config), toldFacts });
75
+ }
76
+
77
+ /** The board as it stands, in a tick's own payload shape, without running one —
78
+ * what a renderer draws between opening a session and the first tick. */
79
+ export function spiderFlyBoard(memoryDir, { toldFacts = [], config = DEFAULT_GAME_CONFIG.spiderFly } = {}) {
80
+ return townSquareBoard(memoryDir, { ...engineOpts(config), toldFacts });
81
+ }
82
+
28
83
  // ---- recognizers: the closed opening/stop/tick/address set -------------------
29
84
 
30
85
  // The opener names the game without requiring a specific phrasing order —
@@ -42,15 +97,14 @@ const SPIDER_FLY_OPEN_RE =
42
97
  // player thinks of the session.
43
98
  const SPIDER_FLY_STOP_RE =
44
99
  /^(?:stop\s+(?:watching|playing)|quit\s+(?:the\s+)?(?:spider\s+and\s+fly\s+)?game|end\s+the\s+spider(?:\s+and\s+fly)?\s+game|leave\s+the\s+game)[.!?\s]*$/i;
45
- // The bare tick command NOT specified by PLAN_SPIDER_FLY.md itself (§11's
46
- // Play/step button is the page's own answer to "nothing requires the human
47
- // to act"; this is that same need's CLI/chat equivalent). Styled after the
48
- // plan lane's own PLAN_NEXT_RE ("next"/"next move"/"go on"/"continue").
100
+ // The chat equivalent of the page's own Play/step button: nothing here
101
+ // requires the human to act, so a watcher needs a word that advances a turn.
102
+ // Styled after the plan lane's own PLAN_NEXT_RE ("next"/"next move"/"go on").
49
103
  const SPIDER_FLY_TICK_RE = /^(?:tick|next\s+turn|advance(?:\s+the\s+turn)?)[.!?\s]*$/i;
50
104
 
51
- // The spatial teach-frame (§6.1): "@spider the fly is east" / "@spider the
52
- // fly is at cell-7-3". Its own closed regex, not a route through
53
- // parseRelation/parseCopula/parseOfForm — §6.1 found both hit real grammar
105
+ // The spatial teach-frame: "@spider the fly is east" / "@spider the fly is at
106
+ // cell-7-3". Its own closed regex, not a route through
107
+ // parseRelation/parseCopula/parseOfForm — both hit real grammar
54
108
  // gaps for this exact shape (the bare copula reading mints a nonsense
55
109
  // subclass axiom; the "of" form hits parseAce's own hard-null guard before
56
110
  // ever reaching parseRelation/parseCopula). A fixed compass set — north,
@@ -284,6 +338,21 @@ export function pillsForSpiderFly(agents, explicitAddresseeId, opts = {}) {
284
338
 
285
339
  // ---- rendering one tick's return value as plain chat text --------------------
286
340
 
341
+ /** One ecology event as the clause a player reads. Null for an event type this
342
+ * board never produces, so a cast that grows one later reads as silence rather
343
+ * than as a broken sentence. */
344
+ function ecologyClause(event) {
345
+ switch (event.type) {
346
+ case "catch-prey": return `${event.predator} caught ${event.prey} at ${event.cell}`;
347
+ case "eat-agent": return `${event.prey} was eaten by ${event.predator} at ${event.cell}`;
348
+ case "starve": return `${event.agent} starved`;
349
+ case "lay-egg": return `${event.egg} was laid`;
350
+ case "hatch-egg": return `${event.egg} hatched into ${event.hatchlings.map((h) => h.id).join(" and ")} at ${event.cell}`;
351
+ case "spawn-prey": return `${event.agent} arrived at the board edge`;
352
+ default: return null;
353
+ }
354
+ }
355
+
287
356
  function renderTickText(tick, addressedNote) {
288
357
  const parts = [];
289
358
  if (addressedNote) parts.push(`${addressedNote}.`);
@@ -291,15 +360,8 @@ function renderTickText(tick, addressedNote) {
291
360
  parts.push(ids.length
292
361
  ? `Turn ${tick.turn} — ${ids.map((id) => `${id} is now at ${tick.agents[id].cell}`).join("; ")}.`
293
362
  : `Turn ${tick.turn} — no agents remain on the board.`);
294
- const eco = tick.ecology;
295
- const events = [];
296
- for (const c of eco.caught) events.push(`${c.spider} caught ${c.fly} at ${c.cell}`);
297
- for (const e of eco.eaten) events.push(`${e.fly} was eaten by ${e.spider} at ${e.cell}`);
298
- for (const f of eco.starved) events.push(`${f} starved`);
299
- if (eco.laid) events.push(`${eco.laid} was laid`);
300
- for (const h of eco.hatched) events.push(`${h.egg} hatched into ${h.spiders.map((s) => s.spider).join(" and ")} at ${h.cell}`);
301
- if (eco.spawned) events.push(`${eco.spawned} arrived at the board edge`);
302
- if (events.length) parts.push(`${events.join("; ")}.`);
363
+ const clauses = tick.ecology.map(ecologyClause).filter(Boolean);
364
+ if (clauses.length) parts.push(`${clauses.join("; ")}.`);
303
365
  return parts.join(" ");
304
366
  }
305
367
 
@@ -316,14 +378,26 @@ function combinedGoalLine(agents) {
316
378
  return ids.map((id) => `${id} — ${agents[id].goal.replace(/\.\s*$/, "")}`).join("; ");
317
379
  }
318
380
 
319
- function describeEcologyNote(eco) {
320
- const bits = [];
321
- if (eco.caught.length) bits.push(`${eco.caught.length} caught`);
322
- if (eco.eaten.length) bits.push(`${eco.eaten.length} eaten`);
323
- if (eco.starved.length) bits.push(`${eco.starved.length} starved`);
324
- if (eco.laid) bits.push("1 laid");
325
- if (eco.hatched.length) bits.push(`${eco.hatched.reduce((n, h) => n + h.spiders.length, 0)} hatched`);
326
- if (eco.spawned) bits.push("1 spawned");
381
+ const ECOLOGY_NOTE_WORDS = Object.freeze({
382
+ "catch-prey": "caught",
383
+ "eat-agent": "eaten",
384
+ starve: "starved",
385
+ "lay-egg": "laid",
386
+ "hatch-egg": "hatched",
387
+ "spawn-prey": "spawned",
388
+ });
389
+
390
+ function describeEcologyNote(events) {
391
+ const tally = new Map();
392
+ for (const event of events) {
393
+ const word = ECOLOGY_NOTE_WORDS[event.type];
394
+ if (!word) continue;
395
+ const n = event.type === "hatch-egg" ? event.hatchlings.length : 1;
396
+ tally.set(word, (tally.get(word) ?? 0) + n);
397
+ }
398
+ const bits = Object.values(ECOLOGY_NOTE_WORDS)
399
+ .filter((word) => tally.has(word))
400
+ .map((word) => `${tally.get(word)} ${word}`);
327
401
  return bits.length ? `; ${bits.join(", ")}` : "";
328
402
  }
329
403
 
@@ -352,8 +426,8 @@ export function believedFactSentence(id, believedCell) {
352
426
  }
353
427
 
354
428
  /** "what does the fly see?" / "what does the spider see?" rendered as plain
355
- * text: the same beliefSnapshotFor read spider-fly.mjs's own tick loop and
356
- * the browser panel already use, over the CURRENT board state — read-only,
429
+ * text: the same beliefSnapshotFor read the engine's own tick loop and the
430
+ * browser panel already use, over the CURRENT board state — read-only,
357
431
  * no tick runs, nothing is written. Candidates are every OTHER live agent
358
432
  * of either kind; toldFacts is empty (a told position only ever arrives
359
433
  * fresh alongside a tick — see runToldFactTurn — so there is none standing
@@ -385,11 +459,10 @@ async function spiderFlyBeliefAnswer(match, { memoryDir, gameConfig = DEFAULT_GA
385
459
  /** The addressed teach-frame turn: resolve the addressee and the belief
386
460
  * subject, resolve the told cell, and run ONE tick with that told-fact fed
387
461
  * in. Told-facts are NOT persisted on the session slot across turns — each
388
- * addressed line supplies belief for the NEXT tick only, then is gone
389
- * (the simplest of the plan doc's own named options, §4's "carry only the
390
- * current turn's told-facts"). This also matches how runSpiderFlyTick
391
- * itself already works: it holds no standing plan or belief between calls,
392
- * recomputing everything fresh from the folded fact rows every tick. */
462
+ * addressed line supplies belief for the NEXT tick only, then is gone. That
463
+ * matches how the engine itself already works: it holds no standing plan or
464
+ * belief between calls, recomputing everything fresh from the folded fact
465
+ * rows every tick. */
393
466
  async function runToldFactTurn(match, { planHolder, memoryDir, cache, gameConfig = DEFAULT_GAME_CONFIG }) {
394
467
  const [, addrKindRaw, addrNum, subjKindRaw, subjNum, direction, cellLiteral] = match;
395
468
  const addrKind = addrKindRaw.toLowerCase();
@@ -414,7 +487,7 @@ async function runToldFactTurn(match, { planHolder, memoryDir, cache, gameConfig
414
487
  }
415
488
 
416
489
  const targetCellId = cellId(targetCell.x, targetCell.y);
417
- const toldFacts = [{ subject: subjectId, toAgent: addresseeId, cell: targetCellId, turn: state.turnCount + 1 }];
490
+ const toldFacts = [{ subject: subjectId, toAgent: addresseeId, cell: targetCellId, turn: state.tickCount + 1 }];
418
491
  return runTickAndRender({
419
492
  planHolder, memoryDir, cache, toldFacts, gameConfig,
420
493
  addressedNote: `told the ${addresseeId} the ${subjectId} is at ${targetCellId}`,
@@ -487,13 +560,12 @@ async function spiderFlyContextAnswer(line, { memoryDir }) {
487
560
 
488
561
  /**
489
562
  * The whole spider-and-fly lane for one turn: the opening moves, the stop
490
- * command, the addressed spatial teach-frame (§6.1), the bare tick command,
491
- * and the one-at-a-time declines across the shared plan slot both other
492
- * lanes already implement pairwise. Returns { text, lane, note, goal?,
493
- * miss? } or null when the turn is not this lane's to answer — an
494
- * unaddressed aside (e.g. "where is the spider") falls through to the
495
- * ordinary lanes unchanged, board untouched (§6.2 no special-cased
496
- * spider-fly code path for plain questions).
563
+ * command, the addressed spatial teach-frame, the bare tick command, and the
564
+ * one-at-a-time declines across the shared plan slot the other lanes already
565
+ * implement pairwise. Returns { text, lane, note, goal?, miss? } or null when
566
+ * the turn is not this lane's to answer — an unaddressed aside (e.g. "where is
567
+ * the spider") falls through to the ordinary lanes unchanged, board untouched,
568
+ * with no special-cased code path for plain questions.
497
569
  */
498
570
  export async function spiderFlyTurn(line, { planHolder, memoryDir, env, cache = null, isPlanFrameLine = () => false, gameConfig = DEFAULT_GAME_CONFIG }) {
499
571
  const slot = planHolder?.state ?? null;
@@ -1,18 +1,17 @@
1
- // spider-fly-viz.mjs — the spider-and-fly full-screen page (PLAN_SPIDER_FLY.md
2
- // §8/§9/§11): a self-contained document shaped exactly like ledger-viz.mjs/
3
- // plan-viz.mjs — one inlined <style> (importing viz-theme.mjs's shared
4
- // tokens), behaviour as an inlined IIFE but unlike those two, almost
5
- // nothing is embedded as build-time data: the whole game is LIVE client-side
6
- // state (spider-fly-browser-entry.mjs's createSpiderFlySession), so
7
- // renderSpiderFlyHtml only needs the page's own static grid geometry (which
8
- // never changes) plus a title. The engine, sprite resolver and chat turn
9
- // engine all arrive via ./spider-fly-browser.bundle.js, referenced with a
10
- // plain same-origin <script src> the same sibling-file arrangement
11
- // index.html already uses for chat-browser.bundle.js (both are the FULL
12
- // turn engine, both generated fresh per build, neither meant to be
13
- // committed), not memory-ask-browser.bundle.js's inlined-text arrangement
14
- // (that bundle is small and inlined specifically so ledger.html stays
15
- // portable on its own — that reason doesn't apply here).
1
+ // spider-fly-viz.mjs — the spider-and-fly full-screen page: a self-contained
2
+ // document shaped exactly like ledger-viz.mjs/plan-viz.mjs — one inlined <style>
3
+ // (importing viz-theme.mjs's shared tokens), behaviour as an inlined IIFE — but
4
+ // unlike those two, almost nothing is embedded as build-time data: the whole
5
+ // game is LIVE client-side state (spider-fly-browser-entry.mjs's
6
+ // createSpiderFlySession), so renderSpiderFlyHtml only needs the page's own
7
+ // static grid geometry (which never changes) plus a title. The engine, sprite
8
+ // resolver and chat turn engine all arrive via ./spider-fly-browser.bundle.js,
9
+ // referenced with a plain same-origin <script src> — the same sibling-file
10
+ // arrangement index.html already uses for chat-browser.bundle.js (both are the
11
+ // FULL turn engine, both generated fresh per build, neither meant to be
12
+ // committed), not memory-ask-browser.bundle.js's inlined-text arrangement (that
13
+ // bundle is small and inlined specifically so ledger.html stays portable on its
14
+ // own that reason doesn't apply here).
16
15
  //
17
16
  // renderSpiderFlyHtml() is pure: no I/O, deterministic output for identical
18
17
  // input. scripts/build-demo-site.mjs calls it directly and writes the result
@@ -37,7 +36,6 @@ import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embed
37
36
  import { createTicker } from "./viz-ticker.mjs";
38
37
  import { loadWinkVendor } from "./viz-boot.mjs";
39
38
  import { GRID_SIZE, WEB_HOME, WEB_RADIUS, isInWebBlock, cellId, agentKindOf } from "../domain/spider-fly-world.mjs";
40
- import { FLY_INITIAL_MASS, EGG_LAY_MASS_THRESHOLD } from "./spider-fly.mjs";
41
39
  import { believedFactSentence } from "./spider-fly-turn.mjs";
42
40
  import { DEFAULT_GAME_CONFIG, massScaleFor } from "../domain/game-config.mjs";
43
41
  import { resolveSpriteRequest } from "../domain/sprite-request.mjs";
@@ -69,11 +67,11 @@ function webCellIds() {
69
67
  /**
70
68
  * Reconstruct the spider's remaining silk-thread path — the sequence of
71
69
  * cell ids from its CURRENT cell (after this tick's one executed step) to
72
- * wherever `findActionPath` was aiming — from spider-fly.mjs's own
70
+ * wherever `findActionPath` was aiming — from spider-fly-turn.mjs's own
73
71
  * `agents[spiderId]` shape ({ cell, plan }), where `plan` is the FULL
74
72
  * direction list `findActionPath` returned (the step already taken this
75
73
  * tick, `plan[0]`, plus every step still to come). Only a spider with a
76
- * REAL multi-step plan draws a thread at all: spider-fly.mjs's own
74
+ * REAL multi-step plan draws a thread at all: spider-fly-turn.mjs's own
77
75
  * `planSpiderPath` only ever returns one when the believed fly cell sits
78
76
  * inside the web block (its `isGoal`'s own requirement) — most ticks the
79
77
  * spider is greedily closing distance with no such plan, and this
@@ -104,7 +102,7 @@ export function threadCellsForSpiderPlan(agents, geometry) {
104
102
  }
105
103
 
106
104
  /** The sprite-facing rotation (degrees) for one agent this tick, driven by
107
- * its CURRENT plan's first step (spider-fly.mjs's own `agents[id].plan`),
105
+ * its CURRENT plan's first step (spider-fly-turn.mjs's own `agents[id].plan`),
108
106
  * never its actual next move — the two usually coincide, but re-planning
109
107
  * fresh every tick means they can visibly diverge as a plan gets clobbered
110
108
  * and replaced, which is the intended, honest demonstration of "plans get
@@ -126,19 +124,17 @@ export function facingDegreesFor(plan, previousDegrees) {
126
124
  }
127
125
 
128
126
  /**
129
- * The updated corpse set for one redraw (§A.2.5 — visual-only, entirely
130
- * client-side; the actual starve/eat removal already happened in the
131
- * engine). Every id present in `prevAgents` but absent from `agents` died
132
- * THIS tick — eaten or starved are the only two ways an agent ever leaves
133
- * the engine's own returned roster — and is added at its last-known cell
134
- * and class; every corpse already older than `lingerTurns` past its own
135
- * death turn is dropped first, so the set never grows without bound.
136
- * Returns a plain `{ [id]: { cls, cell, diedAtTurn } }` map. Pure.
137
- * `lingerTurns` defaults to a literal 4 (not the module-level
138
- * CORPSE_LINGER_TURNS constant) so this function stays fully
139
- * `.toString()`-splice safe every real caller (both the inlined page and
140
- * CORPSE_LINGER_TURNS's own callers elsewhere in this module) passes it
141
- * explicitly anyway.
127
+ * The updated corpse set for one redraw — visual-only, entirely client-side;
128
+ * the actual starve/eat removal already happened in the engine. Every id
129
+ * present in `prevAgents` but absent from `agents` died THIS tick — eaten or
130
+ * starved are the only two ways an agent ever leaves the engine's own returned
131
+ * roster — and is added at its last-known cell and class; every corpse already
132
+ * older than `lingerTurns` past its own death turn is dropped first, so the set
133
+ * never grows without bound. Returns a plain `{ [id]: { cls, cell, diedAtTurn }
134
+ * }` map. Pure. `lingerTurns` defaults to a literal 4 (not the module-level
135
+ * CORPSE_LINGER_TURNS constant) so this function stays fully `.toString()`-splice
136
+ * safe every real caller (both the inlined page and CORPSE_LINGER_TURNS's own
137
+ * callers elsewhere in this module) passes it explicitly anyway.
142
138
  */
143
139
  export function nextCorpses(prevCorpses, prevAgents, agents, turn, lingerTurns = 4) {
144
140
  const out = {};
@@ -167,9 +163,9 @@ export function nextCorpses(prevCorpses, prevAgents, agents, turn, lingerTurns =
167
163
  * back to the flat SPRITE_REGISTRY, unchanged from before this module
168
164
  * existed).
169
165
  * `?preview=1` on the page's own URL switches it into the small, auto-
170
- * playing, non-interactive mode the home page's hero iframe embeds (§11)
171
- * one file serves both the hero and the "open full-screen" link, matching
172
- * how ledger.html/plan.html are each one file embedded two ways.
166
+ * playing, non-interactive mode the home page's hero iframe embeds one file
167
+ * serves both the hero and the "open full-screen" link, matching how
168
+ * ledger.html/plan.html are each one file embedded two ways.
173
169
  * `engineBundleJs` (the built spider-fly-browser bundle's own text) inlines
174
170
  * the engine into the page instead of the sibling `<script src>`, for the
175
171
  * CLI's standalone export — one downloadable file that runs from file://
@@ -188,13 +184,11 @@ export function renderSpiderFlyHtml({ title = DEFAULT_TITLE, spriteTemplates = [
188
184
  previewMaxTurns: PREVIEW_MAX_TURNS,
189
185
  tickWaitMs: TICK_WAIT_MS,
190
186
  corpseLingerTurns: CORPSE_LINGER_TURNS,
191
- maxFlyMass: FLY_INITIAL_MASS,
192
- // The spider's mass bar now scales against the EGG-LAY threshold, not
193
- // its own starting mass "how close to laying" is the meaningful cap
194
- // to visualize under the new mass-gated lay mechanic (§A.2.2); the old
195
- // denominator (a flat starting mass) said nothing about progress toward
196
- // the spider's actual goal.
197
- maxSpiderMass: EGG_LAY_MASS_THRESHOLD,
187
+ maxFlyMass: DEFAULT_GAME_CONFIG.spiderFly.flyInitialMass,
188
+ // The spider's mass bar scales against the egg-lay threshold rather than
189
+ // its own starting mass: "how close to laying" is what a viewer wants to
190
+ // read, and a flat starting mass says nothing about progress toward it.
191
+ maxSpiderMass: DEFAULT_GAME_CONFIG.spiderFly.eggLayMassThreshold,
198
192
  defaultConfig: DEFAULT_GAME_CONFIG.spiderFly,
199
193
  spriteTemplates,
200
194
  });
@@ -347,11 +341,11 @@ ${THEME_TOKENS_CSS}
347
341
  .hud-id.spider { color: var(--taught); } .hud-id.fly { color: var(--fly); } .hud-id.egg { color: var(--muted); }
348
342
  .hud-goal { font-size: .85rem; }
349
343
  .hud-plan, .hud-belief { font-family: ${MONO_STACK}; font-size: .66rem; color: var(--muted); margin-top: .25rem; line-height: 1.4; padding-left: .5rem; border-left: 2px solid var(--chrome-accent); }
350
- /* the click-expand facts panel (§28): beside the clicked spider/fly's own
351
- row, never a separate popover or a second panel elsewhere on the page —
352
- the same believedCellOf/beliefSnapshotFor read path spider-fly.mjs
353
- already computes every tick for planning, rendered here as full
354
- sentences instead of the compact believes:-line above. */
344
+ /* the click-expand facts panel: beside the clicked spider/fly's own row,
345
+ never a separate popover or a second panel elsewhere on the page — the same
346
+ believedCellOf/beliefSnapshotFor read path spider-fly-turn.mjs already
347
+ computes every tick for planning, rendered here as full sentences instead of
348
+ the compact believes:-line above. */
355
349
  .hud-detail { flex: 1 1 auto; min-width: 0; font-family: ${MONO_STACK}; font-size: .64rem; line-height: 1.5; color: var(--chrome-well-ink); background: var(--chrome-well); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-inset); border-radius: 2px; padding: .35rem .5rem; }
356
350
  .hud-detail-title { text-transform: uppercase; letter-spacing: .06em; opacity: .75; margin-bottom: .2rem; }
357
351
  /* A stat-readout track: an inset "LCD" well, filled with a segmented pip
@@ -381,11 +375,11 @@ ${THEME_TOKENS_CSS}
381
375
  .pill:hover:not(:disabled) { border-color: var(--chrome-accent); }
382
376
  .pill:disabled { opacity: .45; cursor: default; }
383
377
  .pill[data-role="addr"].active { border-color: var(--taught); color: var(--taught); }
384
- /* The dynamic deception-pill rail (§A.2.4): a true/false tag shown ONLY
385
- here, via border style/color and a small human-facing glyph — the
386
- submitted sentence itself (data-sentence, filled into #chatq on click)
387
- never carries the tag, so a clicked pill is indistinguishable from a
388
- hand-typed claim once it's in the input. */
378
+ /* The dynamic deception-pill rail: a true/false tag shown ONLY here, via
379
+ border style/color and a small human-facing glyph — the submitted sentence
380
+ itself (data-sentence, filled into #chatq on click) never carries the tag,
381
+ so a clicked pill is indistinguishable from a hand-typed claim once it's in
382
+ the input. */
389
383
  .dynpills { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .5rem; padding-top: .5rem; border-top: 1px solid var(--chrome-edge-lo); }
390
384
  .dynpills:empty { display: none; padding-top: 0; border-top: none; }
391
385
  .pill[data-role="dyn-addr"][data-active="1"] { border-color: var(--taught); color: var(--taught); }
@@ -748,13 +742,13 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
748
742
  lastAgents = agents;
749
743
  }
750
744
 
751
- // ---- corpses (§A.2.5): visual-only — the actual starve/eat removal
752
- // already happened in the engine before this redraw ever sees "agents".
753
- // A corpse sinks to the bottom row of the SAME board column it died in
754
- // ("drops to the bottom" — spider-fly-world.mjs's own x/y convention,
755
- // y = GRID_SIZE is the bottom row) and fades out once CORPSE_LINGER_TURNS
756
- // passes, drawn in the same sprite layer as every live sprite, just
757
- // grayscaled and non-interactive (see the .sprite.corpse CSS rule).
745
+ // ---- corpses: visual-only — the actual starve/eat removal already happened
746
+ // in the engine before this redraw ever sees "agents". A corpse sinks to the
747
+ // bottom row of the SAME board column it died in ("drops to the bottom" —
748
+ // spider-fly-world.mjs's own x/y convention, y = GRID_SIZE is the bottom row)
749
+ // and fades out once CORPSE_LINGER_TURNS passes, drawn in the same sprite
750
+ // layer as every live sprite, just grayscaled and non-interactive (see the
751
+ // .sprite.corpse CSS rule).
758
752
  function renderCorpses() {
759
753
  for (const id of Object.keys(corpseEls)) {
760
754
  if (!corpses[id]) { corpseEls[id].remove(); delete corpseEls[id]; }
@@ -796,12 +790,12 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
796
790
  return '<div class="hud-plan">plan: ' + text + ".</div>";
797
791
  }
798
792
 
799
- // The agent's own current world-knowledge-graph snapshot (§A.2.7) — every
800
- // OTHER live individual it believes it knows the position of, or
801
- // "unseen" when it has no belief at all. Deliberately never ground truth:
802
- // this is what the agent would actually ACT on, which a false pill or a
803
- // told fact can make visibly wrong compared to where that individual
804
- // really is — the gap IS the demonstration.
793
+ // The agent's own current world-knowledge-graph snapshot — every OTHER live
794
+ // individual it believes it knows the position of, or "unseen" when it has no
795
+ // belief at all. Deliberately never ground truth: this is what the agent
796
+ // would actually ACT on, which a false pill or a told fact can make visibly
797
+ // wrong compared to where that individual really is — the gap IS the
798
+ // demonstration.
805
799
  function beliefLineHtml(belief) {
806
800
  const entries = Object.entries(belief || {});
807
801
  if (!entries.length) return "";
@@ -1026,12 +1020,12 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1026
1020
  chatqEl.addEventListener("input", refreshPills);
1027
1021
  refreshPills();
1028
1022
 
1029
- // ---- deception pills (§A.2.4): a SEPARATE dynamic rail, alongside (never
1030
- // replacing) the static one above. tmct.page.pillsForSpiderFly is the
1031
- // exact same pure function spider-fly-turn.mjs exports — this page never
1032
- // reimplements the true/false claim logic, only renders its output and
1033
- // fills #chatq on click, same click-to-fill discipline as every other
1034
- // pill on this page (never auto-submits).
1023
+ // ---- deception pills: a SEPARATE dynamic rail, alongside (never replacing)
1024
+ // the static one above. tmct.page.pillsForSpiderFly is the exact same pure
1025
+ // function spider-fly-turn.mjs exports — this page never reimplements the
1026
+ // true/false claim logic, only renders its output and fills #chatq on click,
1027
+ // same click-to-fill discipline as every other pill on this page (never
1028
+ // auto-submits).
1035
1029
  function renderDynamicPills() {
1036
1030
  if (!session || !Object.keys(lastAgents).length) { dynamicPillsEl.innerHTML = ""; return; }
1037
1031
  const result = tmct.page.pillsForSpiderFly(lastAgents, selectedAddresseeId, {});
@@ -34,7 +34,7 @@
34
34
  // page's own edit mode already makes.
35
35
  import { appendFacts, loadMemory, readFactRows } from "../adapters/memory/core.mjs";
36
36
  import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
37
- import { correctMisspellings } from "../domain/interpret/normalize.mjs";
37
+ import { correctMisspellings, QUESTION_LEAD_RE } from "../domain/interpret/normalize.mjs";
38
38
  import {
39
39
  classMassFacts, foldWorldState, freshObjectId, snapshotSubject, worldActionRows, worldRelook,
40
40
  } from "./adventure.mjs";
@@ -44,8 +44,8 @@ import {
44
44
  // word earlier, run over the closed misspelling repair so a typo'd "wat is the
45
45
  // lamp" goes back to the question side rather than reading as a declarative.
46
46
  // Both mirror chat.mjs's own teach lane, which stands the whole lane down on
47
- // either — a world teach has exactly the same reason to.
48
- const QUESTION_LEAD_RE = /^(?:what|who|which|where|when|why|how|is|are|do|does|did|can|could|should|would|will|has|have)\b/i;
47
+ // either — a world teach has exactly the same reason to (see
48
+ // normalize.mjs's QUESTION_LEAD_RE).
49
49
 
50
50
  // A subject with no referent of its own. The generic "X is a Y." fallback in
51
51
  // both sentence tables would otherwise read "There is a book in the study" as
@@ -43,6 +43,7 @@ import {
43
43
  import { parseEntities } from "../../domain/codegraph.mjs";
44
44
  import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
45
45
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
46
+ import { DEFAULT_GAME_CONFIG } from "../../domain/game-config.mjs";
46
47
  import { foldWorldState, worldDigestRows, roomAffordances, worldActionRows, snapshotSubject } from "../../services/adventure.mjs";
47
48
  import { runAdventureAutoplayTick, exposedFacts } from "../../services/adventure-autoplay.mjs";
48
49
  import { parseWorldEditorText, planWorldEditorSync } from "../../services/adventure-editor.mjs";
@@ -66,8 +67,13 @@ import { openPersistedStore } from "./idb-persist.mjs";
66
67
  * already carries them, plus every @turnN state row played since, so the
67
68
  * world resumes exactly where the fold left it. `restoredVisitedRoomIds`
68
69
  * carries the matching exposure set forward; without it, only the player's
69
- * current room counts as visited. */
70
- export async function createAdventureSession(worldPayload, { restoredPayload = null, restoredVisitedRoomIds = null } = {}) {
70
+ * current room counts as visited.
71
+ *
72
+ * `getTeachEnabled` (optional) is read fresh on every turn, never once at
73
+ * boot — the page's own teach checkbox, so flipping it mid-game changes the
74
+ * very next line's reading without a reset. Defaults to always-off, which is
75
+ * DEFAULT_GAME_CONFIG.adventure.teach's own default. */
76
+ export async function createAdventureSession(worldPayload, { restoredPayload = null, restoredVisitedRoomIds = null, getTeachEnabled = () => false } = {}) {
71
77
  const memoryDir = createInMemoryStore();
72
78
  const tag = `world:${worldPayload.name}`;
73
79
  if (restoredPayload) {
@@ -134,7 +140,10 @@ export async function createAdventureSession(worldPayload, { restoredPayload = n
134
140
  const turnSession = createTurnSession({
135
141
  memoryDir, graph: codeGraph, lexicon, sessionId,
136
142
  vocabHint: 'Try a world question ("where is the key"), or teach me: "remember: the moat is a ditch".',
137
- buildExtraOptions: () => ({ planState: planHolder.state }),
143
+ buildExtraOptions: () => ({
144
+ planState: planHolder.state,
145
+ gameConfig: { ...DEFAULT_GAME_CONFIG, adventure: { ...DEFAULT_GAME_CONFIG.adventure, teach: getTeachEnabled() } },
146
+ }),
138
147
  captureExtraState: async (result) => {
139
148
  if ("planState" in result) planHolder.state = result.planState;
140
149
  const here = foldWorldState(worldActionRows(readFactRows(await loadMemory(memoryDir)))).placements.get("player")?.object ?? null;