@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
|
@@ -497,32 +497,41 @@ ${scenarioList.map((s, i) => ` <option value="${i}"${i === 0 ? " select
|
|
|
497
497
|
<button type="button" class="deck-info-btn" id="deckInfoBtn" aria-expanded="false" aria-controls="deckInfoPopup" aria-label="about this demo">?</button>
|
|
498
498
|
<span class="mono deck-turns" id="globalTurnCount">turns: 0</span>
|
|
499
499
|
</div>
|
|
500
|
-
<div class="deck-
|
|
501
|
-
<
|
|
502
|
-
<
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
<
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
<
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
<
|
|
524
|
-
|
|
525
|
-
|
|
500
|
+
<div class="deck-body">
|
|
501
|
+
<div class="deck-sliders">
|
|
502
|
+
<label class="deck-slider">foxes
|
|
503
|
+
<input type="range" id="playerCountSlider" min="0" max="${PLAYER_COUNTS.length - 1}" step="1"
|
|
504
|
+
value="${Math.max(0, PLAYER_COUNTS.indexOf(DEFAULT_PLAYER_COUNT))}"
|
|
505
|
+
list="playerCountTicks" aria-valuetext="${DEFAULT_PLAYER_COUNT} foxes">
|
|
506
|
+
<datalist id="playerCountTicks">${PLAYER_COUNTS.map((n, i) => `<option value="${i}" label="${n}"></option>`).join("")}</datalist>
|
|
507
|
+
<span class="mono" id="playerCountValue">${DEFAULT_PLAYER_COUNT}</span>
|
|
508
|
+
</label>
|
|
509
|
+
<label class="deck-slider">goblins
|
|
510
|
+
<input type="range" id="npcCountSlider" min="${NPC_COUNT_MIN}" max="${NPC_COUNT_MAX}" step="1"
|
|
511
|
+
value="${DEFAULT_NPC_COUNT}"
|
|
512
|
+
list="npcCountTicks" aria-valuetext="${DEFAULT_NPC_COUNT} goblins">
|
|
513
|
+
<datalist id="npcCountTicks">${Array.from({ length: NPC_COUNT_MAX - NPC_COUNT_MIN + 1 }, (_, i) => {
|
|
514
|
+
const n = NPC_COUNT_MIN + i;
|
|
515
|
+
return NPC_COUNT_LABELLED.includes(n) ? `<option value="${n}" label="${n}"></option>` : `<option value="${n}"></option>`;
|
|
516
|
+
}).join("")}</datalist>
|
|
517
|
+
<span class="mono" id="npcCountValue">${DEFAULT_NPC_COUNT}</span>
|
|
518
|
+
</label>
|
|
519
|
+
<label class="deck-slider">delay
|
|
520
|
+
<input type="range" id="delaySlider" min="80" max="2000" step="20" value="${DEFAULT_DELAY_MS}">
|
|
521
|
+
<span class="mono" id="delayValue">${DEFAULT_DELAY_MS}ms</span>
|
|
522
|
+
</label>
|
|
523
|
+
<label class="deck-slider">max turns
|
|
524
|
+
<input type="range" id="maxTurnsSlider" min="20" max="2000" step="20" value="${DEFAULT_MAX_TURNS}">
|
|
525
|
+
<span class="mono" id="maxTurnsValue">${DEFAULT_MAX_TURNS}</span>
|
|
526
|
+
</label>
|
|
527
|
+
</div>
|
|
528
|
+
<section class="map-panel" id="mapPanel" aria-label="the town square, from above">
|
|
529
|
+
<div class="map-panel-head">
|
|
530
|
+
<span class="map-panel-title">the square, from above</span>
|
|
531
|
+
<span class="mono map-panel-turn" id="mapPanelTurn">turn 0</span>
|
|
532
|
+
</div>
|
|
533
|
+
<div class="map-panel-board" id="mapPanelBoard"></div>
|
|
534
|
+
</section>
|
|
526
535
|
</div>
|
|
527
536
|
<div class="deck-camera">
|
|
528
537
|
<label class="deck-slider">follow
|
|
@@ -542,13 +551,6 @@ ${openingAgents.map((a) => ` <option value="${escapeHtml(a.id)}">${es
|
|
|
542
551
|
<button type="button" class="deck-info-popup-close" id="deckInfoClose" aria-label="close">×</button>
|
|
543
552
|
</div>
|
|
544
553
|
</section>
|
|
545
|
-
<section class="map-panel" id="mapPanel" aria-label="the town square, from above">
|
|
546
|
-
<div class="map-panel-head">
|
|
547
|
-
<span class="map-panel-title">the square, from above</span>
|
|
548
|
-
<span class="mono map-panel-turn" id="mapPanelTurn">turn 0</span>
|
|
549
|
-
</div>
|
|
550
|
-
<div class="map-panel-board" id="mapPanelBoard"></div>
|
|
551
|
-
</section>
|
|
552
554
|
</div>
|
|
553
555
|
<section class="scene-stage" id="sceneStage" aria-label="the town square, in three dimensions">
|
|
554
556
|
<canvas id="sceneCanvas"></canvas>
|
|
@@ -628,7 +630,7 @@ const MUDIII_STYLE = `
|
|
|
628
630
|
}
|
|
629
631
|
.mudiii-topbar-help:hover { border-color: var(--square-accent); }
|
|
630
632
|
|
|
631
|
-
.deck-row {
|
|
633
|
+
.deck-row { margin-bottom: 1rem; }
|
|
632
634
|
.deck {
|
|
633
635
|
position: relative;
|
|
634
636
|
background: var(--parchment); border: 1px solid var(--square-stone-dark); border-radius: 4px;
|
|
@@ -656,9 +658,10 @@ const MUDIII_STYLE = `
|
|
|
656
658
|
.deck-play { background: var(--square-ink) !important; color: var(--parchment); border-color: var(--square-ink) !important; padding: .38rem 1.1rem !important; }
|
|
657
659
|
.deck-play[aria-pressed="true"] { background: var(--square-accent) !important; border-color: var(--square-accent) !important; color: var(--square-ink); }
|
|
658
660
|
.deck-turns { margin-left: auto; font-size: .74rem; color: var(--square-stone-dark); background: var(--square-stone-dark); background: rgba(43,35,24,.9); color: var(--square-accent); border-radius: 2px; padding: .1rem .5rem; }
|
|
659
|
-
.deck-
|
|
660
|
-
.deck-
|
|
661
|
-
.deck-slider
|
|
661
|
+
.deck-body { display: flex; gap: .7rem; align-items: flex-start; }
|
|
662
|
+
.deck-sliders { display: flex; flex-wrap: wrap; gap: 1rem; flex: 1 1 auto; min-width: 0; }
|
|
663
|
+
.deck-slider { display: flex; align-items: center; gap: .35rem; font-family: ${MONO_STACK}; font-size: .62rem; text-transform: uppercase; letter-spacing: .08em; color: var(--square-stone-dark); min-width: 0; }
|
|
664
|
+
.deck-slider input[type="range"] { accent-color: var(--square-accent); flex: 1 1 4rem; min-width: 2.5rem; width: auto; max-width: 8rem; }
|
|
662
665
|
.camera-mode { display: inline-flex; gap: .25rem; }
|
|
663
666
|
.camera-mode button[aria-pressed="true"] { background: var(--square-accent); border-color: var(--square-accent); color: var(--square-ink); }
|
|
664
667
|
.deck-info-popup {
|
|
@@ -678,13 +681,14 @@ const MUDIII_STYLE = `
|
|
|
678
681
|
|
|
679
682
|
.map-panel {
|
|
680
683
|
background: var(--square-stone-dark); color: var(--parchment);
|
|
681
|
-
border: 1px solid var(--square-accent); border-radius: 4px; padding: .
|
|
682
|
-
display: flex; flex-direction: column; gap: .
|
|
684
|
+
border: 1px solid var(--square-accent); border-radius: 4px; padding: .5rem .55rem .55rem;
|
|
685
|
+
display: flex; flex-direction: column; gap: .35rem; min-width: 0;
|
|
686
|
+
flex: 0 0 50%; max-width: 50%;
|
|
683
687
|
}
|
|
684
|
-
.map-panel-head { display: flex; justify-content: space-between; align-items: baseline; gap: .
|
|
685
|
-
.map-panel-title { font-family: ${MONO_STACK}; font-size: .
|
|
686
|
-
.map-panel-turn { font-size: .
|
|
687
|
-
.map-panel-board { position: relative; flex: 1; min-height:
|
|
688
|
+
.map-panel-head { display: flex; justify-content: space-between; align-items: baseline; gap: .4rem; }
|
|
689
|
+
.map-panel-title { font-family: ${MONO_STACK}; font-size: .54rem; text-transform: uppercase; letter-spacing: .1em; opacity: .85; }
|
|
690
|
+
.map-panel-turn { font-size: .58rem; opacity: .7; }
|
|
691
|
+
.map-panel-board { position: relative; flex: 1; min-height: 110px; aspect-ratio: 1; background: rgba(124,154,91,.25); border: 1px solid rgba(233,217,182,.35); border-radius: 3px; }
|
|
688
692
|
.map-dot { position: absolute; width: .55rem; height: .55rem; margin: -.28rem 0 0 -.28rem; border-radius: 50%; border: 1px solid rgba(0,0,0,.4); }
|
|
689
693
|
.map-dot-predator { background: var(--square-predator); }
|
|
690
694
|
.map-dot-prey { background: var(--square-prey); }
|
|
@@ -716,7 +720,6 @@ const MUDIII_STYLE = `
|
|
|
716
720
|
|
|
717
721
|
.edit-stage { display: none; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 1rem; align-items: start; margin-bottom: 1rem; }
|
|
718
722
|
body.editing .mudiii-chat, body.editing .scene-stage, body.editing .hud-row, body.editing .deck-row .map-panel { display: none; }
|
|
719
|
-
body.editing .deck-row { grid-template-columns: 1fr; }
|
|
720
723
|
body.editing .edit-stage { display: grid; }
|
|
721
724
|
#editModeBtn[aria-pressed="true"] { background: var(--square-accent); border-color: var(--square-accent); }
|
|
722
725
|
.edit-text, .edit-panel {
|
|
@@ -769,10 +772,22 @@ const MUDIII_STYLE = `
|
|
|
769
772
|
.pill.affordance[aria-pressed="true"] { background: var(--square-accent); border-style: solid; }
|
|
770
773
|
|
|
771
774
|
@media (max-width: 900px) {
|
|
772
|
-
.deck-row { grid-template-columns: 1fr; }
|
|
773
775
|
.edit-stage { grid-template-columns: 1fr; }
|
|
774
776
|
#editorText { min-height: 16rem; }
|
|
775
777
|
}
|
|
778
|
+
|
|
779
|
+
/* A landscape phone and a narrow desktop window are both under 900px but
|
|
780
|
+
want different slider/map arrangements, so the split has to key off
|
|
781
|
+
orientation as well as width. */
|
|
782
|
+
@media (max-width: 900px) and (orientation: landscape) {
|
|
783
|
+
.deck-sliders { display: grid; grid-template-columns: 1fr 1fr; gap: .4rem 1rem; }
|
|
784
|
+
.deck-body { align-items: stretch; }
|
|
785
|
+
.map-panel { flex-basis: 33%; max-width: 33%; }
|
|
786
|
+
/* The square aspect-ratio that suits a tall portrait column would blow
|
|
787
|
+
the map back up to full column width in a short landscape viewport —
|
|
788
|
+
here it follows the two-row slider stack's own height instead. */
|
|
789
|
+
.map-panel-board { aspect-ratio: auto; min-height: 90px; }
|
|
790
|
+
}
|
|
776
791
|
`;
|
|
777
792
|
|
|
778
793
|
/** The inlined page script, spliced the same way mud-viz.mjs's own
|
|
@@ -813,6 +828,7 @@ function pageScript() {
|
|
|
813
828
|
const el = (id) => document.getElementById(id);
|
|
814
829
|
let scenarioIndex = 0;
|
|
815
830
|
const scenario = function () { return DATA.scenarios[scenarioIndex]; };
|
|
831
|
+
const gridSizeOf = function () { return scenario().gridSize || DATA.gridSize; };
|
|
816
832
|
const rosterOf = function (s, role) {
|
|
817
833
|
return (s.agents || []).filter(function (a) { return !role || a.role === role; }).map(function (a) { return a.id; });
|
|
818
834
|
};
|
|
@@ -881,6 +897,9 @@ function pageScript() {
|
|
|
881
897
|
|
|
882
898
|
function applyTickResult(result) {
|
|
883
899
|
if (!result) return;
|
|
900
|
+
// The engine owns the count. Anything that advances a turn — the deck, a
|
|
901
|
+
// chat frame — lands here, so the page never keeps a rival tally.
|
|
902
|
+
if (typeof result.turn === "number") globalTurn = result.turn;
|
|
884
903
|
if (result.agents) agentsById = result.agents;
|
|
885
904
|
if (result.items) itemsById = result.items;
|
|
886
905
|
callScene("applyTick", { agents: result.agents, items: result.items, ecology: result.ecology });
|
|
@@ -892,8 +911,7 @@ function pageScript() {
|
|
|
892
911
|
async function runOneTick() {
|
|
893
912
|
return serializeTick(async function () {
|
|
894
913
|
if (!session) return null;
|
|
895
|
-
|
|
896
|
-
const result = await session.tick(globalTurn);
|
|
914
|
+
const result = await session.tick();
|
|
897
915
|
applyTickResult(result);
|
|
898
916
|
renderAll();
|
|
899
917
|
return result;
|
|
@@ -921,8 +939,16 @@ function pageScript() {
|
|
|
921
939
|
function sendCommand(line) {
|
|
922
940
|
appendChat("u", line);
|
|
923
941
|
if (!session) { appendChat("a", "no session is open yet \\u2014 reset to start one."); return Promise.resolve(); }
|
|
924
|
-
|
|
942
|
+
// A chat line can run a real turn. An addressed told-fact does, and the
|
|
943
|
+
// visitor should watch the lie land, so the board is read back in the same
|
|
944
|
+
// queue slot. board() spends no turn, so a line that ran none costs
|
|
945
|
+
// nothing. It reports no goal or plan either, because a resting board has
|
|
946
|
+
// decided nothing, which is the blank boot() already draws at turn 0.
|
|
947
|
+
return serializeTick(async function () {
|
|
948
|
+
const res = await tmct.turn(line);
|
|
949
|
+
const board = await session.board();
|
|
925
950
|
appendChat("a", res.answer);
|
|
951
|
+
applyTickResult(board);
|
|
926
952
|
renderAll();
|
|
927
953
|
return res;
|
|
928
954
|
});
|
|
@@ -1027,7 +1053,7 @@ function pageScript() {
|
|
|
1027
1053
|
|
|
1028
1054
|
// ---- the top-down map panel ---------------------------------------------
|
|
1029
1055
|
function renderMapPanel() {
|
|
1030
|
-
const dots = mapDotsFor(agentsList(), itemsList(),
|
|
1056
|
+
const dots = mapDotsFor(agentsList(), itemsList(), gridSizeOf());
|
|
1031
1057
|
el("mapPanelBoard").innerHTML = dots.map(function (d) {
|
|
1032
1058
|
return '<span class="map-dot map-dot-' + esc(d.kind) + '" style="left:' + d.xPct + '%;top:' + d.yPct + '%" title="' + esc(d.id) + '"></span>';
|
|
1033
1059
|
}).join("");
|
|
@@ -1216,7 +1242,7 @@ function pageScript() {
|
|
|
1216
1242
|
agentsById = {};
|
|
1217
1243
|
itemsById = {};
|
|
1218
1244
|
el("chatInput").disabled = false;
|
|
1219
|
-
await callScene("boot", { propPlacements: props, assetManifest: DATA.assetManifest, gridSize:
|
|
1245
|
+
await callScene("boot", { propPlacements: props, assetManifest: DATA.assetManifest, gridSize: gridSizeOf(), cellSize: 1 });
|
|
1220
1246
|
|
|
1221
1247
|
// The opening board, drawn through the very path a tick takes. Without
|
|
1222
1248
|
// this the page's first sight of where anything stands is the first tick,
|
|
@@ -1225,7 +1251,6 @@ function pageScript() {
|
|
|
1225
1251
|
// the cells both come back from it rather than being guessed here.
|
|
1226
1252
|
const opening = await session.board();
|
|
1227
1253
|
if (seq !== bootSeq) return;
|
|
1228
|
-
globalTurn = opening.turn || 0;
|
|
1229
1254
|
camera.selectedId = Object.keys(opening.agents || {}).sort()[0] || null;
|
|
1230
1255
|
applyTickResult(opening);
|
|
1231
1256
|
renderAll();
|
|
@@ -58,6 +58,12 @@ const PLACEMENT_PREDICATE = "mgx:currently-in";
|
|
|
58
58
|
const MASS_PREDICATE = "mgx:hasMass";
|
|
59
59
|
const MOOD_PREDICATE = "mgx:feels";
|
|
60
60
|
const FACING_PREDICATE = "mgx:facing";
|
|
61
|
+
// The facing a visitor's own hand turned an agent to, written only on a driven
|
|
62
|
+
// turn. It folds into the same facing map as the ordinary row below, which is
|
|
63
|
+
// what makes a hand turn hold while the agent stands still and lose the moment
|
|
64
|
+
// the planner takes a step of its own: the step's own facing row is stamped
|
|
65
|
+
// with a later turn, and a later turn outranks.
|
|
66
|
+
const DRIVEN_FACING_PREDICATE = "mgx:driven-facing";
|
|
61
67
|
const EATEN_BY_PREDICATE = "mgx:eaten-by";
|
|
62
68
|
const STARVED_PREDICATE = "mgx:starved";
|
|
63
69
|
const PLACED_BY_PREDICATE = "mgx:placed-by";
|
|
@@ -86,7 +92,7 @@ const TURN_PLAYED_PREDICATE = "mgx:turn-played";
|
|
|
86
92
|
const BOARD_SUBJECT = "square";
|
|
87
93
|
|
|
88
94
|
const MUDIII_STATE_PREDICATE_SET = new Set([
|
|
89
|
-
PLACEMENT_PREDICATE, MASS_PREDICATE, MOOD_PREDICATE, FACING_PREDICATE,
|
|
95
|
+
PLACEMENT_PREDICATE, MASS_PREDICATE, MOOD_PREDICATE, FACING_PREDICATE, DRIVEN_FACING_PREDICATE,
|
|
90
96
|
EATEN_BY_PREDICATE, STARVED_PREDICATE, PLACED_BY_PREDICATE,
|
|
91
97
|
MODEL_PREDICATE, ROTATION_PREDICATE, TURN_PLAYED_PREDICATE,
|
|
92
98
|
CARRYING_PREDICATE, PREY_EATEN_PREDICATE, WEB_BUILT_PREDICATE,
|
|
@@ -208,6 +214,7 @@ export function foldTownSquareState(factRows) {
|
|
|
208
214
|
if (snap && rowEpoch === epoch) tickCount = Math.max(tickCount, turn);
|
|
209
215
|
break;
|
|
210
216
|
case FACING_PREDICATE:
|
|
217
|
+
case DRIVEN_FACING_PREDICATE:
|
|
211
218
|
if (outranks(rowEpoch, turn, facing.get(base))) facing.set(base, { value: row.object, turn, epoch: rowEpoch });
|
|
212
219
|
break;
|
|
213
220
|
case EATEN_BY_PREDICATE:
|
|
@@ -448,6 +455,29 @@ function stepPlan(fromCell, toCell) {
|
|
|
448
455
|
return direction ? [direction] : [];
|
|
449
456
|
}
|
|
450
457
|
|
|
458
|
+
/**
|
|
459
|
+
* The hand-driven move `entry` asks of an agent standing at `fromCell`, as
|
|
460
|
+
* `{ cell, facing }` with `cell` a parsed cell — or null when nothing was
|
|
461
|
+
* asked, or when what was asked is not a legal one-step move.
|
|
462
|
+
*
|
|
463
|
+
* `entry` is a target cell id, or `{ cell, facing }` with both parts optional:
|
|
464
|
+
* no cell holds the agent where it stands, which is what a turn on the spot
|
|
465
|
+
* is, and no facing leaves the facing to the step itself.
|
|
466
|
+
*
|
|
467
|
+
* The legality table is oneStepOptions — the very list the wander, evade and
|
|
468
|
+
* chase rungs pick from — so a wall or a prop is a missing exit here too,
|
|
469
|
+
* never a second notion of blocked, and a refusal reads as "no such move"
|
|
470
|
+
* rather than as a rule this seam invented. Pure.
|
|
471
|
+
*/
|
|
472
|
+
function acceptedManualMove(entry, fromCell, applyActions) {
|
|
473
|
+
if (!entry) return null;
|
|
474
|
+
const request = typeof entry === "string" ? { cell: entry } : entry;
|
|
475
|
+
const facing = typeof request.facing === "string" && request.facing ? request.facing : null;
|
|
476
|
+
const target = typeof request.cell === "string" && request.cell ? request.cell : cellId(fromCell.x, fromCell.y);
|
|
477
|
+
const cell = oneStepOptions(fromCell, applyActions).find((option) => cellId(option.x, option.y) === target);
|
|
478
|
+
return cell ? { cell, facing } : null;
|
|
479
|
+
}
|
|
480
|
+
|
|
451
481
|
const round2 = (n) => Math.round(n * 100) / 100;
|
|
452
482
|
|
|
453
483
|
// ---- the goal line and the mood word -------------------------------------------
|
|
@@ -457,8 +487,9 @@ const round2 = (n) => Math.round(n * 100) / 100;
|
|
|
457
487
|
// is scared, anything wandering or foraging is calm, and anything that just ate
|
|
458
488
|
// is happy.
|
|
459
489
|
|
|
460
|
-
function goalLine(kind, { subject, cell, arrived, boardNoun = "square", catches = false } = {}) {
|
|
490
|
+
function goalLine(kind, { subject, cell, arrived, boardNoun = "square", catches = false, facing = null, held = false } = {}) {
|
|
461
491
|
switch (kind) {
|
|
492
|
+
case "driven": return held ? `driven by hand — holding at ${cell}, facing ${facing}.` : `driven by hand — stepping to ${cell}.`;
|
|
462
493
|
case "avoid": return `avoiding ${subject}, last seen at ${cell}.`;
|
|
463
494
|
case "chase":
|
|
464
495
|
if (!arrived) return `chasing ${subject}, last seen at ${cell}.`;
|
|
@@ -980,11 +1011,20 @@ function itemsPayload(liveItemIds, { state, itemCellOf, roles, taken = null }) {
|
|
|
980
1011
|
* to where the predator was when it looked), while eating resolves on the
|
|
981
1012
|
* post-move ones (it is caught where the predator actually ends up).
|
|
982
1013
|
*
|
|
1014
|
+
* `manualMoves` is the one place a visitor's own hand reaches the world:
|
|
1015
|
+
* `{ agentId: cellId }`, or `{ agentId: { cell, facing } }`, checked before
|
|
1016
|
+
* that agent's belief chain runs. A legal request moves the agent under the
|
|
1017
|
+
* `driven` rung; an illegal one is refused and that agent decides for itself
|
|
1018
|
+
* this turn, so a rejected press never freezes it. A driven turn spends a
|
|
1019
|
+
* turn like any other: the ecology pass runs, and every other agent decides
|
|
1020
|
+
* and moves in this same call.
|
|
1021
|
+
*
|
|
983
1022
|
* Returns `{ turn, epoch, agents, items, ecology, rungs, activeWebs, writes }`.
|
|
984
1023
|
* `agents` and `items` and `ecology` are the frozen render payload — see
|
|
985
1024
|
* townSquareTickPayload, which projects exactly those three plus the turn.
|
|
986
|
-
* `rungs` is the decision each live agent reached this turn ("
|
|
987
|
-
* "forage", "avoid", "wander", and —
|
|
1025
|
+
* `rungs` is the decision each live agent reached this turn ("driven" for a
|
|
1026
|
+
* hand-driven move, then "chase", "evade", "forage", "avoid", "wander", and —
|
|
1027
|
+
* on a cast that carries or spins webs —
|
|
988
1028
|
* "carry", "deliver", "carried", "trapped", "hold-web", "build-web"); an agent
|
|
989
1029
|
* that decided and then died still has a rung and no longer has an `agents`
|
|
990
1030
|
* entry, which is the difference between a decision and a survivor.
|
|
@@ -993,6 +1033,7 @@ function itemsPayload(liveItemIds, { state, itemCellOf, roles, taken = null }) {
|
|
|
993
1033
|
*/
|
|
994
1034
|
export async function runTownSquareTick(memoryDir, {
|
|
995
1035
|
layout, toldFacts = [], config = DEFAULT_GAME_CONFIG.mudiii, roles = MUDIII_ROLES,
|
|
1036
|
+
manualMoves = {},
|
|
996
1037
|
} = {}) {
|
|
997
1038
|
const lay = typeof layout === "string" ? TOWN_SQUARE_LAYOUTS[layout] : layout;
|
|
998
1039
|
if (!lay) throw new Error(`runTownSquareTick: no such layout "${layout}"`);
|
|
@@ -1031,6 +1072,11 @@ export async function runTownSquareTick(memoryDir, {
|
|
|
1031
1072
|
|
|
1032
1073
|
const decide = (agentId, role) => {
|
|
1033
1074
|
const fromCell = parseCellId(state.placements.get(agentId).cell);
|
|
1075
|
+
// The visitor's hand, checked before any of the belief chain below. A
|
|
1076
|
+
// refused request leaves `driven` null, and the chain then runs exactly as
|
|
1077
|
+
// it would have on a turn nobody touched.
|
|
1078
|
+
const driven = acceptedManualMove(manualMoves?.[agentId], fromCell, applyActions);
|
|
1079
|
+
const restingFacing = state.facing.get(agentId)?.value ?? DEFAULT_FACING;
|
|
1034
1080
|
const visionRadius = role === "predator" ? config.predatorVisionRadius : config.preyVisionRadius;
|
|
1035
1081
|
const beliefOpts = { visionRadius, toldFacts };
|
|
1036
1082
|
// True unless a caller explicitly turns it off. A config object built by
|
|
@@ -1045,13 +1091,14 @@ export async function runTownSquareTick(memoryDir, {
|
|
|
1045
1091
|
// lookup below stays on beliefOpts either way: this switch is about food.
|
|
1046
1092
|
const foodBeliefOpts = foodVisionGated ? beliefOpts : { ...beliefOpts, visionRadius: Infinity };
|
|
1047
1093
|
const rivals = role === "predator" ? predators.filter((id) => id !== agentId) : predators;
|
|
1048
|
-
const threat = nearestBelievedTarget(agentId, fromCell, rivals, state, beliefOpts);
|
|
1094
|
+
const threat = driven ? null : nearestBelievedTarget(agentId, fromCell, rivals, state, beliefOpts);
|
|
1049
1095
|
|
|
1050
1096
|
let rung;
|
|
1051
1097
|
let nextCell;
|
|
1052
1098
|
let plan;
|
|
1053
1099
|
let goal;
|
|
1054
1100
|
let mood;
|
|
1101
|
+
let drivenFacing = null;
|
|
1055
1102
|
// A carried prey and its captor both leave the ordinary chain. A carrying
|
|
1056
1103
|
// predator never drops its catch to dodge a rival or chase a second one:
|
|
1057
1104
|
// nothing here eats a predator, so "avoid" is contention, not survival.
|
|
@@ -1060,7 +1107,18 @@ export async function runTownSquareTick(memoryDir, {
|
|
|
1060
1107
|
const captorId = (carriesPrey && role === "prey") ? (captorOfPrey.get(agentId) ?? null) : null;
|
|
1061
1108
|
let beliefCell = fromCell;
|
|
1062
1109
|
|
|
1063
|
-
if (
|
|
1110
|
+
if (driven) {
|
|
1111
|
+
rung = "driven";
|
|
1112
|
+
nextCell = driven.cell;
|
|
1113
|
+
plan = stepPlan(fromCell, nextCell);
|
|
1114
|
+
drivenFacing = driven.facing;
|
|
1115
|
+
goal = goalLine("driven", {
|
|
1116
|
+
cell: cellId(nextCell.x, nextCell.y),
|
|
1117
|
+
facing: driven.facing ?? restingFacing,
|
|
1118
|
+
held: plan.length === 0,
|
|
1119
|
+
});
|
|
1120
|
+
mood = "calm";
|
|
1121
|
+
} else if (isCarrying && webbedAt(fromCell)) {
|
|
1064
1122
|
// Already standing in a web: hold, so the pass's own eat gate reads this
|
|
1065
1123
|
// same cell and resolves the delivery on this exact tick.
|
|
1066
1124
|
rung = "deliver";
|
|
@@ -1166,7 +1224,10 @@ export async function runTownSquareTick(memoryDir, {
|
|
|
1166
1224
|
const foodCandidates = beliefCandidates.filter((id) => foodIds.has(id));
|
|
1167
1225
|
Object.assign(belief, beliefSnapshotFor(agentId, beliefCell, foodCandidates, state, foodBeliefOpts));
|
|
1168
1226
|
}
|
|
1169
|
-
|
|
1227
|
+
// A hand-picked facing beats the step it came with, which is what lets the
|
|
1228
|
+
// page walk an agent backwards without spinning it round.
|
|
1229
|
+
const facing = drivenFacing ?? plan[0] ?? restingFacing;
|
|
1230
|
+
if (rung === "driven") movementWrites.push({ subject: stamp(agentId), predicate: DRIVEN_FACING_PREDICATE, object: facing });
|
|
1170
1231
|
postMovePlacements.set(agentId, nextCell);
|
|
1171
1232
|
rungs[agentId] = rung;
|
|
1172
1233
|
agents[agentId] = { role, cell: cellId(nextCell.x, nextCell.y), facing, goal, mood, plan, mass: 0, belief };
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
// spider-fly-viz.mjs — the spider-and-fly full-screen page
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// portable on its own — that reason doesn't apply here).
|
|
1
|
+
// spider-fly-viz.mjs — the spider-and-fly full-screen page: a self-contained
|
|
2
|
+
// document shaped exactly like ledger-viz.mjs/plan-viz.mjs — one inlined <style>
|
|
3
|
+
// (importing viz-theme.mjs's shared tokens), behaviour as an inlined IIFE — but
|
|
4
|
+
// unlike those two, almost nothing is embedded as build-time data: the whole
|
|
5
|
+
// game is LIVE client-side state (spider-fly-browser-entry.mjs's
|
|
6
|
+
// createSpiderFlySession), so renderSpiderFlyHtml only needs the page's own
|
|
7
|
+
// static grid geometry (which never changes) plus a title. The engine, sprite
|
|
8
|
+
// resolver and chat turn engine all arrive via ./spider-fly-browser.bundle.js,
|
|
9
|
+
// referenced with a plain same-origin <script src> — the same sibling-file
|
|
10
|
+
// arrangement index.html already uses for chat-browser.bundle.js (both are the
|
|
11
|
+
// FULL turn engine, both generated fresh per build, neither meant to be
|
|
12
|
+
// committed), not memory-ask-browser.bundle.js's inlined-text arrangement (that
|
|
13
|
+
// bundle is small and inlined specifically so ledger.html stays portable on its
|
|
14
|
+
// own — that reason doesn't apply here).
|
|
16
15
|
//
|
|
17
16
|
// renderSpiderFlyHtml() is pure: no I/O, deterministic output for identical
|
|
18
17
|
// input. scripts/build-demo-site.mjs calls it directly and writes the result
|
|
@@ -125,19 +124,17 @@ export function facingDegreesFor(plan, previousDegrees) {
|
|
|
125
124
|
}
|
|
126
125
|
|
|
127
126
|
/**
|
|
128
|
-
* The updated corpse set for one redraw
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* CORPSE_LINGER_TURNS's own callers elsewhere in this module) passes it
|
|
140
|
-
* explicitly anyway.
|
|
127
|
+
* The updated corpse set for one redraw — visual-only, entirely client-side;
|
|
128
|
+
* the actual starve/eat removal already happened in the engine. Every id
|
|
129
|
+
* present in `prevAgents` but absent from `agents` died THIS tick — eaten or
|
|
130
|
+
* starved are the only two ways an agent ever leaves the engine's own returned
|
|
131
|
+
* roster — and is added at its last-known cell and class; every corpse already
|
|
132
|
+
* older than `lingerTurns` past its own death turn is dropped first, so the set
|
|
133
|
+
* never grows without bound. Returns a plain `{ [id]: { cls, cell, diedAtTurn }
|
|
134
|
+
* }` map. Pure. `lingerTurns` defaults to a literal 4 (not the module-level
|
|
135
|
+
* CORPSE_LINGER_TURNS constant) so this function stays fully `.toString()`-splice
|
|
136
|
+
* safe — every real caller (both the inlined page and CORPSE_LINGER_TURNS's own
|
|
137
|
+
* callers elsewhere in this module) passes it explicitly anyway.
|
|
141
138
|
*/
|
|
142
139
|
export function nextCorpses(prevCorpses, prevAgents, agents, turn, lingerTurns = 4) {
|
|
143
140
|
const out = {};
|
|
@@ -166,9 +163,9 @@ export function nextCorpses(prevCorpses, prevAgents, agents, turn, lingerTurns =
|
|
|
166
163
|
* back to the flat SPRITE_REGISTRY, unchanged from before this module
|
|
167
164
|
* existed).
|
|
168
165
|
* `?preview=1` on the page's own URL switches it into the small, auto-
|
|
169
|
-
* playing, non-interactive mode the home page's hero iframe embeds
|
|
170
|
-
*
|
|
171
|
-
*
|
|
166
|
+
* playing, non-interactive mode the home page's hero iframe embeds — one file
|
|
167
|
+
* serves both the hero and the "open full-screen" link, matching how
|
|
168
|
+
* ledger.html/plan.html are each one file embedded two ways.
|
|
172
169
|
* `engineBundleJs` (the built spider-fly-browser bundle's own text) inlines
|
|
173
170
|
* the engine into the page instead of the sibling `<script src>`, for the
|
|
174
171
|
* CLI's standalone export — one downloadable file that runs from file://
|
|
@@ -344,11 +341,11 @@ ${THEME_TOKENS_CSS}
|
|
|
344
341
|
.hud-id.spider { color: var(--taught); } .hud-id.fly { color: var(--fly); } .hud-id.egg { color: var(--muted); }
|
|
345
342
|
.hud-goal { font-size: .85rem; }
|
|
346
343
|
.hud-plan, .hud-belief { font-family: ${MONO_STACK}; font-size: .66rem; color: var(--muted); margin-top: .25rem; line-height: 1.4; padding-left: .5rem; border-left: 2px solid var(--chrome-accent); }
|
|
347
|
-
/* the click-expand facts panel
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
344
|
+
/* the click-expand facts panel: beside the clicked spider/fly's own row,
|
|
345
|
+
never a separate popover or a second panel elsewhere on the page — the same
|
|
346
|
+
believedCellOf/beliefSnapshotFor read path spider-fly-turn.mjs already
|
|
347
|
+
computes every tick for planning, rendered here as full sentences instead of
|
|
348
|
+
the compact believes:-line above. */
|
|
352
349
|
.hud-detail { flex: 1 1 auto; min-width: 0; font-family: ${MONO_STACK}; font-size: .64rem; line-height: 1.5; color: var(--chrome-well-ink); background: var(--chrome-well); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-inset); border-radius: 2px; padding: .35rem .5rem; }
|
|
353
350
|
.hud-detail-title { text-transform: uppercase; letter-spacing: .06em; opacity: .75; margin-bottom: .2rem; }
|
|
354
351
|
/* A stat-readout track: an inset "LCD" well, filled with a segmented pip
|
|
@@ -378,11 +375,11 @@ ${THEME_TOKENS_CSS}
|
|
|
378
375
|
.pill:hover:not(:disabled) { border-color: var(--chrome-accent); }
|
|
379
376
|
.pill:disabled { opacity: .45; cursor: default; }
|
|
380
377
|
.pill[data-role="addr"].active { border-color: var(--taught); color: var(--taught); }
|
|
381
|
-
/* The dynamic deception-pill rail
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
378
|
+
/* The dynamic deception-pill rail: a true/false tag shown ONLY here, via
|
|
379
|
+
border style/color and a small human-facing glyph — the submitted sentence
|
|
380
|
+
itself (data-sentence, filled into #chatq on click) never carries the tag,
|
|
381
|
+
so a clicked pill is indistinguishable from a hand-typed claim once it's in
|
|
382
|
+
the input. */
|
|
386
383
|
.dynpills { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .5rem; padding-top: .5rem; border-top: 1px solid var(--chrome-edge-lo); }
|
|
387
384
|
.dynpills:empty { display: none; padding-top: 0; border-top: none; }
|
|
388
385
|
.pill[data-role="dyn-addr"][data-active="1"] { border-color: var(--taught); color: var(--taught); }
|
|
@@ -745,13 +742,13 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
745
742
|
lastAgents = agents;
|
|
746
743
|
}
|
|
747
744
|
|
|
748
|
-
// ---- corpses
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
//
|
|
745
|
+
// ---- corpses: visual-only — the actual starve/eat removal already happened
|
|
746
|
+
// in the engine before this redraw ever sees "agents". A corpse sinks to the
|
|
747
|
+
// bottom row of the SAME board column it died in ("drops to the bottom" —
|
|
748
|
+
// spider-fly-world.mjs's own x/y convention, y = GRID_SIZE is the bottom row)
|
|
749
|
+
// and fades out once CORPSE_LINGER_TURNS passes, drawn in the same sprite
|
|
750
|
+
// layer as every live sprite, just grayscaled and non-interactive (see the
|
|
751
|
+
// .sprite.corpse CSS rule).
|
|
755
752
|
function renderCorpses() {
|
|
756
753
|
for (const id of Object.keys(corpseEls)) {
|
|
757
754
|
if (!corpses[id]) { corpseEls[id].remove(); delete corpseEls[id]; }
|
|
@@ -793,12 +790,12 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
793
790
|
return '<div class="hud-plan">plan: ' + text + ".</div>";
|
|
794
791
|
}
|
|
795
792
|
|
|
796
|
-
// The agent's own current world-knowledge-graph snapshot
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
//
|
|
793
|
+
// The agent's own current world-knowledge-graph snapshot — every OTHER live
|
|
794
|
+
// individual it believes it knows the position of, or "unseen" when it has no
|
|
795
|
+
// belief at all. Deliberately never ground truth: this is what the agent
|
|
796
|
+
// would actually ACT on, which a false pill or a told fact can make visibly
|
|
797
|
+
// wrong compared to where that individual really is — the gap IS the
|
|
798
|
+
// demonstration.
|
|
802
799
|
function beliefLineHtml(belief) {
|
|
803
800
|
const entries = Object.entries(belief || {});
|
|
804
801
|
if (!entries.length) return "";
|
|
@@ -1023,12 +1020,12 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1023
1020
|
chatqEl.addEventListener("input", refreshPills);
|
|
1024
1021
|
refreshPills();
|
|
1025
1022
|
|
|
1026
|
-
// ---- deception pills
|
|
1027
|
-
//
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1023
|
+
// ---- deception pills: a SEPARATE dynamic rail, alongside (never replacing)
|
|
1024
|
+
// the static one above. tmct.page.pillsForSpiderFly is the exact same pure
|
|
1025
|
+
// function spider-fly-turn.mjs exports — this page never reimplements the
|
|
1026
|
+
// true/false claim logic, only renders its output and fills #chatq on click,
|
|
1027
|
+
// same click-to-fill discipline as every other pill on this page (never
|
|
1028
|
+
// auto-submits).
|
|
1032
1029
|
function renderDynamicPills() {
|
|
1033
1030
|
if (!session || !Object.keys(lastAgents).length) { dynamicPillsEl.innerHTML = ""; return; }
|
|
1034
1031
|
const result = tmct.page.pillsForSpiderFly(lastAgents, selectedAddresseeId, {});
|