@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.
- package/corpus/sprites/src/sprite-facts.jsonl +10 -0
- package/corpus/tier2/generate.mjs +10 -1
- package/corpus/tier2/human.jsonl +23 -0
- package/corpus/tier2/manifest.json +3 -3
- package/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +15 -5
- package/corpus/worlds/shards/mud-garden.jsonl.gz +0 -0
- package/corpus/worlds/src/mud-garden.jsonl +68 -0
- package/package.json +2 -1
- package/src/domain/game-config.mjs +46 -0
- package/src/domain/grammar/ace.mjs +11 -3
- package/src/domain/grammar/lexicon-core.json +3 -0
- package/src/domain/memory/trust.mjs +13 -0
- package/src/services/adventure.mjs +351 -62
- package/src/services/chat-session.mjs +28 -3
- package/src/services/chat.mjs +2 -2
- package/src/services/mud-turn.mjs +395 -0
- package/src/services/mud-viz.mjs +752 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +60 -60
- package/src/surfaces/web/mud-browser-entry.mjs +164 -0
|
@@ -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
|
|
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
|
-
|
|
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
|
|
330
|
+
const carriedBy = (state, thing, holder) => {
|
|
315
331
|
const place = state.placements.get(thing);
|
|
316
|
-
return !!place && place.predicate === "mgx:located-in" && place.object ===
|
|
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 ===
|
|
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",
|
|
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 ===
|
|
512
|
-
push(
|
|
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" &&
|
|
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 ===
|
|
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,
|
|
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 !==
|
|
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(
|
|
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") &&
|
|
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:
|
|
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;
|
|
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 (
|
|
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:
|
|
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 (!
|
|
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,
|
|
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,
|
|
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 (!
|
|
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 (
|
|
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 ===
|
|
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(
|
|
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
|
-
|
|
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 ===
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
}
|