@polycode-projects/the-mechanical-code-talker 3.2.0 → 3.3.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.
@@ -183,7 +183,13 @@ const PLACEMENT_PREDICATES = new Set([
183
183
  // which surface a thing rests against.
184
184
  const POSITION_PREDICATES = new Set(["mgx:on-top-of", "mgx:on-plane", "mgx:under"]);
185
185
  const OPEN_PREDICATE = "mgx:is-open";
186
+ const MASS_PREDICATE = "mgx:hasMass";
187
+ const KNOWS_ABOUT_PREDICATE = "mgx:knows-about";
186
188
  const EXIT_PREDICATE_RE = /^mgx:has-exit-([a-z]+)$/;
189
+ // Where an eaten thing is placed. The world has no other way to say "out of
190
+ // play", and no room can be called this, so the sentinel is the whole
191
+ // convention: the readers below skip it exactly as they skip a hiding place.
192
+ const CONSUMED_PLACE = "eaten";
187
193
 
188
194
  /** The rows a live world's STATE fold may see: those the world itself wrote
189
195
  * (provenance empty, or `world:*` — the loaded shard and its @turn
@@ -201,12 +207,13 @@ export function worldActionRows(rows) {
201
207
 
202
208
  /** Fold fact rows into the CURRENT world state: per subject, the newest
203
209
  * placement (base row = turn 0, @turnN snapshots override), the newest
204
- * open/closed state, the exit map, and the turn counter (the largest @turnN
205
- * suffix written so far — derived, never stored). Pure. */
210
+ * open/closed state, the newest mass, the exit map, and the turn counter (the
211
+ * largest @turnN suffix written so far — derived, never stored). Pure. */
206
212
  export function foldWorldState(factRows) {
207
213
  const placements = new Map(); // subject -> { predicate, object, turn }
208
214
  const positions = new Map(); // subject -> { predicate, object, turn }
209
215
  const openness = new Map(); // subject -> { open, turn }
216
+ const masses = new Map(); // subject -> { value, turn }
210
217
  const exits = new Map(); // room -> Map(direction -> room)
211
218
  let turnCount = 0;
212
219
  for (const row of factRows || []) {
@@ -229,13 +236,20 @@ export function foldWorldState(factRows) {
229
236
  if (!prior || turn >= prior.turn) openness.set(base, { open: row.object === "true", turn });
230
237
  continue;
231
238
  }
239
+ if (row.predicate === MASS_PREDICATE) {
240
+ const value = Number(row.object);
241
+ if (!Number.isFinite(value)) continue; // masses hold numbers; an unparsable one is no mass at all
242
+ const prior = masses.get(base);
243
+ if (!prior || turn >= prior.turn) masses.set(base, { value, turn });
244
+ continue;
245
+ }
232
246
  const exit = EXIT_PREDICATE_RE.exec(row.predicate);
233
247
  if (exit && !m) {
234
248
  if (!exits.has(row.subject)) exits.set(row.subject, new Map());
235
249
  exits.get(row.subject).set(exit[1], row.object);
236
250
  }
237
251
  }
238
- return { placements, positions, openness, exits, turnCount };
252
+ return { placements, positions, openness, masses, exits, turnCount };
239
253
  }
240
254
 
241
255
  /** A subject's CURRENT within-room position, or null. A position goes stale
@@ -305,15 +319,17 @@ function visibleRoomOf(thing, { rows, state }) {
305
319
  if (!place || place.predicate === "mgx:hidden-in") return null;
306
320
  if (place.predicate === "mgx:currently-in" || isTyped(rows, place.object, "room")) return place.object;
307
321
  const holder = place.object;
308
- if (holder === "player") return null; // carried, not on show in a room
322
+ // A non-container holder is a character carrying the thing, whoever they
323
+ // are — carried, so not on show in the room they stand in.
324
+ if (!isContainer(rows, holder)) return null;
309
325
  if (!state.openness.get(holder)?.open) return null;
310
326
  const holderPlace = state.placements.get(holder);
311
327
  return holderPlace && holderPlace.predicate !== "mgx:hidden-in" ? holderPlace.object : null;
312
328
  }
313
329
 
314
- const carriedByPlayer = (state, thing) => {
330
+ const carriedBy = (state, thing, holder) => {
315
331
  const place = state.placements.get(thing);
316
- return !!place && place.predicate === "mgx:located-in" && place.object === "player";
332
+ return !!place && place.predicate === "mgx:located-in" && place.object === holder;
317
333
  };
318
334
 
319
335
  /** True when `object` is never a real placed game entity (no entry in
@@ -337,13 +353,13 @@ function backgroundOnlyMention(rows, state, object) {
337
353
  * would then refuse. A locked container offers "unlock", never "open" (that
338
354
  * would only decline); an already-open one offers neither, since there is
339
355
  * nothing left for either verb to do. Pure. */
340
- export function roomAffordances(rows, state, here) {
356
+ export function roomAffordances(rows, state, here, actingSubject = "player") {
341
357
  const actions = [];
342
358
  for (const direction of state.exits.get(here)?.keys() ?? []) {
343
359
  actions.push(`go ${direction}`);
344
360
  }
345
361
  for (const subject of [...state.placements.keys()].sort()) {
346
- if (subject === "player") continue;
362
+ if (subject === actingSubject) continue;
347
363
  if (visibleRoomOf(subject, { rows, state }) !== here) continue;
348
364
  const place = state.placements.get(subject);
349
365
  const container = isContainer(rows, subject);
@@ -433,6 +449,47 @@ async function writeWorldTurn(memoryDir, world, k, facts, cache) {
433
449
  if (cache) cache.rows = null;
434
450
  }
435
451
 
452
+ // ---- what a character knows, and who it heard it from -----------------------
453
+ //
454
+ // A character telling another character about something, or looking at
455
+ // something itself, leaves a REAL fact behind: an mgx:knows-about edge the
456
+ // hearer carries from that turn on, readable by personKnowledgeLines exactly
457
+ // like a world-authored one. Nothing here is per-tick or in-memory — one
458
+ // animal can walk off, come back ten turns later, and still know what it was
459
+ // told.
460
+ //
461
+ // These deliberately bypass writeWorldTurn. That tags everything
462
+ // `world:<name>:turnN`, which credits the WORLD for the claim; a character's
463
+ // testimony belongs to the character, so it carries its own
464
+ // `mud:<character>:turnN` tag and lands on that character's own Source and
465
+ // trust track record. The side effect is that worldActionRows filters these
466
+ // out of the playable state fold, which is what you want — being told about a
467
+ // stone must never move the stone.
468
+
469
+ const characterTestimonyTag = (character, k) => `mud:${character}:turn${k}`;
470
+
471
+ async function appendTestimony(memoryDir, { knower, source, thing, k, cache }) {
472
+ await appendFacts(memoryDir, [{
473
+ subject: knower, predicate: KNOWS_ABOUT_PREDICATE, object: thing,
474
+ provenance: characterTestimonyTag(source, k),
475
+ }]);
476
+ if (cache) cache.rows = null;
477
+ }
478
+
479
+ /** Record that `teller` told `asker` about `thing` on turn `k`. The asker is
480
+ * the subject — it is the one who now knows — and the teller is named in the
481
+ * provenance, so the claim corroborates the teller's Source, not the asker's. */
482
+ export async function recordTold(memoryDir, { asker, teller, thing, k, cache = null }) {
483
+ return appendTestimony(memoryDir, { knower: asker, source: teller, thing, k, cache });
484
+ }
485
+
486
+ /** Record that `observer` examined `thing` on turn `k`. The observer is both
487
+ * the subject and the provenance's character: it learned this by looking, so
488
+ * it is its own source for it. */
489
+ export async function recordExamined(memoryDir, { observer, thing, k, cache = null }) {
490
+ return appendTestimony(memoryDir, { knower: observer, source: observer, thing, k, cache });
491
+ }
492
+
436
493
  // ---- the look/inventory digest ----------------------------------------------
437
494
  //
438
495
  // "look" and "what am I carrying" are generateCompletion calls (the shipped
@@ -448,6 +505,9 @@ async function writeWorldTurn(memoryDir, world, k, facts, cache) {
448
505
 
449
506
  const VIEW_EXCLUDED_PREDICATES = new Set([
450
507
  "mgx:hidden-in", "mgx:is-open", "mgx:is-npc", "mgx:is-container",
508
+ // A bare number reads as an untranslated triple in room prose ("Mole-1
509
+ // mgx:hasMass 8"). Mass reaches a player through the verbs that change it.
510
+ MASS_PREDICATE,
451
511
  "mgx:unlocks-with", "mgx:acts-on-turn", "mgx:acts-toward",
452
512
  // is-objective is an internal marker for auto-play's goal inference — the
453
513
  // same information the opening narration already tells a human player in
@@ -457,7 +517,7 @@ const VIEW_EXCLUDED_PREDICATES = new Set([
457
517
  // Staff knowledge is the whole puzzle: a room look must never leak
458
518
  // "Gardener knows-where letter" or the game is spoiled. It reaches the
459
519
  // player only through the talk lane, which resolves each pointer live.
460
- "mgx:knows-where", "mgx:knows-objective", "mgx:knows-about",
520
+ "mgx:knows-where", "mgx:knows-objective", KNOWS_ABOUT_PREDICATE,
461
521
  // Class-schema facts describe the ontology, not the scene. default-contains
462
522
  // is already materialized into real instances at load; default-plane and
463
523
  // subClassOf drive positional rendering by their own readers, and read as
@@ -497,7 +557,7 @@ function isNonWorldSourced(row) {
497
557
  * subjects so the pipeline's sentence splitter sees real sentences. Room text
498
558
  * is world-sourced only — a merged corpus's overlap on a room's own vocabulary
499
559
  * never leaks into the description. Pure. */
500
- export function worldDigestRows(rows, state) {
560
+ export function worldDigestRows(rows, state, actingSubject = "player") {
501
561
  const out = [];
502
562
  const seen = new Set();
503
563
  const push = (subject, phrase, object) => {
@@ -506,13 +566,19 @@ export function worldDigestRows(rows, state) {
506
566
  seen.add(key);
507
567
  out.push({ subject: sentenceCase(subject), predicate: phrase, object });
508
568
  };
569
+ // Whoever holds a located-in thing is carrying it rather than housing it,
570
+ // and the cast are exactly the individuals the world places with
571
+ // currently-in — props ride located-in/fixed-in/stands-locked-in, rooms are
572
+ // never placed at all.
573
+ const isCarryingCharacter = (holder) =>
574
+ isTyped(rows, holder, "person") || state.placements.get(holder)?.predicate === "mgx:currently-in";
509
575
  for (const [subject, place] of state.placements) {
510
- if (place.predicate === "mgx:hidden-in") continue;
511
- if (place.predicate === "mgx:located-in" && place.object === "player") {
512
- push("player", "carries the", subject);
576
+ if (place.predicate === "mgx:hidden-in" || place.object === CONSUMED_PLACE) continue;
577
+ if (place.predicate === "mgx:located-in" && place.object === actingSubject) {
578
+ push(actingSubject, "carries the", subject);
513
579
  continue;
514
580
  }
515
- if (place.predicate === "mgx:located-in" && isTyped(rows, place.object, "person")) {
581
+ if (place.predicate === "mgx:located-in" && isCarryingCharacter(place.object)) {
516
582
  push(place.object, "carries the", subject);
517
583
  continue;
518
584
  }
@@ -530,14 +596,18 @@ export function worldDigestRows(rows, state) {
530
596
  // already assumes, so it is left unsaid.
531
597
  const POSITION_PHRASE = { "mgx:on-top-of": "is on the", "mgx:on-plane": "is on the", "mgx:under": "is under the" };
532
598
  for (const [subject, place] of state.placements) {
533
- if (place.predicate === "mgx:hidden-in" || place.object === "player") continue;
599
+ if (place.predicate === "mgx:hidden-in" || place.object === actingSubject || place.object === CONSUMED_PLACE) continue;
534
600
  const pos = currentPosition(state, subject);
535
601
  if (pos && POSITION_PHRASE[pos.predicate]) { push(subject, POSITION_PHRASE[pos.predicate], pos.object); continue; }
536
602
  const plane = classDefaultPlane(rows, subject);
537
603
  if (plane && plane !== "floor") push(subject, "is usually on the", plane);
538
604
  }
605
+ const consumed = new Set([...state.placements]
606
+ .filter(([, place]) => place.object === CONSUMED_PLACE)
607
+ .map(([subject]) => subject));
539
608
  for (const row of rows || []) {
540
609
  if (SNAPSHOT_RE.test(row.subject)) continue; // folded above
610
+ if (consumed.has(row.subject)) continue; // eaten, so out of the world entirely
541
611
  // Room text comes from the world source only. A merged corpus overlaps a
542
612
  // room's own vocabulary ("library rdfs:subClassOf literary study"), and
543
613
  // without this those rows leak into the room description as stray sentences.
@@ -564,9 +634,9 @@ export function worldDigestRows(rows, state) {
564
634
  * the class hierarchy renders that as its own is-a chain instead. A carried
565
635
  * object surfaces through the "carries the" line the digest already produces.
566
636
  * Pure. */
567
- export function objectLookProperties(rows, state, object) {
637
+ export function objectLookProperties(rows, state, object, actingSubject = "player") {
568
638
  const subjectCased = sentenceCase(object);
569
- return worldDigestRows(rows, state)
639
+ return worldDigestRows(rows, state, actingSubject)
570
640
  .filter((r) => (r.subject === subjectCased && r.predicate !== "is a" && r.predicate !== "is an")
571
641
  || (r.predicate === "carries the" && r.object === object))
572
642
  .map((r) => `${r.subject} ${r.predicate} ${r.object}.`);
@@ -595,8 +665,8 @@ export function objectClassChain(rows, object) {
595
665
  return chain;
596
666
  }
597
667
 
598
- async function worldDigest(prompt, { memoryDir, memory, rows, state, graph }) {
599
- const view = worldDigestRows(rows, state);
668
+ async function worldDigest(prompt, { memoryDir, memory, rows, state, graph, actingSubject = "player" }) {
669
+ const view = worldDigestRows(rows, state, actingSubject);
600
670
  const store = {
601
671
  ...COMPLETIONS_STORE,
602
672
  readFactRows: () => view,
@@ -626,6 +696,51 @@ const answer = (text, note, { goal, miss = false } = {}) => ({
626
696
  text, note, lane: "game-answer", miss, ...(goal ? { goal } : {}),
627
697
  });
628
698
 
699
+ // A dug room needs the way back written too, and the exit vocabulary is only
700
+ // ever a direction word in a predicate name, so the pairing lives here.
701
+ const OPPOSITE_DIRECTION = new Map([
702
+ ["north", "south"], ["south", "north"],
703
+ ["east", "west"], ["west", "east"],
704
+ ["up", "down"], ["down", "up"],
705
+ ]);
706
+
707
+ // What a freshly dug room holds. The pool is placeholder scenery so a new room
708
+ // is never bare; a later workstream swaps it for the garden's real food
709
+ // content, which this module has no business naming.
710
+ const DIG_SPAWN_KINDS = ["root", "carrot", "worm"];
711
+ const DIG_SPAWN_MIN = 0;
712
+ const DIG_SPAWN_MAX = 2;
713
+
714
+ const FOOD_CLASS = "food";
715
+ // A shared reference mass standing in for per-species maxima until the game
716
+ // config carries them, and what an eaten thing is worth when the world wrote
717
+ // it no mass of its own.
718
+ const ASSUMED_FULL_MASS = 20;
719
+ const HUNGRY_FRACTION = 0.5;
720
+ const DEFAULT_FOOD_MASS = 1;
721
+
722
+ /** A stable small number for a string, so the same dig always opens the same
723
+ * room: this world writes no randomness anywhere, and a re-run that differed
724
+ * would make the fold's own history unreproducible. Pure. */
725
+ function stableIndex(seed, span) {
726
+ let h = 0;
727
+ for (const ch of String(seed)) h = (h * 31 + ch.codePointAt(0)) % 100003;
728
+ return h % span;
729
+ }
730
+
731
+ /** An unused id for a newly dug room, reading as the room it was dug from
732
+ * plus the direction ("garden-down"). A collision takes a numeric suffix, so
733
+ * digging never renames or overwrites a room that already stands. Pure. */
734
+ function freshRoomId(rows, here, direction) {
735
+ const base = `${here}-${direction}`;
736
+ const taken = (id) => (rows || []).some((r) => r.subject === id || r.object === id);
737
+ if (!taken(base)) return base;
738
+ for (let n = 2; n <= (rows || []).length + 2; n += 1) {
739
+ if (!taken(`${base}-${n}`)) return `${base}-${n}`;
740
+ }
741
+ return `${base}-${(rows || []).length + 3}`;
742
+ }
743
+
629
744
  /** A container's open/locked status, stated plainly, and (only once already
630
745
  * open) its visible contents — the one thing examine/talk's reused
631
746
  * worldDigest call never states on its own, since mgx:is-open is a
@@ -681,17 +796,29 @@ export function personKnowledgeLines(rows, state, person) {
681
796
  ? `you'll find the ${thing} in the ${place.object}.`
682
797
  : `the ${thing} is in the ${place.object}.`);
683
798
  }
684
- return { lines, aboutTopics: factObjects(rows, person, "mgx:knows-about") };
799
+ return { lines, aboutTopics: factObjects(rows, person, KNOWS_ABOUT_PREDICATE) };
800
+ }
801
+
802
+ /** The FOOD_CLASS things `person` durably knows about — from being told, or
803
+ * from having examined them itself (the mgx:knows-about facts
804
+ * recordTold/recordExamined write, read back exactly like
805
+ * personKnowledgeLines's own aboutTopics), filtered to whatever's
806
+ * objectClassChain reaches "food". Unlike personKnowledgeLines's topics, a
807
+ * food query has no per-topic sub-digest to hand back, so this returns the
808
+ * plain list of known food things rather than a {lines, topics} pair. Pure. */
809
+ export function personKnownFoodLines(rows, state, person) {
810
+ return factObjects(rows, person, KNOWS_ABOUT_PREDICATE)
811
+ .filter((thing) => objectClassChain(rows, thing).includes(FOOD_CLASS));
685
812
  }
686
813
 
687
814
  /** What a person can report from where they stand this turn — derived each
688
815
  * turn, never stored: who and what shares their room, each container's
689
816
  * open/locked status, and what unlocks a locked one there. Pure. */
690
- export function personRoomReport(rows, state, person) {
817
+ export function personRoomReport(rows, state, person, actingSubject = "player") {
691
818
  const room = state.placements.get(person)?.object ?? null;
692
819
  if (!room) return "";
693
820
  const here = [...state.placements.keys()]
694
- .filter((s) => s !== person && s !== "player" && visibleRoomOf(s, { rows, state }) === room)
821
+ .filter((s) => s !== person && s !== actingSubject && visibleRoomOf(s, { rows, state }) === room)
695
822
  .sort();
696
823
  const parts = [];
697
824
  if (here.length) parts.push(`here in the ${room}: the ${here.join(", the ")}.`);
@@ -706,11 +833,11 @@ export function personRoomReport(rows, state, person) {
706
833
  return parts.join(" ");
707
834
  }
708
835
 
709
- async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
836
+ export async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache, actingSubject = "player" }) {
710
837
  const memory = await loadMemory(memoryDir);
711
838
  const rows = readFactRows(memory);
712
839
  const state = foldWorldState(worldActionRows(rows));
713
- const here = state.placements.get("player")?.object ?? null;
840
+ const here = state.placements.get(actingSubject)?.object ?? null;
714
841
  const noteFor = (detail) => `ADVENTURE — ${detail}`;
715
842
 
716
843
  if (cmd.residue?.length) {
@@ -729,8 +856,8 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
729
856
  }
730
857
 
731
858
  if (cmd.verb === "look" && !cmd.object) {
732
- const digest = await worldDigest(here, { memoryDir, memory, rows, state, graph });
733
- const actions = roomAffordances(rows, state, here);
859
+ const digest = await worldDigest(here, { memoryDir, memory, rows, state, graph, actingSubject });
860
+ const actions = roomAffordances(rows, state, here, actingSubject);
734
861
  return answer(
735
862
  `${digest ?? `you are in the ${here}. Nothing more about it is written down yet.`}${affordanceSuffix(actions)}`,
736
863
  noteFor(`look — an extractive completions digest over the current world facts mentioning "${here}"; appended the room's roomAffordances action list`),
@@ -744,7 +871,7 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
744
871
  // null for anything held by the player) — examine and look still apply to
745
872
  // it, the same way "what am I carrying" already reads inventory contents.
746
873
  // talk has no carried exception: NPCs are never portable.
747
- const carried = (cmd.verb === "examine" || cmd.verb === "look") && carriedByPlayer(state, object);
874
+ const carried = (cmd.verb === "examine" || cmd.verb === "look") && carriedBy(state, object, actingSubject);
748
875
  // The room the player is standing in is never the SUBJECT of a placement
749
876
  // fact (only ever the OBJECT other things are placed in), so
750
877
  // visibleRoomOf(object) can never equal `here` for a room's own name —
@@ -786,7 +913,7 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
786
913
  // placed facts of its own, so it falls through to the examine digest below
787
914
  // (the same general-knowledge answer "what is a flower" gives).
788
915
  if (cmd.verb === "look" && !backgroundOnlyMention(rows, state, object)) {
789
- const propLines = objectLookProperties(rows, state, object);
916
+ const propLines = objectLookProperties(rows, state, object, actingSubject);
790
917
  const chain = objectClassChain(rows, object);
791
918
  const parts = [`you look closely at the ${object}.`];
792
919
  if (propLines.length) parts.push(propLines.join(" "));
@@ -805,10 +932,10 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
805
932
  const { lines, aboutTopics } = personKnowledgeLines(rows, state, object);
806
933
  const aboutLines = [];
807
934
  for (const topic of aboutTopics) {
808
- const digested = await worldDigest(topic, { memoryDir, memory, rows, state, graph });
935
+ const digested = await worldDigest(topic, { memoryDir, memory, rows, state, graph, actingSubject });
809
936
  if (digested) aboutLines.push(digested);
810
937
  }
811
- const report = personRoomReport(rows, state, object);
938
+ const report = personRoomReport(rows, state, object, actingSubject);
812
939
  const said = [...lines, ...aboutLines, report].filter(Boolean).join(" ");
813
940
  return answer(
814
941
  said ? `the ${object} says: ${said}` : `the ${object} has nothing to tell you right now.`,
@@ -816,7 +943,7 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
816
943
  { goal: `talk to the ${object}` },
817
944
  );
818
945
  }
819
- const digest = await worldDigest(object, { memoryDir, memory, rows, state, graph });
946
+ const digest = await worldDigest(object, { memoryDir, memory, rows, state, graph, actingSubject });
820
947
  const body = digest ?? `nothing more about the ${object} is written down yet.`;
821
948
  const containerNote = !person && isContainer(rows, object) ? ` ${containerStatusPhrase(object, { state })}` : "";
822
949
  // Framing follows the VERB the player typed, not the object's type: talking
@@ -858,8 +985,8 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
858
985
  const freshMemory = await loadMemory(memoryDir);
859
986
  const freshRows = readFactRows(freshMemory);
860
987
  const freshState = foldWorldState(worldActionRows(freshRows));
861
- const relookDigest = await worldDigest(playerRoomAfter, { memoryDir, memory: freshMemory, rows: freshRows, state: freshState, graph });
862
- const actions = roomAffordances(freshRows, freshState, playerRoomAfter);
988
+ const relookDigest = await worldDigest(playerRoomAfter, { memoryDir, memory: freshMemory, rows: freshRows, state: freshState, graph, actingSubject });
989
+ const actions = roomAffordances(freshRows, freshState, playerRoomAfter, actingSubject);
863
990
  const relook = `you are in the ${playerRoomAfter}. ${relookDigest ?? "Nothing more about it is written down yet."}${affordanceSuffix(actions)}`;
864
991
  return answer(
865
992
  `${text2} ${relook}`,
@@ -880,9 +1007,9 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
880
1007
  );
881
1008
  }
882
1009
  return commit(
883
- [{ subject: `player@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:currently-in", object: target }],
1010
+ [{ subject: `${actingSubject}@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:currently-in", object: target }],
884
1011
  `you go ${cmd.direction}. Now in the ${target}.`,
885
- `go — the taught "go" family fired; player moves ${here} -> ${target}`,
1012
+ `go — the taught "go" family fired; ${actingSubject} moves ${here} -> ${target}`,
886
1013
  `move through the world (now in the ${target})`,
887
1014
  target,
888
1015
  );
@@ -892,7 +1019,7 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
892
1019
  if (isTyped(rows, object, "room")) {
893
1020
  return answer(`you can't take the ${object} — it's a whole room.`, noteFor("take — the object is a room; declined"), { miss: true });
894
1021
  }
895
- if (carriedByPlayer(state, object)) {
1022
+ if (carriedBy(state, object, actingSubject)) {
896
1023
  return answer(`you're already carrying the ${object}.`, noteFor("take — already carried; declined"), { miss: true });
897
1024
  }
898
1025
  if (place && (place.predicate === "mgx:fixed-in" || place.predicate === "mgx:stands-locked-in") && place.object === here) {
@@ -916,7 +1043,7 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
916
1043
  return answer(`I don't see a ${object} here.`, noteFor(`take — ${object} isn't visible in the ${here}; declined, hidden things stay hidden`), { miss: true });
917
1044
  }
918
1045
  return commit(
919
- [{ subject: `${object}@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: "player" }],
1046
+ [{ subject: `${object}@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: actingSubject }],
920
1047
  `you take the ${object}.`,
921
1048
  `take — the taught "take" family fired; ${object} is now carried`,
922
1049
  `carry the ${object}`,
@@ -924,7 +1051,7 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
924
1051
  }
925
1052
 
926
1053
  if (cmd.verb === "drop" || cmd.verb === "give") {
927
- if (!carriedByPlayer(state, object)) {
1054
+ if (!carriedBy(state, object, actingSubject)) {
928
1055
  return answer(`you're not carrying the ${object}.`, noteFor(`${cmd.verb} — ${object} isn't carried; precondition declined by name`), { miss: true });
929
1056
  }
930
1057
  if (cmd.verb === "drop") {
@@ -947,6 +1074,125 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
947
1074
  );
948
1075
  }
949
1076
 
1077
+ if (cmd.verb === "dig") {
1078
+ const direction = cmd.direction;
1079
+ if (state.exits.get(here)?.get(direction)) {
1080
+ return answer(
1081
+ `there's already an exit ${direction} from the ${here}.`,
1082
+ noteFor(`dig — an mgx:has-exit-${direction} fact already stands on ${here}; declined, a dig never overwrites an exit`),
1083
+ { miss: true },
1084
+ );
1085
+ }
1086
+ const back = OPPOSITE_DIRECTION.get(direction);
1087
+ if (!back) {
1088
+ return answer(
1089
+ `I don't know which way back a ${direction} tunnel would run.`,
1090
+ noteFor(`dig — "${direction}" has no opposite to write the return exit with; declined by name`),
1091
+ { miss: true },
1092
+ );
1093
+ }
1094
+ const dug = freshRoomId(rows, here, direction);
1095
+ const spawnCount = DIG_SPAWN_MIN + stableIndex(dug, DIG_SPAWN_MAX - DIG_SPAWN_MIN + 1);
1096
+ const spawnedKinds = DIG_SPAWN_KINDS.slice(0, spawnCount);
1097
+ const spawned = spawnedKinds.map((kind) => `${kind}-${dug}`);
1098
+ return commit(
1099
+ [
1100
+ { subject: dug, predicate: "rdf:type", object: "room" },
1101
+ { subject: here, predicate: `mgx:has-exit-${direction}`, object: dug },
1102
+ { subject: dug, predicate: `mgx:has-exit-${back}`, object: here },
1103
+ // Typed to its OWN kind, not a flat "portable" — a spawned kind the
1104
+ // world already declares rdfs:subClassOf food (DIG_SPAWN_KINDS may
1105
+ // carry one) needs its real class reachable here for isFood's own
1106
+ // objectClassChain walk, or digging up "carrot-..." would still read
1107
+ // as inedible scenery.
1108
+ ...spawnedKinds.flatMap((kind, i) => ([
1109
+ { subject: spawned[i], predicate: "rdf:type", object: kind },
1110
+ { subject: spawned[i], predicate: "mgx:located-in", object: dug },
1111
+ ])),
1112
+ ],
1113
+ spawned.length
1114
+ ? `you dig ${direction} and open up a new room. In the loose earth: the ${spawned.join(", the ")}.`
1115
+ : `you dig ${direction} and open up a new room. There's nothing in it but bare earth.`,
1116
+ `dig — minted the room ${dug} with exits both ways (${direction} out, ${back} back)${spawned.length ? `, and ${spawned.length} object(s) in it` : ""}; digging spends the turn, so the digger stays in the ${here}`,
1117
+ `dig ${direction} out of the ${here}`,
1118
+ );
1119
+ }
1120
+
1121
+ if (cmd.verb === "eat") {
1122
+ const present = visibleRoomOf(object, { rows, state }) === here || carriedBy(state, object, actingSubject);
1123
+ if (!present) {
1124
+ return answer(
1125
+ `I don't see a ${object} here.`,
1126
+ noteFor(`eat — ${object} is neither visible in the ${here} nor carried; declined`),
1127
+ { miss: true },
1128
+ );
1129
+ }
1130
+ if (!objectClassChain(rows, object).includes(FOOD_CLASS)) {
1131
+ return answer(
1132
+ `the ${object} isn't food.`,
1133
+ noteFor(`eat — ${object}'s rdf:type/rdfs:subClassOf chain never reaches "${FOOD_CLASS}"; declined by name`),
1134
+ { miss: true },
1135
+ );
1136
+ }
1137
+ const eaterMass = state.masses.get(actingSubject)?.value ?? null;
1138
+ if (eaterMass !== null && eaterMass >= ASSUMED_FULL_MASS * HUNGRY_FRACTION) {
1139
+ return answer(
1140
+ `you're too full to eat the ${object}.`,
1141
+ noteFor(`eat — ${actingSubject} weighs ${eaterMass}, at or over half of ${ASSUMED_FULL_MASS}; declined by name`),
1142
+ { miss: true },
1143
+ );
1144
+ }
1145
+ const gained = state.masses.get(object)?.value ?? DEFAULT_FOOD_MASS;
1146
+ const grown = Math.round(((eaterMass ?? 0) + gained) * 100) / 100;
1147
+ return commit(
1148
+ [
1149
+ { subject: `${actingSubject}@turn${k}`, predicate: MASS_PREDICATE, object: String(grown) },
1150
+ { subject: `${object}@turn${k}`, predicate: "mgx:located-in", object: CONSUMED_PLACE },
1151
+ ],
1152
+ `you eat the ${object}. It adds ${gained} to your mass, so you weigh ${grown} now.`,
1153
+ `eat — the ${object}'s ${gained} mass moves onto ${actingSubject} (now ${grown}) and the ${object} leaves the world`,
1154
+ `eat the ${object}`,
1155
+ );
1156
+ }
1157
+
1158
+ if (cmd.verb === "put") {
1159
+ const container = cmd.indirectObject;
1160
+ if (!carriedBy(state, object, actingSubject)) {
1161
+ return answer(
1162
+ `you're not carrying the ${object}.`,
1163
+ noteFor(`put — ${object} isn't carried; precondition declined by name`),
1164
+ { miss: true },
1165
+ );
1166
+ }
1167
+ if (visibleRoomOf(container, { rows, state }) !== here) {
1168
+ return answer(
1169
+ `I don't see a ${container} here.`,
1170
+ noteFor(`put — ${container} isn't visible in the ${here}; declined`),
1171
+ { miss: true },
1172
+ );
1173
+ }
1174
+ if (!isContainer(rows, container)) {
1175
+ return answer(
1176
+ `the ${container} doesn't hold things.`,
1177
+ noteFor(`put — no mgx:is-container fact on ${container}; declined by name`),
1178
+ { miss: true },
1179
+ );
1180
+ }
1181
+ if (!state.openness.get(container)?.open) {
1182
+ return answer(
1183
+ `the ${container} is closed.`,
1184
+ noteFor(`put — the ${container} isn't open; precondition declined by name`),
1185
+ { miss: true },
1186
+ );
1187
+ }
1188
+ return commit(
1189
+ [{ subject: `${object}@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: container }],
1190
+ `you put the ${object} in the ${container}.`,
1191
+ `put — the taught "put" family fired; the ${object} now sits in the ${container}`,
1192
+ `put the ${object} in the ${container}`,
1193
+ );
1194
+ }
1195
+
950
1196
  // open / unlock / close — the container verbs. presence and container-ness
951
1197
  // stay hand-checked here (visibility gating, not a state precondition);
952
1198
  // unlock's instrument match stays fully hand-written below it too — it
@@ -981,14 +1227,14 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
981
1227
  );
982
1228
  }
983
1229
  const factState = containerDatatypeState(state, object);
984
- const failed = taughtAction.preconds.find((p) => !precondHolds(p, "player", object, factState, domain));
1230
+ const failed = taughtAction.preconds.find((p) => !precondHolds(p, actingSubject, object, factState, domain));
985
1231
  if (failed) {
986
1232
  const text = failed.predicate === "mgx:stands-locked-in"
987
1233
  ? `the ${object} is locked.`
988
1234
  : cmd.verb === "open" ? `the ${object} is already open.` : `the ${object} isn't open.`;
989
1235
  return answer(text, noteFor(`${cmd.verb} — the taught "${cmd.verb}" family's ${failed.predicate} precondition declined by name`), { miss: true });
990
1236
  }
991
- const effSubject = roleBinding(effect.subjectRole, "player", object, domain);
1237
+ const effSubject = roleBinding(effect.subjectRole, actingSubject, object, domain);
992
1238
  const writeIsOpen = { subject: `${effSubject}@turn${k}`, predicate: effect.predicate, object: effect.value };
993
1239
 
994
1240
  if (cmd.verb === "open") {
@@ -1035,7 +1281,7 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
1035
1281
  { miss: true },
1036
1282
  );
1037
1283
  }
1038
- if (!carriedByPlayer(state, cmd.instrument)) {
1284
+ if (!carriedBy(state, cmd.instrument, actingSubject)) {
1039
1285
  return answer(
1040
1286
  `you're not carrying the ${cmd.instrument}.`,
1041
1287
  noteFor(`unlock — the ${cmd.instrument} isn't carried; precondition declined by name`),
@@ -1076,7 +1322,7 @@ const WORLD_IS_OPEN_RE = /^is\s+(?:the\s+|a\s+|an\s+)?(.+?)\s+(open|closed|shut)
1076
1322
  * when the asked thing has no placement in the world, so an ordinary
1077
1323
  * locative question (a code symbol, a taught board piece) keeps its lane. A
1078
1324
  * hidden thing is declined without naming its hiding place. */
1079
- async function worldWhereAnswer(line, { memoryDir }) {
1325
+ async function worldWhereAnswer(line, { memoryDir, actingSubject = "player" }) {
1080
1326
  const m = String(line).match(WORLD_WHERE_RE);
1081
1327
  if (!m) return null;
1082
1328
  const thing = normFactTerm(m[1]);
@@ -1092,14 +1338,21 @@ async function worldWhereAnswer(line, { memoryDir }) {
1092
1338
  { miss: true, goal: `locate the ${thing}` },
1093
1339
  );
1094
1340
  }
1095
- if (thing === "player") {
1341
+ if (place.object === CONSUMED_PLACE) {
1342
+ return answer(
1343
+ `the ${thing} has been eaten — it's gone from the world.`,
1344
+ `ADVENTURE — where-aside: ${thing} was eaten, so it has no place left to name`,
1345
+ { goal: `locate the ${thing}` },
1346
+ );
1347
+ }
1348
+ if (thing === actingSubject) {
1096
1349
  return answer(
1097
1350
  `you are in the ${place.object}.`,
1098
1351
  "ADVENTURE — where-aside: the player's own room, from the current world fold",
1099
1352
  { goal: "check where you are" },
1100
1353
  );
1101
1354
  }
1102
- if (place.object === "player") {
1355
+ if (place.object === actingSubject) {
1103
1356
  return answer(
1104
1357
  `you are carrying the ${thing}.`,
1105
1358
  `ADVENTURE — where-aside: ${thing} is carried, from the current world fold`,
@@ -1143,11 +1396,16 @@ async function worldOpennessAnswer(line, { memoryDir }) {
1143
1396
  const WORLD_WHERE_AM_I_RE = /^where\s+am\s+i(?:\s+now)?[?.!\s]*$/i;
1144
1397
  const WORLD_OPTIONS_RE = /^(?:what\s+can\s+i\s+do(?:\s+(?:here|now))?|what\s+are\s+my\s+options|what\s+(?:should|do)\s+i\s+do(?:\s+(?:here|now))?|what\s+now)[?.!\s]*$/i;
1145
1398
  const WORLD_QUEST_RE = /^(?:what(?:'s|\s+is)\s+(?:the\s+|my\s+)?(?:quest|goal|objective|mission|aim)|what\s+am\s+i\s+(?:trying\s+to\s+do|(?:supposed|meant)\s+to\s+do)|what\s+do\s+i\s+do\s+here)[?.!\s]*$/i;
1399
+ // "what food do you know about" and its natural variants — the asking
1400
+ // character's OWN durable food knowledge (personKnownFoodLines), never the
1401
+ // whole world's food. Covers the plural ("foods") and the "what do you know
1402
+ // about food" inversion alongside the base phrasing.
1403
+ const WORLD_KNOWN_FOOD_RE = /^(?:what\s+foods?\s+do\s+you\s+know\s+about|what\s+do\s+you\s+know\s+about\s+food)[?.!\s]*$/i;
1146
1404
 
1147
1405
  /** The in-game orientation asides, answered from the world fold: the player's
1148
1406
  * room, the room's real affordances, and the world's objective. Null when the
1149
1407
  * line is none of them, so an ordinary question keeps its lane. */
1150
- async function worldContextAnswer(line, { memoryDir }) {
1408
+ async function worldContextAnswer(line, { memoryDir, actingSubject = "player" }) {
1151
1409
  const l = String(line).trim();
1152
1410
  const asksWhere = WORLD_WHERE_AM_I_RE.test(l);
1153
1411
  const asksOptions = WORLD_OPTIONS_RE.test(l);
@@ -1156,7 +1414,7 @@ async function worldContextAnswer(line, { memoryDir }) {
1156
1414
  let rows;
1157
1415
  try { rows = readFactRows(await loadMemory(memoryDir)); } catch { return null; }
1158
1416
  const state = foldWorldState(worldActionRows(rows));
1159
- const here = state.placements.get("player")?.object ?? null;
1417
+ const here = state.placements.get(actingSubject)?.object ?? null;
1160
1418
 
1161
1419
  if (asksWhere) {
1162
1420
  return here
@@ -1165,7 +1423,7 @@ async function worldContextAnswer(line, { memoryDir }) {
1165
1423
  }
1166
1424
 
1167
1425
  if (asksOptions) {
1168
- const actions = here ? roomAffordances(rows, state, here) : [];
1426
+ const actions = here ? roomAffordances(rows, state, here, actingSubject) : [];
1169
1427
  return answer(
1170
1428
  actions.length ? `you can: ${actions.join(", ")}.` : `nothing obvious here — say "look" to look around${here ? ` the ${here}` : ""}.`,
1171
1429
  `ADVENTURE — options aside: the ${here}'s roomAffordances, the same list "look" appends`,
@@ -1187,12 +1445,40 @@ async function worldContextAnswer(line, { memoryDir }) {
1187
1445
  );
1188
1446
  }
1189
1447
 
1190
- async function inventoryAnswer({ memoryDir, graph }) {
1448
+ /** "what food do you know about" — the ASKING character's own durable food
1449
+ * knowledge, read from the same mgx:knows-about facts recordTold/
1450
+ * recordExamined write (personKnownFoodLines). An honest "you don't know of
1451
+ * any food yet" when none is known — a real, on-topic answer, not a miss,
1452
+ * the same convention inventoryAnswer's own empty-carry case already uses.
1453
+ * Null when the line isn't this aside, so an ordinary question keeps its
1454
+ * lane. */
1455
+ async function worldKnownFoodAnswer(line, { memoryDir, actingSubject = "player" }) {
1456
+ const l = String(line).trim();
1457
+ if (!WORLD_KNOWN_FOOD_RE.test(l)) return null;
1458
+ let rows;
1459
+ try { rows = readFactRows(await loadMemory(memoryDir)); } catch { return null; }
1460
+ const state = foldWorldState(worldActionRows(rows));
1461
+ const foods = personKnownFoodLines(rows, state, actingSubject);
1462
+ if (!foods.length) {
1463
+ return answer(
1464
+ "you don't know of any food yet.",
1465
+ `ADVENTURE — known-food aside: ${actingSubject}'s mgx:knows-about facts reach no food-classed thing; the honest empty answer`,
1466
+ { goal: "check what food you know about" },
1467
+ );
1468
+ }
1469
+ return answer(
1470
+ `you know about: the ${foods.join(", the ")}.`,
1471
+ `ADVENTURE — known-food aside: ${actingSubject}'s durable mgx:knows-about facts, filtered to the food class`,
1472
+ { goal: "check what food you know about" },
1473
+ );
1474
+ }
1475
+
1476
+ async function inventoryAnswer({ memoryDir, graph, actingSubject = "player" }) {
1191
1477
  const memory = await loadMemory(memoryDir);
1192
1478
  const rows = readFactRows(memory);
1193
1479
  const state = foldWorldState(worldActionRows(rows));
1194
1480
  const carried = [...state.placements]
1195
- .filter(([, p]) => p.predicate === "mgx:located-in" && p.object === "player")
1481
+ .filter(([, p]) => p.predicate === "mgx:located-in" && p.object === actingSubject)
1196
1482
  .map(([thing]) => thing)
1197
1483
  .sort();
1198
1484
  if (!carried.length) {
@@ -1202,7 +1488,7 @@ async function inventoryAnswer({ memoryDir, graph }) {
1202
1488
  { goal: "check what you carry" },
1203
1489
  );
1204
1490
  }
1205
- const digest = await worldDigest("player", { memoryDir, memory, rows, state, graph });
1491
+ const digest = await worldDigest(actingSubject, { memoryDir, memory, rows, state, graph, actingSubject });
1206
1492
  return answer(
1207
1493
  digest ?? `you are carrying the ${carried.join(", the ")}.`,
1208
1494
  "ADVENTURE — inventory: an extractive completions digest over the facts mentioning the player",
@@ -1246,14 +1532,14 @@ const commandHasPronoun = (cmd) => PRONOUN_SLOTS.some((s) => cmd[s] && OBJECT_PR
1246
1532
  * real, actionable object from the current room when one is on show (else a
1247
1533
  * static example). Never the "I don't know the word" line — the vocabulary
1248
1534
  * misdiagnosis is unreachable for a pronoun. */
1249
- async function noFocusPronounNudge(pronoun, { memoryDir }) {
1535
+ async function noFocusPronounNudge(pronoun, { memoryDir, actingSubject = "player" }) {
1250
1536
  let example = null;
1251
1537
  try {
1252
1538
  const rows = readFactRows(await loadMemory(memoryDir));
1253
1539
  const state = foldWorldState(worldActionRows(rows));
1254
- const here = state.placements.get("player")?.object ?? null;
1540
+ const here = state.placements.get(actingSubject)?.object ?? null;
1255
1541
  if (here) {
1256
- for (const action of roomAffordances(rows, state, here)) {
1542
+ for (const action of roomAffordances(rows, state, here, actingSubject)) {
1257
1543
  const m = action.match(/^(?:examine|take|open|unlock|talk to) (.+)$/);
1258
1544
  if (m) { example = m[1]; break; }
1259
1545
  }
@@ -1274,14 +1560,15 @@ async function noFocusPronounNudge(pronoun, { memoryDir }) {
1274
1560
  * passes straight through untouched. All four surface pronouns
1275
1561
  * (it/them/him/her) normalize to the one `it` probe, then bind to the newest
1276
1562
  * referent THIS lane registered — the record may also hold code-graph
1277
- * referents, so the bind is scoped to `lane: "adventure"`. */
1278
- async function bindPronouns(cmd, { discourseHolder, memoryDir }) {
1563
+ * referents, so the bind is scoped to `lane: "adventure"`. The record is one
1564
+ * per session, so several acting subjects sharing a world share one focus. */
1565
+ async function bindPronouns(cmd, { discourseHolder, memoryDir, actingSubject = "player" }) {
1279
1566
  if (!commandHasPronoun(cmd)) return { cmd };
1280
1567
  const probe = discourseHolder ? bindDiscourseForm(discourseHolder.record, "it") : null;
1281
1568
  const focusTerm = (probe?.candidates || []).find((r) => r.from?.lane === "adventure")?.label ?? null;
1282
1569
  if (!focusTerm) {
1283
1570
  const pronoun = PRONOUN_SLOTS.map((s) => cmd[s]).find((v) => v && OBJECT_PRONOUNS.has(v));
1284
- return { nudge: await noFocusPronounNudge(pronoun, { memoryDir }) };
1571
+ return { nudge: await noFocusPronounNudge(pronoun, { memoryDir, actingSubject }) };
1285
1572
  }
1286
1573
  const bound = { ...cmd };
1287
1574
  for (const s of PRONOUN_SLOTS) {
@@ -1301,7 +1588,7 @@ async function bindPronouns(cmd, { discourseHolder, memoryDir }) {
1301
1588
  * recognizer, injected so the two lanes can never disagree about what a plan
1302
1589
  * frame is.
1303
1590
  */
1304
- export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "", env, lexicon = null, graph = null, cache = null, isPlanFrameLine = () => false, discourseHolder = null }) {
1591
+ export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "", env, lexicon = null, graph = null, cache = null, isPlanFrameLine = () => false, discourseHolder = null, actingSubject = "player" }) {
1305
1592
  const slot = planHolder?.state ?? null;
1306
1593
  const adventure = slot?.adventure ?? null;
1307
1594
  const opening = matchAdventureOpening(line);
@@ -1357,13 +1644,13 @@ export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "
1357
1644
  note: "ADVENTURE — a plan frame arrived mid-adventure; the slot holds one thing at a time",
1358
1645
  };
1359
1646
  }
1360
- if (INVENTORY_RE.test(line)) return inventoryAnswer({ memoryDir, graph });
1647
+ if (INVENTORY_RE.test(line)) return inventoryAnswer({ memoryDir, graph, actingSubject });
1361
1648
  const parsed = parseImperative(line, lexicon ?? undefined);
1362
1649
  if (parsed) {
1363
- const bound = await bindPronouns(parsed, { discourseHolder, memoryDir });
1650
+ const bound = await bindPronouns(parsed, { discourseHolder, memoryDir, actingSubject });
1364
1651
  if (bound.nudge) return bound.nudge;
1365
1652
  const cmd = bound.cmd;
1366
- const result = await runWorldCommand(cmd, { world: adventure.world, memoryDir, env, graph, cache });
1653
+ const result = await runWorldCommand(cmd, { world: adventure.world, memoryDir, env, graph, cache, actingSubject });
1367
1654
  // The object a command SUCCESSFULLY named registers as a discourse referent
1368
1655
  // a later pronoun binds to — so "look lamp" then "examine it" reads the
1369
1656
  // lamp, and "talk to housekeeper" makes "him"/"her" the housekeeper. A miss
@@ -1387,11 +1674,13 @@ export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "
1387
1674
  note: `${result.note}; corrected ${cmd.corrected.map((c) => `"${c.from}" -> "${c.to}"`).join(", ")} before executing`,
1388
1675
  };
1389
1676
  }
1390
- const whereAside = await worldWhereAnswer(line, { memoryDir });
1677
+ const whereAside = await worldWhereAnswer(line, { memoryDir, actingSubject });
1391
1678
  if (whereAside) return whereAside;
1392
1679
  const opennessAside = await worldOpennessAnswer(line, { memoryDir });
1393
1680
  if (opennessAside) return opennessAside;
1394
- const contextAside = await worldContextAnswer(line, { memoryDir });
1681
+ const contextAside = await worldContextAnswer(line, { memoryDir, actingSubject });
1395
1682
  if (contextAside) return contextAside;
1683
+ const knownFoodAside = await worldKnownFoodAnswer(line, { memoryDir, actingSubject });
1684
+ if (knownFoodAside) return knownFoodAside;
1396
1685
  return null; // a mid-game aside — the ordinary lanes answer, world untouched
1397
1686
  }