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

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.
@@ -14,13 +14,15 @@
14
14
 
15
15
  import {
16
16
  TOWN_SQUARE_LAYOUTS, DEFAULT_GRID_SIZE, DIRECTION_DELTA,
17
- cellId, parseCellId, inBounds, chebyshevDistance, oneStepDirectionBetween,
17
+ cellId, parseCellId, inBounds, isSolid, chebyshevDistance, oneStepDirectionBetween,
18
18
  agentKindOf, liveIdsOfKind, layoutNamed, isFoodId,
19
19
  } from "../domain/town-square-world.mjs";
20
20
  import {
21
21
  MUDIII_ROLES, foldTownSquareState, startTownSquareGame, runTownSquareTick,
22
22
  placeFood, roleOfId, beliefSnapshotFor,
23
23
  } from "./predator-prey.mjs";
24
+ import { snapshotSubject } from "./adventure.mjs";
25
+ import { correctMisspellings, QUESTION_LEAD_RE } from "../domain/interpret/normalize.mjs";
24
26
  import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
25
27
  import { getWorldsPackProvider } from "../adapters/corpus/worlds-pack.mjs";
26
28
  import { appendFacts, appendRule, loadMemory, readFactRows } from "../adapters/memory/core.mjs";
@@ -553,6 +555,242 @@ async function runToldFactTurn(match, { planHolder, memoryDir, cache, gameConfig
553
555
  });
554
556
  }
555
557
 
558
+ // ---- the teach lane: a declarative sentence read as a board fact -------------
559
+ //
560
+ // The town square's own half of the world-teach act world-teach.mjs performs
561
+ // for a manor and a burrow, and shaped the same way: a small closed sentence
562
+ // table, one additive planner over the live fold, and a "noted — … now."
563
+ // confirmation carrying the same `world:<name>:taught:turnK` provenance.
564
+ //
565
+ // It stays here rather than routing through world-teach.mjs because that
566
+ // module's gates are written for a ROOM world. It declines by naming rooms,
567
+ // mints a fresh portable when the subject is one the world has never heard
568
+ // of, and plans against foldWorldState. A board has cells instead of rooms
569
+ // and one fold of its own, and nothing here ever mints: every subject must
570
+ // already resolve to a live individual foldTownSquareState folds, or the
571
+ // write would put a fact on the board that the board cannot draw.
572
+
573
+ const PLACEMENT_PREDICATE = "mgx:currently-in";
574
+ const MASS_PREDICATE = "mgx:hasMass";
575
+ const MOOD_PREDICATE = "mgx:feels";
576
+ const FACING_PREDICATE = "mgx:facing";
577
+ const PLACED_BY_PREDICATE = "mgx:placed-by";
578
+
579
+ // The families foldTownSquareState ranks by (epoch, turn). A row in one of
580
+ // them needs a snapshot subject or it ranks as turn 0 and loses to anything
581
+ // already played about the same thing. mgx:placed-by is read raw and keeps
582
+ // its bare subject, exactly as placeFood writes it.
583
+ const TAUGHT_SNAPSHOT_PREDICATES = new Set([
584
+ PLACEMENT_PREDICATE, MASS_PREDICATE, MOOD_PREDICATE, FACING_PREDICATE,
585
+ ]);
586
+
587
+ // The closed cast vocabulary a taught sentence may name, matching the lane's
588
+ // own recognizers above. Mood and facing take an agent alone — a crumb has
589
+ // neither — while a cell and a weight are true of an inert item too.
590
+ const CAST_SUBJECT = "(fox|goblin|crumb|morsel)(?:-(\\d+))?";
591
+ const AGENT_SUBJECT = "(fox|goblin)(?:-(\\d+))?";
592
+ const ITEM_SUBJECT = "(crumb|morsel)(?:-(\\d+))?";
593
+
594
+ /**
595
+ * The town square's sentence table: every fact about this board a person can
596
+ * state, one row per predicate the fold reads. Checked in order, first match
597
+ * wins.
598
+ *
599
+ * Fox-1 is at cell-3-4. mgx:currently-in
600
+ * The goblin weighs 4. mgx:hasMass
601
+ * The fox feels angry. mgx:feels
602
+ * Goblin-2 faces north. mgx:facing
603
+ * The baker put morsel-1 there. mgx:placed-by
604
+ *
605
+ * A bare kind ("the fox") names whichever individual of that kind is live and
606
+ * lowest-numbered; a numbered id names exactly one. Closed on both sides: a
607
+ * mood or a direction outside the engine's own words does not parse at all,
608
+ * so nothing here can write a value a renderer has no drawing for.
609
+ */
610
+ const TOWN_SQUARE_TEACH_PATTERNS = [
611
+ { kind: "placement", predicate: PLACEMENT_PREDICATE,
612
+ re: new RegExp(`^(?:the\\s+)?${CAST_SUBJECT}\\s+is\\s+at\\s+(cell-\\d+-\\d+)[.!\\s]*$`, "i") },
613
+ { kind: "mass", predicate: MASS_PREDICATE,
614
+ re: new RegExp(`^(?:the\\s+)?${CAST_SUBJECT}\\s+weighs\\s+(\\d+(?:\\.\\d+)?)[.!\\s]*$`, "i") },
615
+ { kind: "mood", predicate: MOOD_PREDICATE,
616
+ re: new RegExp(`^(?:the\\s+)?${AGENT_SUBJECT}\\s+feels\\s+(calm|angry|scared|happy)[.!\\s]*$`, "i") },
617
+ { kind: "facing", predicate: FACING_PREDICATE,
618
+ re: new RegExp(`^(?:the\\s+)?${AGENT_SUBJECT}\\s+faces\\s+(north|south|east|west)[.!\\s]*$`, "i") },
619
+ ];
620
+
621
+ // The one sentence whose subject is not its leading noun: the item is the
622
+ // subject and the placer is the object, which is the direction "who put that
623
+ // there?" reads the row back in.
624
+ const TOWN_SQUARE_PLACED_BY_RE = new RegExp(
625
+ `^(?:the\\s+)?([a-z][a-z-]*)\\s+(?:put|placed|dropped)\\s+(?:the\\s+)?${ITEM_SUBJECT}\\s+there[.!\\s]*$`,
626
+ "i",
627
+ );
628
+
629
+ /** One line -> `{ kind, predicate, kindWord, num, object }`, or null when the
630
+ * table recognizes nothing — an honest miss, never a guessed shape.
631
+ * `kindWord`/`num` name the individual the sentence is about; the caller
632
+ * resolves that pair against the live board. Pure. */
633
+ export function parseTownSquareTeachLine(line) {
634
+ const trimmed = String(line || "").trim();
635
+ if (!trimmed) return null;
636
+ for (const { kind, predicate, re } of TOWN_SQUARE_TEACH_PATTERNS) {
637
+ const m = trimmed.match(re);
638
+ if (!m) continue;
639
+ return { kind, predicate, kindWord: m[1].toLowerCase(), num: m[2] ?? null, object: m[3].toLowerCase() };
640
+ }
641
+ const placed = trimmed.match(TOWN_SQUARE_PLACED_BY_RE);
642
+ if (!placed) return null;
643
+ return {
644
+ kind: "placed-by", predicate: PLACED_BY_PREDICATE,
645
+ kindWord: placed[2].toLowerCase(), num: placed[3] ?? null, object: placed[1].toLowerCase(),
646
+ };
647
+ }
648
+
649
+ /**
650
+ * The rows one already-resolved taught triple implies against the board's
651
+ * current fold — the town square's counterpart to mud-editor.mjs's
652
+ * planTaughtMudTriple, and additive for the same reason: one sentence only
653
+ * ever says what it says, so nothing it leaves out is evidence of anything.
654
+ * Re-asserting a fact the board already holds appends nothing, and `reason`
655
+ * says which of the two happened.
656
+ *
657
+ * Takes the fold alone rather than the raw rows its burrow counterpart also
658
+ * needs: every family this table can say is one foldTownSquareState folds, so
659
+ * there is no raw-row family left to diff against. Pure.
660
+ */
661
+ export function planTaughtTownSquareTriple(state, triple) {
662
+ if (!triple?.subject || !triple?.object) return { toAppend: [], reason: "nothing parsed" };
663
+ const { subject, object } = triple;
664
+ switch (triple.kind) {
665
+ case "placement": {
666
+ const current = state?.placements?.get(subject);
667
+ if (current?.cell === object) return { toAppend: [], reason: `${subject} already stands at ${object}` };
668
+ return {
669
+ toAppend: [triple],
670
+ reason: current ? `${subject} moves from ${current.cell} to ${object}` : `${subject} is placed at ${object}`,
671
+ };
672
+ }
673
+ case "mass": {
674
+ const current = state?.mass?.get(subject);
675
+ if (current && Number(current.value) === Number(object)) return { toAppend: [], reason: `${subject} already weighs ${object}` };
676
+ return { toAppend: [triple], reason: `${subject} weighs ${object}` };
677
+ }
678
+ case "mood": {
679
+ const current = state?.mood?.get(subject);
680
+ if (current?.value === object) return { toAppend: [], reason: `${subject} already feels ${object}` };
681
+ return { toAppend: [triple], reason: `${subject} feels ${object}` };
682
+ }
683
+ case "facing": {
684
+ const current = state?.facing?.get(subject);
685
+ if (current?.value === object) return { toAppend: [], reason: `${subject} already faces ${object}` };
686
+ return { toAppend: [triple], reason: `${subject} faces ${object}` };
687
+ }
688
+ case "placed-by": {
689
+ const current = state?.placedBy?.get(subject);
690
+ if (current?.by === object) return { toAppend: [], reason: `${object} already put ${subject} there` };
691
+ return { toAppend: [triple], reason: `${object} put ${subject} there` };
692
+ }
693
+ default:
694
+ return { toAppend: [], reason: `the board folds nothing for ${triple.predicate}` };
695
+ }
696
+ }
697
+
698
+ /** One taught triple as the sentence the board says back, in world-teach.mjs's
699
+ * own `noted — … now.` shape. Pure. */
700
+ export function townSquareTeachConfirmation(triple) {
701
+ switch (triple.kind) {
702
+ case "placement": return `noted — ${triple.subject} is at ${triple.object} now.`;
703
+ case "mass": return `noted — ${triple.subject} weighs ${triple.object} now.`;
704
+ case "mood": return `noted — ${triple.subject} feels ${triple.object} now.`;
705
+ case "facing": return `noted — ${triple.subject} faces ${triple.object} now.`;
706
+ case "placed-by": return `noted — the ${triple.object} put ${triple.subject} there now.`;
707
+ default: return "noted — the board says that now.";
708
+ }
709
+ }
710
+
711
+ const teachDecline = (text, note) => ({ text, lane: "game-answer", note: `MUDIII — world-teach: ${note}`, miss: true });
712
+
713
+ /**
714
+ * One line read as a fact about the LIVE board, or null when it is not a
715
+ * teach sentence at all and the ordinary lane should have it. Writes the
716
+ * fold-versioned families under a snapshot subject stamped at the next tick's
717
+ * own turn number, the same convention placeFood uses so a teach and the tick
718
+ * that resolves it share one turn rather than the teach quietly spending one.
719
+ *
720
+ * Nobody moves in response: a taught fact never runs the ecology pass, which
721
+ * is the same trade world-teach.mjs makes for a manor.
722
+ */
723
+ async function mudiiiTeachTurn(line, { memoryDir, cache, world, layout }) {
724
+ const trimmed = String(line || "").trim();
725
+ if (!trimmed || !memoryDir) return null;
726
+ // A trailing "?" is an unambiguous question, and a leading interrogative is
727
+ // the same signal one word earlier — a question must never reach a write
728
+ // boundary. Both mirror world-teach.mjs, which stands down on either.
729
+ if (/\?\s*$/.test(trimmed)) return null;
730
+ if (QUESTION_LEAD_RE.test(correctMisspellings(trimmed))) return null;
731
+
732
+ const parsed = parseTownSquareTeachLine(trimmed);
733
+ if (!parsed) return null;
734
+
735
+ const rows = readFactRows(await loadMemory(memoryDir));
736
+ const state = foldTownSquareState(rows);
737
+ const subject = resolveAgentId(parsed.kindWord, parsed.num, state);
738
+ if (!subject) {
739
+ return teachDecline(
740
+ `there's no live ${parsed.kindWord} on the board for that to be about.`,
741
+ `"${trimmed}" is about a ${parsed.kindWord} nothing live answers to; declined rather than minting one the board cannot draw`,
742
+ );
743
+ }
744
+
745
+ if (parsed.kind === "placement") {
746
+ const cell = parseCellId(parsed.object);
747
+ if (!cell || !inBounds(layout.gridSize, cell.x, cell.y)) {
748
+ return teachDecline(
749
+ `${parsed.object} is off the board — this square runs cell-1-1 to cell-${layout.gridSize}-${layout.gridSize}.`,
750
+ `"${trimmed}" names a cell outside the ${layout.gridSize}x${layout.gridSize} board`,
751
+ );
752
+ }
753
+ if (isSolid(layout, parsed.object)) {
754
+ return teachDecline(
755
+ `${parsed.object} is blocked — nothing stands inside a building.`,
756
+ `"${trimmed}" would stand ${subject} on a prop cell, which no path ever reaches`,
757
+ );
758
+ }
759
+ }
760
+
761
+ const triple = { subject, predicate: parsed.predicate, object: parsed.object, kind: parsed.kind };
762
+ const { toAppend, reason } = planTaughtTownSquareTriple(state, triple);
763
+ if (!toAppend.length) {
764
+ return {
765
+ text: "the board already says that.",
766
+ lane: "game-answer",
767
+ miss: false,
768
+ note: `MUDIII — world-teach: "${trimmed}" asserts a fact the board already holds (${reason}); nothing written`,
769
+ taught: [],
770
+ };
771
+ }
772
+
773
+ const k = state.tickCount + 1;
774
+ const epoch = state.epoch;
775
+ const facts = toAppend.map((t) => ({
776
+ subject: TAUGHT_SNAPSHOT_PREDICATES.has(t.predicate) ? snapshotSubject(t.subject, k, epoch) : t.subject,
777
+ predicate: t.predicate,
778
+ object: t.object,
779
+ }));
780
+ const provenance = `${worldProvenanceTag(world)}:taught:turn${k}`;
781
+ await appendFacts(memoryDir, facts.map((f) => ({ ...f, provenance })));
782
+ if (cache) cache.rows = null;
783
+
784
+ return {
785
+ text: townSquareTeachConfirmation(triple),
786
+ lane: "game-answer",
787
+ miss: false,
788
+ goal: `change what the board says about ${subject}`,
789
+ note: `MUDIII — world-teach: ${reason}; wrote ${facts.length} row(s) at turn ${k} with provenance ${provenance}; no tick rides a taught fact`,
790
+ taught: facts,
791
+ };
792
+ }
793
+
556
794
  // ---- in-game orientation asides ---------------------------------------------
557
795
  //
558
796
  // "where is the fox", "where am I", "what can I do", "what is the fox's
@@ -708,6 +946,17 @@ export async function mudiiiTurn(line, { planHolder, memoryDir, env, cache = nul
708
946
  return runPlaceFoodTurn(putMatch[1], { memoryDir, gameConfig, world: mudiii.world, layout });
709
947
  }
710
948
 
949
+ // The teach switch runs before the plan-frame guard for the same reason the
950
+ // food verb does: "the fox is at cell-3-4" reads as a planning frame on its
951
+ // leading noun, and answering a sentence this lane's own table accepts with
952
+ // "stop watching, then set your goal" refuses the one thing the switch is
953
+ // for. With the switch off nothing here runs and the lane behaves exactly
954
+ // as it always has.
955
+ if (gameConfig?.mudiii?.teach) {
956
+ const taught = await mudiiiTeachTurn(trimmed, { memoryDir, cache, world: mudiii.world, layout });
957
+ if (taught) return taught;
958
+ }
959
+
711
960
  if (isPlanFrameLine(line)) {
712
961
  return {
713
962
  text: 'the town square game is running — say "stop watching" to end it, then set your goal.',