@polycode-projects/the-mechanical-code-talker 5.0.3 → 5.0.5
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/worlds/manifest.json +9 -9
- package/corpus/worlds/shards/town-square-chapel.jsonl.gz +0 -0
- package/corpus/worlds/shards/town-square-market.jsonl.gz +0 -0
- package/corpus/worlds/shards/town-square.jsonl.gz +0 -0
- package/corpus/worlds/src/town-square-chapel.jsonl +1 -1
- package/corpus/worlds/src/town-square-market.jsonl +1 -1
- package/corpus/worlds/src/town-square.jsonl +1 -1
- package/data/mudiii-assets.json +11 -6
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +53 -0
- package/src/domain/game-config.mjs +9 -1
- package/src/domain/memory/causal-stability.mjs +82 -0
- package/src/domain/spider-fly-world.mjs +11 -13
- package/src/domain/town-square-world.mjs +36 -0
- package/src/services/mudiii-scene.mjs +131 -39
- package/src/services/mudiii-turn.mjs +332 -2
- package/src/services/mudiii-viz.mjs +76 -51
- package/src/services/predator-prey.mjs +68 -7
- package/src/services/spider-fly-viz.mjs +57 -60
- package/src/surfaces/web/memory-ask-browser.bundle.js +106 -106
- package/src/surfaces/web/mudiii-browser-entry.mjs +89 -5
|
@@ -186,12 +186,14 @@ export function currentActionFor(agentId, agentsById, moving) {
|
|
|
186
186
|
* so acting on the ecology row too would double it). `kind` is one of
|
|
187
187
|
* "death" (the rig's own Death clip, then scale to zero) or "consume" (an
|
|
188
188
|
* eaten item scales to zero, no clip — items are primitive geometry).
|
|
189
|
-
*
|
|
189
|
+
* `eat-agent` alone also carries `actorId`, the predator that gets a
|
|
190
|
+
* one-shot attack clip while its prey plays death — the one event where a
|
|
191
|
+
* second, surviving agent has its own flourish to play. Pure. */
|
|
190
192
|
export function flourishForEcologyEvent(event) {
|
|
191
193
|
if (!event || !event.type) return null;
|
|
192
194
|
switch (event.type) {
|
|
193
195
|
case "eat-agent":
|
|
194
|
-
return { kind: "death", targetId: event.prey, cell: event.cell };
|
|
196
|
+
return { kind: "death", targetId: event.prey, cell: event.cell, actorId: event.predator };
|
|
195
197
|
case "starve":
|
|
196
198
|
return { kind: "death", targetId: event.agent, cell: event.cell };
|
|
197
199
|
case "eat-item":
|
|
@@ -359,6 +361,11 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
359
361
|
var manifestByKind = {};
|
|
360
362
|
var lastAgentsById = {};
|
|
361
363
|
var lastItemsById = {};
|
|
364
|
+
// Every removeAgent/removeItem call, kept small and reset on boot — an
|
|
365
|
+
// e2e assertion's read, so it can tell an eaten agent/item's flourish
|
|
366
|
+
// removal (source "ecology") apart from applyTick's own diff no-op
|
|
367
|
+
// (source "diff") without guessing from a vanished mesh alone.
|
|
368
|
+
var removalLog = [];
|
|
362
369
|
var cameraState = { mode: "overhead", selectedId: null };
|
|
363
370
|
var cameraTween = null, lookAtTween = null;
|
|
364
371
|
var lastFrameTs = null;
|
|
@@ -385,9 +392,6 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
385
392
|
});
|
|
386
393
|
});
|
|
387
394
|
}
|
|
388
|
-
var glbCache = null;
|
|
389
|
-
function loadGlb(url) { return glbCache.load(url); }
|
|
390
|
-
|
|
391
395
|
async function ensureThree() {
|
|
392
396
|
if (THREE) return true;
|
|
393
397
|
var result = await loadThreeVendor();
|
|
@@ -396,7 +400,6 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
396
400
|
OrbitControlsCtor = result.OrbitControls; MeshoptDecoderRef = result.MeshoptDecoder;
|
|
397
401
|
gltfLoader = new GLTFLoaderCtor();
|
|
398
402
|
gltfLoader.setMeshoptDecoder(MeshoptDecoderRef);
|
|
399
|
-
glbCache = createCachedLoader(loadGlbRaw);
|
|
400
403
|
setUpScene();
|
|
401
404
|
return true;
|
|
402
405
|
}
|
|
@@ -513,13 +516,23 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
513
516
|
object3D.userData.tmctNormalized = true;
|
|
514
517
|
}
|
|
515
518
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
519
|
+
// Keyed on the manifest row's own key field, not its destPath: food-crumb
|
|
520
|
+
// and food-morsel share one GLB at two different targetHeights, and
|
|
521
|
+
// normalizeToHeight refuses a second call on the same object, so a
|
|
522
|
+
// destPath-keyed cache would hand the second row the first row's
|
|
523
|
+
// already-normalized graph and throw.
|
|
524
|
+
function loadPropTemplate(asset) {
|
|
525
|
+
var key = asset.key;
|
|
526
|
+
if (!propTemplates[key]) {
|
|
527
|
+
var url = modelUrlFor(asset.destPath);
|
|
528
|
+
var promise = loadGlbRaw(url).then(function (gltf) {
|
|
529
|
+
normalizeToHeight(gltf.scene, asset.targetHeight);
|
|
530
|
+
return gltf.scene;
|
|
531
|
+
});
|
|
532
|
+
promise.catch(function () { delete propTemplates[key]; });
|
|
533
|
+
propTemplates[key] = promise;
|
|
534
|
+
}
|
|
535
|
+
return propTemplates[key];
|
|
523
536
|
}
|
|
524
537
|
|
|
525
538
|
async function placeProps(propPlacements) {
|
|
@@ -544,13 +557,11 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
544
557
|
}
|
|
545
558
|
}
|
|
546
559
|
|
|
547
|
-
// ---- items: crumbs and morsels,
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
function
|
|
552
|
-
function itemMaterialFor(kind) { return new THREE.MeshStandardMaterial({ color: kind === "morsel" ? 0xd98a2b : 0x8c6a3f }); }
|
|
553
|
-
function itemHeightFor(kind) { return kind === "morsel" ? MORSEL_RADIUS : CRUMB_RADIUS; }
|
|
560
|
+
// ---- items: crumbs and morsels, the committed hay bale at two target
|
|
561
|
+
// heights (food-crumb 0.16, food-morsel 0.36 in data/mudiii-assets.json),
|
|
562
|
+
// matching the on-ground footprint the primitive spheres they replace were
|
|
563
|
+
// already tuned to. ---------------------------------------------------------
|
|
564
|
+
function itemAssetKeyFor(kind) { return kind === "morsel" ? "food-morsel" : "food-crumb"; }
|
|
554
565
|
|
|
555
566
|
function animateScaleTo(object3D, target, now) {
|
|
556
567
|
var duration = tweenDurationMs;
|
|
@@ -566,29 +577,55 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
566
577
|
requestAnimationFrame(step);
|
|
567
578
|
}
|
|
568
579
|
|
|
580
|
+
// A Group per live item, the same shape ensureAgent uses for agents: the
|
|
581
|
+
// group is in the scene (and in itemMeshes) synchronously so a second tick
|
|
582
|
+
// arriving before the GLB resolves finds an existing entry rather than
|
|
583
|
+
// starting a second load, but stays invisible until the model is in it.
|
|
569
584
|
function applyItemTick(id, item, now) {
|
|
570
585
|
var world = cellToWorld(item.cell, GRID_SIZE, CELL_SIZE);
|
|
571
586
|
if (!world) return;
|
|
572
587
|
var entry = itemMeshes[id];
|
|
573
|
-
if (
|
|
574
|
-
|
|
575
|
-
mesh.
|
|
576
|
-
mesh.position.set(world.x, itemHeightFor(item.kind), world.z);
|
|
577
|
-
mesh.scale.setScalar(0);
|
|
578
|
-
scene.add(mesh);
|
|
579
|
-
entry = { mesh: mesh, cell: item.cell, kind: item.kind };
|
|
580
|
-
itemMeshes[id] = entry;
|
|
581
|
-
animateScaleTo(mesh, 1, now);
|
|
588
|
+
if (entry) {
|
|
589
|
+
entry.cell = item.cell;
|
|
590
|
+
entry.mesh.position.set(world.x, 0, world.z);
|
|
582
591
|
return;
|
|
583
592
|
}
|
|
584
|
-
|
|
585
|
-
|
|
593
|
+
var group = new THREE.Group();
|
|
594
|
+
group.name = "item-" + id;
|
|
595
|
+
group.position.set(world.x, 0, world.z);
|
|
596
|
+
group.visible = false;
|
|
597
|
+
scene.add(group);
|
|
598
|
+
entry = { mesh: group, cell: item.cell, kind: item.kind };
|
|
599
|
+
itemMeshes[id] = entry;
|
|
600
|
+
var asset = manifestByKind[itemAssetKeyFor(item.kind)];
|
|
601
|
+
if (!asset) return;
|
|
602
|
+
loadPropTemplate(asset).then(function (template) {
|
|
603
|
+
if (itemMeshes[id] !== entry) return; // removed while the model was still loading
|
|
604
|
+
var instance = template.clone();
|
|
605
|
+
instance.position.y = template.position.y;
|
|
606
|
+
group.add(instance);
|
|
607
|
+
group.visible = true;
|
|
608
|
+
group.scale.setScalar(0);
|
|
609
|
+
animateScaleTo(group, 1, performance.now());
|
|
610
|
+
}).catch(function (err) {
|
|
611
|
+
// eslint-disable-next-line no-console
|
|
612
|
+
console.warn("tmct: an item model failed to load", id, err);
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function recordRemoval(kind, id, source) {
|
|
617
|
+
removalLog.push({ kind: kind, id: id, source: source });
|
|
586
618
|
}
|
|
587
619
|
|
|
588
|
-
|
|
620
|
+
// source ("ecology" | "diff") is recorded, never branched on — it exists
|
|
621
|
+
// so an e2e assertion can tell an eaten item's flourish removal apart from
|
|
622
|
+
// a plain diff no-op, since a vanished mesh alone does not say which path
|
|
623
|
+
// did the removing.
|
|
624
|
+
function removeItem(id, withFlourish, source) {
|
|
589
625
|
var entry = itemMeshes[id];
|
|
590
626
|
if (!entry) return;
|
|
591
627
|
delete itemMeshes[id];
|
|
628
|
+
recordRemoval("item", id, source);
|
|
592
629
|
if (withFlourish) animateScaleTo(entry.mesh, 0, performance.now());
|
|
593
630
|
else if (entry.mesh.parent) entry.mesh.parent.remove(entry.mesh);
|
|
594
631
|
}
|
|
@@ -603,7 +640,7 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
603
640
|
var kind = roleOfAgentId(id);
|
|
604
641
|
var entry = {
|
|
605
642
|
group: new THREE.Group(), tween: null, cell: null, facing: agent.facing, role: agent.role,
|
|
606
|
-
kind: kind, mixer: null, actions: {}, currentClip: null, clipMap: null,
|
|
643
|
+
kind: kind, mixer: null, actions: {}, currentClip: null, clipMap: null, oneShotAction: null,
|
|
607
644
|
};
|
|
608
645
|
entry.group.visible = false;
|
|
609
646
|
scene.add(entry.group);
|
|
@@ -628,6 +665,15 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
628
665
|
}
|
|
629
666
|
|
|
630
667
|
function playClip(entry, clipName) {
|
|
668
|
+
// A one-shot flourish left its own action running at full weight (see
|
|
669
|
+
// playClipOnce below — fadeIn ramps the NEW action in but never ramps
|
|
670
|
+
// the old one out on its own, so without this the flourish pose would
|
|
671
|
+
// keep blending into every clip after it, forever). Fade it out before
|
|
672
|
+
// any of the guards below can skip the rest of this call.
|
|
673
|
+
if (entry.oneShotAction) {
|
|
674
|
+
entry.oneShotAction.fadeOut(0.15);
|
|
675
|
+
entry.oneShotAction = null;
|
|
676
|
+
}
|
|
631
677
|
if (!entry.mixer || !clipName || entry.currentClip === clipName) return;
|
|
632
678
|
var next = entry.actions[clipName];
|
|
633
679
|
if (!next) return;
|
|
@@ -637,6 +683,28 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
637
683
|
entry.currentClip = clipName;
|
|
638
684
|
}
|
|
639
685
|
|
|
686
|
+
// A one-shot version of playClip for a flourish (the predator's own bite
|
|
687
|
+
// on eat-agent): LoopOnce + clampWhenFinished so the clip plays through and
|
|
688
|
+
// holds its last pose rather than looping. Never skips a repeat of
|
|
689
|
+
// entry.currentClip the way playClip does — a flourish must always play,
|
|
690
|
+
// even if the actor happened to already be on this same clip. Clears
|
|
691
|
+
// entry.currentClip to null so the next applyAgentTick's own playClip call
|
|
692
|
+
// is never refused as "already on this clip", and records the action on
|
|
693
|
+
// entry.oneShotAction so that same next call fades it back out.
|
|
694
|
+
function playClipOnce(entry, clipName) {
|
|
695
|
+
if (!entry.mixer || !clipName) return;
|
|
696
|
+
var action = entry.actions[clipName];
|
|
697
|
+
if (!action) return;
|
|
698
|
+
var prev = entry.currentClip && entry.currentClip !== clipName ? entry.actions[entry.currentClip] : null;
|
|
699
|
+
action.reset();
|
|
700
|
+
action.setLoop(THREE.LoopOnce, 1);
|
|
701
|
+
action.clampWhenFinished = true;
|
|
702
|
+
action.fadeIn(0.15).play();
|
|
703
|
+
if (prev) prev.fadeOut(0.15);
|
|
704
|
+
entry.oneShotAction = action;
|
|
705
|
+
entry.currentClip = null;
|
|
706
|
+
}
|
|
707
|
+
|
|
640
708
|
function playSpawnFlourish(group, now) {
|
|
641
709
|
group.scale.setScalar(0);
|
|
642
710
|
animateScaleTo(group, 1, now);
|
|
@@ -667,10 +735,11 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
667
735
|
}
|
|
668
736
|
}
|
|
669
737
|
|
|
670
|
-
function removeAgent(id) {
|
|
738
|
+
function removeAgent(id, source) {
|
|
671
739
|
var entry = agentGroups[id];
|
|
672
740
|
if (!entry) return;
|
|
673
741
|
delete agentGroups[id];
|
|
742
|
+
recordRemoval("agent", id, source);
|
|
674
743
|
var deathClip = entry.clipMap && entry.clipMap.death;
|
|
675
744
|
var deathClipName = Array.isArray(deathClip) ? deathClip[0] : deathClip;
|
|
676
745
|
if (deathClipName) {
|
|
@@ -683,10 +752,17 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
683
752
|
|
|
684
753
|
function applyEcology(ecology) {
|
|
685
754
|
for (var i = 0; i < (ecology || []).length; i += 1) {
|
|
686
|
-
var
|
|
755
|
+
var event = ecology[i];
|
|
756
|
+
var flourish = flourishForEcologyEvent(event);
|
|
687
757
|
if (!flourish) continue;
|
|
688
|
-
if (flourish.kind === "death") removeAgent(flourish.targetId);
|
|
689
|
-
else if (flourish.kind === "consume") removeItem(flourish.targetId, true);
|
|
758
|
+
if (flourish.kind === "death") removeAgent(flourish.targetId, "ecology");
|
|
759
|
+
else if (flourish.kind === "consume") removeItem(flourish.targetId, true, "ecology");
|
|
760
|
+
if (flourish.actorId) {
|
|
761
|
+
var actorEntry = agentGroups[flourish.actorId];
|
|
762
|
+
if (actorEntry && actorEntry.clipMap) {
|
|
763
|
+
playClipOnce(actorEntry, clipForAction(actorEntry.role, event.type, actorEntry.clipMap));
|
|
764
|
+
}
|
|
765
|
+
}
|
|
690
766
|
}
|
|
691
767
|
}
|
|
692
768
|
|
|
@@ -741,6 +817,7 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
741
817
|
itemMeshes = {};
|
|
742
818
|
lastAgentsById = {};
|
|
743
819
|
lastItemsById = {};
|
|
820
|
+
removalLog = [];
|
|
744
821
|
manifestByKind = buildManifestByKind(input && input.assetManifest);
|
|
745
822
|
await placeProps((input && input.propPlacements) || []);
|
|
746
823
|
setCamera({ mode: "overhead", selectedId: null });
|
|
@@ -754,9 +831,15 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
754
831
|
var items = (tick && tick.items) || {};
|
|
755
832
|
for (var id in agents) if (Object.prototype.hasOwnProperty.call(agents, id)) applyAgentTick(id, agents[id], now);
|
|
756
833
|
for (var itemId in items) if (Object.prototype.hasOwnProperty.call(items, itemId)) applyItemTick(itemId, items[itemId], now);
|
|
757
|
-
|
|
758
|
-
|
|
834
|
+
// Ecology first: an eaten agent/item is already gone from this tick's own
|
|
835
|
+
// agents/items map, so applyEcology's own removeAgent/removeItem call is
|
|
836
|
+
// what actually removes it (with its flourish). The two diff loops below
|
|
837
|
+
// then find that id already deleted and no-op — removeAgent/removeItem
|
|
838
|
+
// both delete their map entry before anything else, so a second call for
|
|
839
|
+
// the same id returns immediately.
|
|
759
840
|
applyEcology(tick && tick.ecology);
|
|
841
|
+
for (var goneAgent in lastAgentsById) if (!(goneAgent in agents)) removeAgent(goneAgent, "diff");
|
|
842
|
+
for (var goneItem in lastItemsById) if (!(goneItem in items)) removeItem(goneItem, false, "diff");
|
|
760
843
|
lastAgentsById = agents;
|
|
761
844
|
lastItemsById = items;
|
|
762
845
|
refreshCameraTween(now);
|
|
@@ -812,6 +895,15 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
812
895
|
var asset = manifestByKind[entry.kind];
|
|
813
896
|
return { height: size.y, minY: box.min.y, targetHeight: asset ? asset.targetHeight : null };
|
|
814
897
|
},
|
|
898
|
+
// Every removal recorded for id, oldest first — each entry's own
|
|
899
|
+
// source is "ecology" (an eat/starve flourish removed it) or "diff"
|
|
900
|
+
// (applyTick found it missing from a tick's own agents/items map with no
|
|
901
|
+
// ecology event driving that). An e2e assertion's read.
|
|
902
|
+
removalsFor: function (id) {
|
|
903
|
+
var out = [];
|
|
904
|
+
for (var i = 0; i < removalLog.length; i += 1) if (removalLog[i].id === id) out.push(removalLog[i]);
|
|
905
|
+
return out;
|
|
906
|
+
},
|
|
815
907
|
ready: function () { return booted; },
|
|
816
908
|
};
|
|
817
909
|
})();`;
|
|
@@ -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,
|
|
18
|
-
agentKindOf, liveIdsOfKind, layoutNamed,
|
|
17
|
+
cellId, parseCellId, inBounds, isSolid, chebyshevDistance, oneStepDirectionBetween,
|
|
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";
|
|
@@ -88,6 +90,25 @@ const MUDIII_TOLD_RE = new RegExp(
|
|
|
88
90
|
// never ground truth.
|
|
89
91
|
const MUDIII_SEE_RE = /^what (?:does|can) the (fox|goblin)(?:-(\d+))?\s+see[.!?\s]*$/i;
|
|
90
92
|
|
|
93
|
+
// "who put/placed that there?" — provenance, ground truth (not belief), read
|
|
94
|
+
// from placeFood's own mgx:placed-by row. The subject must be named, either
|
|
95
|
+
// an item id ("who put morsel-1 there?") or a cell ("who put food at
|
|
96
|
+
// cell-3-4?") — a bare "who put that there?" with no referent at all names
|
|
97
|
+
// nothing to look up and is left to the ordinary lanes. Requires the leading
|
|
98
|
+
// "who put/placed/dropped" verb, so it never collides with an unrelated "who"
|
|
99
|
+
// question (the fox's own catches, the goblins' names, anything not about a
|
|
100
|
+
// placement) — those never start with this verb at all.
|
|
101
|
+
const MUDIII_WHO_PLACED_RE = new RegExp(
|
|
102
|
+
"^who\\s+(?:put|placed|dropped)\\s+"
|
|
103
|
+
+ "(?:"
|
|
104
|
+
+ "(?:the\\s+)?(morsel|crumb)(?:-(\\d+))?"
|
|
105
|
+
+ "|"
|
|
106
|
+
+ "(?:(?:the\\s+)?(?:food|morsel|crumb|that|it)\\s+)?(?:at|on)\\s+(cell-\\d+-\\d+)"
|
|
107
|
+
+ ")"
|
|
108
|
+
+ "(?:\\s+there)?[?.!\\s]*$",
|
|
109
|
+
"i",
|
|
110
|
+
);
|
|
111
|
+
|
|
91
112
|
// The player's own verb: "put food at cell-3-4" (primary, never shadowed —
|
|
92
113
|
// the adventure imperative grammar's own "put" arm hard-requires a literal
|
|
93
114
|
// "in", so "at" never parses there) and "drop a morsel at cell-3-4"
|
|
@@ -441,6 +462,63 @@ async function mudiiiBeliefAnswer(match, { memoryDir, gameConfig = DEFAULT_GAME_
|
|
|
441
462
|
};
|
|
442
463
|
}
|
|
443
464
|
|
|
465
|
+
function noLivePlacedSubjectAnswer(kind) {
|
|
466
|
+
return {
|
|
467
|
+
text: `there's no live ${kind} on the board for that to be about.`,
|
|
468
|
+
lane: "game-inform",
|
|
469
|
+
note: `MUDIII — who-placed declined: no live ${kind} resolves`,
|
|
470
|
+
miss: true,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** "who put/placed <item|cell> there?": ground truth read off the folded
|
|
475
|
+
* state's own placedBy Map (predator-prey.mjs's fold of every mgx:placed-by
|
|
476
|
+
* row), never belief. Resolves the subject the same way the told-fact and
|
|
477
|
+
* see recognizers already do — resolveAgentId for a named item, a direct
|
|
478
|
+
* placements scan for a cell — then declines, naming the reason, when the
|
|
479
|
+
* item isn't on the board at all or is on the board but carries no placedBy
|
|
480
|
+
* row (spawned food never does; placeFood is the only writer of that row). */
|
|
481
|
+
async function mudiiiWhoPlacedAnswer(match, { memoryDir }) {
|
|
482
|
+
const [, kindRaw, num, cellLiteral] = match;
|
|
483
|
+
const rows = readFactRows(await loadMemory(memoryDir));
|
|
484
|
+
const state = foldTownSquareState(rows);
|
|
485
|
+
|
|
486
|
+
let itemId = null;
|
|
487
|
+
if (kindRaw) {
|
|
488
|
+
itemId = resolveAgentId(kindRaw.toLowerCase(), num, state);
|
|
489
|
+
} else {
|
|
490
|
+
const entry = [...state.placements.entries()].find(([id, place]) =>
|
|
491
|
+
isFoodId(id) && !state.removed.has(id) && place.cell === cellLiteral);
|
|
492
|
+
itemId = entry ? entry[0] : null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!itemId) {
|
|
496
|
+
if (kindRaw) return noLivePlacedSubjectAnswer(kindRaw.toLowerCase());
|
|
497
|
+
return {
|
|
498
|
+
text: `there's no food at ${cellLiteral} to ask about.`,
|
|
499
|
+
lane: "game-inform",
|
|
500
|
+
note: `MUDIII — who-placed declined: no live food item sits at ${cellLiteral}`,
|
|
501
|
+
miss: true,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const placed = state.placedBy.get(itemId);
|
|
506
|
+
if (!placed) {
|
|
507
|
+
return {
|
|
508
|
+
text: `${itemId} was never placed — it spawned on its own.`,
|
|
509
|
+
lane: "game-inform",
|
|
510
|
+
note: `MUDIII — who-placed declined: ${itemId} carries no mgx:placed-by row (spawned food)`,
|
|
511
|
+
miss: true,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
text: `${placed.by} put ${itemId} at ${state.placements.get(itemId).cell}.`,
|
|
517
|
+
lane: "game-inform",
|
|
518
|
+
note: `MUDIII — who-placed answered from state.placedBy for ${itemId}`,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
444
522
|
/** The addressed teach-frame turn: resolve the addressee and the belief
|
|
445
523
|
* subject (an agent OR a food item), resolve the told cell, and run ONE tick
|
|
446
524
|
* with that told-fact fed in. Told-facts are not persisted across turns —
|
|
@@ -477,6 +555,242 @@ async function runToldFactTurn(match, { planHolder, memoryDir, cache, gameConfig
|
|
|
477
555
|
});
|
|
478
556
|
}
|
|
479
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
|
+
|
|
480
794
|
// ---- in-game orientation asides ---------------------------------------------
|
|
481
795
|
//
|
|
482
796
|
// "where is the fox", "where am I", "what can I do", "what is the fox's
|
|
@@ -632,6 +946,17 @@ export async function mudiiiTurn(line, { planHolder, memoryDir, env, cache = nul
|
|
|
632
946
|
return runPlaceFoodTurn(putMatch[1], { memoryDir, gameConfig, world: mudiii.world, layout });
|
|
633
947
|
}
|
|
634
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
|
+
|
|
635
960
|
if (isPlanFrameLine(line)) {
|
|
636
961
|
return {
|
|
637
962
|
text: 'the town square game is running — say "stop watching" to end it, then set your goal.',
|
|
@@ -659,6 +984,11 @@ export async function mudiiiTurn(line, { planHolder, memoryDir, env, cache = nul
|
|
|
659
984
|
return mudiiiBeliefAnswer(seeMatch, { memoryDir, gameConfig });
|
|
660
985
|
}
|
|
661
986
|
|
|
987
|
+
const whoPlacedMatch = trimmed.match(MUDIII_WHO_PLACED_RE);
|
|
988
|
+
if (whoPlacedMatch) {
|
|
989
|
+
return mudiiiWhoPlacedAnswer(whoPlacedMatch, { memoryDir });
|
|
990
|
+
}
|
|
991
|
+
|
|
662
992
|
if (MUDIII_TICK_RE.test(line)) {
|
|
663
993
|
return runTickAndRender({ planHolder, memoryDir, cache, world: mudiii.world, toldFacts: [], gameConfig });
|
|
664
994
|
}
|