@polycode-projects/the-mechanical-code-talker 5.0.5 → 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.
@@ -266,7 +266,7 @@ export function createP2pRoom({
266
266
  // is a change worth broadcasting even though its provenance never moved.
267
267
  let cachedRetractions = [];
268
268
  const seenRetractionValueById = new Map();
269
- const retractionDiffValue = (fact) => `${fact.provenance}${fact.object}`;
269
+ const retractionDiffValue = (fact) => `${fact.provenance}\u0000${fact.object}`;
270
270
 
271
271
  // Store-touching work runs one job at a time, in arrival order. Every path
272
272
  // that reads or writes memoryDir/seenProvenanceById/cachedRows crosses at
@@ -18,7 +18,7 @@
18
18
  // inline script degrades honestly when that sibling script is absent or
19
19
  // fails to load — the live controls disable themselves rather than pretend
20
20
  // to work.
21
- import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, countLabel } from "./viz-theme.mjs";
21
+ import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, countLabel, demoEyebrowHtml, EYEBROW_LINKS_CSS } from "./viz-theme.mjs";
22
22
  import { planToPddl } from "./plan-pddl.mjs";
23
23
 
24
24
  const BOARD_W = 640;
@@ -334,6 +334,7 @@ button { font: inherit; }
334
334
  /* ---- head strip: the nameplate ---- */
335
335
  .headStrip { display: flex; justify-content: space-between; align-items: flex-end; gap: .6rem; flex-wrap: wrap; padding: .9rem 1.1rem .75rem; background: linear-gradient(180deg, var(--strip2), var(--strip)); border-bottom: 1px solid var(--line); border-radius: 9px 9px 0 0; }
336
336
  .eyebrow { font-family: ${MONO_STACK}; font-size: .64rem; letter-spacing: .16em; text-transform: uppercase; color: var(--muted); margin: 0 0 .35rem; }
337
+ ${EYEBROW_LINKS_CSS}
337
338
  h1 { font-family: ${SERIF_STACK}; font-size: 1.2rem; font-weight: 600; margin: 0; color: var(--ink); }
338
339
  .chip { font-family: ${MONO_STACK}; font-size: .64rem; letter-spacing: .02em; color: var(--muted); border: 1px solid var(--line); border-radius: 3px; padding: .22rem .6rem; background: var(--well); text-align: right; }
339
340
 
@@ -432,7 +433,7 @@ h1 { font-family: ${SERIF_STACK}; font-size: 1.2rem; font-weight: 600; margin: 0
432
433
  <div class="rack">
433
434
  <div class="headStrip">
434
435
  <div>
435
- <div class="eyebrow">tmct &middot; plan</div>
436
+ <div class="eyebrow">${demoEyebrowHtml("plan", "plan")}</div>
436
437
  <h1 id="pageTitle">${escapeHtml(pageTitle)}</h1>
437
438
  </div>
438
439
  <span class="chip">blocks archetype · ${pageData.layouts.length} snapshots · plan: findActionPath</span>
@@ -14,7 +14,7 @@
14
14
  // which is board-size agnostic and models vision as plain Chebyshev distance
15
15
  // with no line of sight — a building blocks movement, never sight.
16
16
  //
17
- // Three mechanics are OPT-IN, off unless `config` asks for them by name, so a
17
+ // Four mechanics are OPT-IN, off unless `config` asks for them by name, so a
18
18
  // cast that does not want them sees the same board it always did:
19
19
  //
20
20
  // - `carryPreyToWeb` splits catching from eating. A predator that shares a
@@ -27,6 +27,11 @@
27
27
  // - `layEggs` adds the reproduction stage: an egg (mgx:laid-at-turn) laid in
28
28
  // a web once mass crosses a threshold, hatching (mgx:hatched-into) into
29
29
  // hatchlings that split the egg's mass.
30
+ // - `blendPreyDecision` replaces the prey's strict evade-then-forage order
31
+ // with one weighted score over both distances (`preyThreatWeight`), so a
32
+ // prey can take a crumb that costs it nothing. Off, prey abandon food the
33
+ // moment anything is in view. scripts/compare-prey-decision.mjs measures
34
+ // the two against each other.
30
35
 
31
36
  import {
32
37
  DEFAULT_FACING, TOWN_SQUARE_LAYOUTS,
@@ -396,6 +401,29 @@ export function greedyToward(fromCell, towardCell, applyActions) {
396
401
  );
397
402
  }
398
403
 
404
+ /** One-ply greedy over BOTH distances at once, rather than one rung after the
405
+ * other: the reachable cell (or staying put) that maximizes
406
+ *
407
+ * weight * distance-from-`awayFrom` - (1 - weight) * distance-to-`towardCell`
408
+ *
409
+ * At weight 1 this is greedyAway and at weight 0 it is greedyToward. In
410
+ * between it lets an agent take a step toward food that costs it little or no
411
+ * distance from the thing hunting it, which strict priority rungs cannot do.
412
+ *
413
+ * Falls back to whichever single term it still has when the other cell is
414
+ * null, so a caller never has to check first. */
415
+ export function greedyBlend(fromCell, awayFrom, towardCell, applyActions, weight) {
416
+ if (!awayFrom) return greedyToward(fromCell, towardCell, applyActions);
417
+ if (!towardCell) return greedyAway(fromCell, awayFrom, applyActions);
418
+ const w = Number.isFinite(Number(weight)) ? Number(weight) : DEFAULT_GAME_CONFIG.mudiii.preyThreatWeight;
419
+ return bestOneStepBy(
420
+ fromCell, applyActions,
421
+ (cell) => w * chebyshevDistance(cell.x, cell.y, awayFrom.x, awayFrom.y)
422
+ - (1 - w) * chebyshevDistance(cell.x, cell.y, towardCell.x, towardCell.y),
423
+ (score, bestScore) => score > bestScore,
424
+ );
425
+ }
426
+
399
427
  /** A seeded, uniform pick among staying put or any one-ply reachable cell.
400
428
  * Deterministic and replayable, and it looks random to somebody watching.
401
429
  * Both roles' last rung: a motionless predator reads as a broken page. */
@@ -1053,6 +1081,10 @@ export async function runTownSquareTick(memoryDir, {
1053
1081
 
1054
1082
  const carriesPrey = config.carryPreyToWeb === true;
1055
1083
  const buildsWebs = config.buildWebs === true;
1084
+ const blendsPreyDecision = config.blendPreyDecision === true;
1085
+ const preyThreatWeight = Number.isFinite(Number(config.preyThreatWeight))
1086
+ ? Number(config.preyThreatWeight)
1087
+ : DEFAULT_GAME_CONFIG.mudiii.preyThreatWeight;
1056
1088
  const boardNoun = lay.boardNoun ?? "square";
1057
1089
  const webbedAt = (c) => hasActiveWebAt(lay, state, k, c.x, c.y, config.webDurationTurns);
1058
1090
  // Widened in place as predators spin webs this tick, for the renderer's own
@@ -1152,18 +1184,41 @@ export async function runTownSquareTick(memoryDir, {
1152
1184
  goal = goalLine("trapped");
1153
1185
  mood = "scared";
1154
1186
  } else if (threat) {
1155
- rung = role === "predator" ? "avoid" : "evade";
1156
- // A fleeing prey that already knows where food is should flee toward
1157
- // it, not away from it — among cells that are equally safe, break the
1158
- // tie toward the nearest believed crumb. Prey-only: the predator's own
1159
- // avoid rung passes nothing, so its ties still resolve the old way.
1160
1187
  const towardFood = role === "prey"
1161
1188
  ? nearestBelievedTarget(agentId, fromCell, [...foodIds].sort(), state, foodBeliefOpts)
1162
1189
  : null;
1163
- nextCell = greedyAway(fromCell, threat.cell, applyActions, { towardCell: towardFood?.cell ?? null });
1164
- plan = stepPlan(fromCell, nextCell);
1165
- goal = goalLine(rung, { subject: threat.subject, cell: cellId(threat.cell.x, threat.cell.y) });
1166
- mood = "scared";
1190
+ if (blendsPreyDecision && role === "prey" && towardFood) {
1191
+ // The one case the two rungs disagree about: this prey believes a
1192
+ // predator AND food, so a strict order has to pick between them and a
1193
+ // score does not.
1194
+ nextCell = greedyBlend(fromCell, threat.cell, towardFood.cell, applyActions, preyThreatWeight);
1195
+ const closedOnFood = chebyshevDistance(fromCell.x, fromCell.y, towardFood.cell.x, towardFood.cell.y)
1196
+ > chebyshevDistance(nextCell.x, nextCell.y, towardFood.cell.x, towardFood.cell.y);
1197
+ // The score is one number, but the step it buys is still one of two
1198
+ // legible things: this move closed on the crumb, or it did not. The
1199
+ // rung and the goal line say which, so no surface has to render
1200
+ // "mostly evading, somewhat hungry".
1201
+ rung = closedOnFood ? "forage" : "evade";
1202
+ plan = stepPlan(fromCell, nextCell);
1203
+ goal = closedOnFood
1204
+ ? goalLine("forage", {
1205
+ subject: towardFood.subject,
1206
+ cell: cellId(towardFood.cell.x, towardFood.cell.y),
1207
+ arrived: nextCell.x === towardFood.cell.x && nextCell.y === towardFood.cell.y,
1208
+ })
1209
+ : goalLine("evade", { subject: threat.subject, cell: cellId(threat.cell.x, threat.cell.y) });
1210
+ mood = closedOnFood ? "calm" : "scared";
1211
+ } else {
1212
+ rung = role === "predator" ? "avoid" : "evade";
1213
+ // A fleeing prey that already knows where food is should flee toward
1214
+ // it, not away from it — among cells that are equally safe, break the
1215
+ // tie toward the nearest believed crumb. Prey-only: the predator's own
1216
+ // avoid rung passes nothing, so its ties still resolve the old way.
1217
+ nextCell = greedyAway(fromCell, threat.cell, applyActions, { towardCell: towardFood?.cell ?? null });
1218
+ plan = stepPlan(fromCell, nextCell);
1219
+ goal = goalLine(rung, { subject: threat.subject, cell: cellId(threat.cell.x, threat.cell.y) });
1220
+ mood = "scared";
1221
+ }
1167
1222
  } else {
1168
1223
  const quarry = role === "predator"
1169
1224
  ? nearestBelievedTarget(agentId, fromCell, prey, state, beliefOpts)
@@ -19,7 +19,7 @@
19
19
  // renderResearchHtml() is pure: no I/O, deterministic output for identical
20
20
  // input. scripts/build-demo-site.mjs calls it directly and writes the result to
21
21
  // public/research.html, after research-browser.bundle.js already exists.
22
- import { THEME_TOKENS_CSS, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
22
+ import { THEME_TOKENS_CSS, MONO_STACK, escapeHtml, demoEyebrowHtml, EYEBROW_LINKS_CSS } from "./viz-theme.mjs";
23
23
  import { fetchWithProgress, loadProgressLine, factTripleParts } from "./memory-panel-viz.mjs";
24
24
  import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
25
25
  import { loadWinkVendor } from "./viz-boot.mjs";
@@ -108,7 +108,8 @@ ${DASH_DARK_CHROME_CSS}
108
108
  header.topbar { display: flex; align-items: flex-start; justify-content: space-between; gap: 1.4rem; flex-wrap: wrap; padding: .6rem 0 1rem; border-bottom: 1px solid var(--line); margin-bottom: 1.3rem; }
109
109
  .brand { display: flex; flex-direction: column; gap: .3rem; max-width: 640px; }
110
110
  .eyebrow { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
111
- .subtitle { font-size: .92rem; color: var(--ink); opacity: .82; max-width: 58ch; }
111
+ ${EYEBROW_LINKS_CSS}
112
+ .subtitle { margin: 0; font-size: .92rem; font-weight: 400; color: var(--ink); opacity: .82; max-width: 58ch; }
112
113
  .statuspanel { display: flex; flex-wrap: wrap; gap: .5rem 1.1rem; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: .5rem .9rem; }
113
114
  .statuspanel .stat { display: flex; flex-direction: column; gap: .14rem; min-width: 7rem; }
114
115
  .statuspanel .stat-label { font-family: ${MONO_STACK}; font-size: .6rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
@@ -240,8 +241,8 @@ ${DASH_DARK_CHROME_CSS}
240
241
  <div class="wrap">
241
242
  <header class="topbar">
242
243
  <div class="brand">
243
- <span class="eyebrow">the-mechanical-code-talker &middot; research</span>
244
- <span class="subtitle">Grow one graph three ways. Watch what it learns, then ask a question scoped to the sources you trust.</span>
244
+ <span class="eyebrow">${demoEyebrowHtml("research", "research")}</span>
245
+ <h1 class="subtitle">Grow one graph three ways. Watch what it learns, then ask a question scoped to the sources you trust.</h1>
245
246
  </div>
246
247
  <div class="statuspanel" id="statusPanel" aria-live="polite">
247
248
  <div class="stat stat-facts"><span class="stat-label">facts in the graph</span><span class="stat-value" id="statFacts">&mdash;</span></div>
@@ -32,7 +32,7 @@
32
32
  // bundle's own real ES exports instead, since (unlike ledger-viz, which
33
33
  // reuses a FIXED shared bundle it can't extend for one page's own needs) this
34
34
  // page ships its own dedicated bundle and can just export what it needs.
35
- import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, meterBarHtml } from "./viz-theme.mjs";
35
+ import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, meterBarHtml, demoEyebrowHtml, EYEBROW_LINKS_CSS } from "./viz-theme.mjs";
36
36
  import { createTicker } from "./viz-ticker.mjs";
37
37
  import { loadWinkVendor } from "./viz-boot.mjs";
38
38
  import { GRID_SIZE, WEB_HOME, WEB_RADIUS, isInWebBlock, cellId, agentKindOf } from "../domain/spider-fly-world.mjs";
@@ -250,6 +250,7 @@ ${THEME_TOKENS_CSS}
250
250
  .mono { font-family: ${MONO_STACK}; }
251
251
  main { max-width: 1120px; margin: 0 auto; padding: 1.4rem 1.2rem 2.2rem; }
252
252
  .eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--chrome-accent); }
253
+ ${EYEBROW_LINKS_CSS}
253
254
  h1 { font-size: 1.4rem; margin: .3rem 0 .9rem; text-wrap: balance; }
254
255
  button { font: inherit; color: inherit; background: none; cursor: pointer; }
255
256
  button:focus-visible, input:focus-visible, .sprite:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
@@ -443,7 +444,7 @@ ${THEME_TOKENS_CSS}
443
444
  <main>
444
445
  <div class="stage page-head">
445
446
  <div class="head-inner">
446
- <div class="eyebrow">tmct &middot; spider and fly</div>
447
+ <div class="eyebrow">${demoEyebrowHtml("spider-fly", "spider and fly")}</div>
447
448
  <h1>Multiple competing planning agents</h1>
448
449
  </div>
449
450
  <div></div>
@@ -51,7 +51,7 @@ import { spriteFactRows } from "../domain/sprite-facts.mjs";
51
51
  import { SEED_TAXONOMY } from "../domain/spider-fly-world.mjs";
52
52
  import { loadSlice, loadMap, toFacts, WORDNET_DIR } from "../adapters/corpus/conceptnet.mjs";
53
53
  import { join } from "node:path";
54
- import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText } from "./viz-theme.mjs";
54
+ import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, demoEyebrowHtml, EYEBROW_LINKS_CSS } from "./viz-theme.mjs";
55
55
 
56
56
  const DEFAULT_TITLE = "tmct — the sprite library";
57
57
  const MAX_CHAIN_DISPLAY = 6;
@@ -905,6 +905,7 @@ ${THEME_TOKENS_CSS}
905
905
  .appbar { display: flex; align-items: flex-end; gap: 1rem; margin: 0 -1.2rem; padding: .6rem 1.2rem 0; background: var(--ai-bar); }
906
906
  .appbar h1 { font-family: ${MONO_STACK}; font-size: .84rem; font-weight: 600; letter-spacing: .02em; margin: 0; padding: .32rem .8rem .38rem; background: var(--ai-panel); color: var(--ink); border-radius: 4px 4px 0 0; }
907
907
  .appbar .doc-sub { font-family: ${MONO_STACK}; font-size: .66rem; letter-spacing: .07em; text-transform: uppercase; color: color-mix(in srgb, var(--ai-bar-ink) 65%, transparent); padding-bottom: .5rem; }
908
+ ${EYEBROW_LINKS_CSS}
908
909
  .topbar { position: sticky; top: 0; z-index: 2; display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .9rem; background: var(--ai-panel); border-bottom: 1px solid var(--ai-edge); margin: 0 -1.2rem 1.4rem; padding: .5rem 1.2rem; }
909
910
  .jump { font-family: ${MONO_STACK}; font-size: .7rem; padding: .2rem .6rem; border: 1px solid var(--ai-edge); border-radius: 3px; background: transparent; color: var(--ink); text-decoration: none; }
910
911
  .jump:hover { border-color: var(--corpus); color: var(--corpus); }
@@ -982,7 +983,7 @@ ${dockCss}</style>
982
983
  <main>
983
984
  <header class="appbar">
984
985
  <h1>Sprites</h1>
985
- <span class="doc-sub">tmct &middot; the sprite library</span>
986
+ <span class="doc-sub">${demoEyebrowHtml("sprites", "the sprite library")}</span>
986
987
  </header>
987
988
  ${composerHtml}
988
989
  ${dockHtml}
@@ -32,6 +32,26 @@ export function embedScriptText(js) {
32
32
  return String(js ?? "").replaceAll("</script", "<\\/script");
33
33
  }
34
34
 
35
+ /** The three-link nav every demo page's header opens with: the site's own
36
+ * name (home), the demo's own name (its own page) and "about" (its about
37
+ * page). `page` is the demo's key in scripts/site-pages.mjs's DEMO_PAGES,
38
+ * which is also its filename stem (e.g. "mud" -> mud.html); the about-page
39
+ * suffix below mirrors that same file's `aboutPageOf`, restated rather than
40
+ * imported since scripts/ sits outside src/ and never ships with the
41
+ * published package. Returns the inner markup only — the caller supplies
42
+ * the wrapping element (div/h1/span) and keeps whatever class and
43
+ * neighbouring markup its own header already carries. Style hook:
44
+ * EYEBROW_LINKS_CSS. */
45
+ export function demoEyebrowHtml(page, label) {
46
+ return `<span class="eyebrow-links"><a href="./index.html">tmct</a> &middot; <a href="./${page}.html">${escapeHtml(label)}</a> &middot; <a href="./${page}-about.html">about</a></span>`;
47
+ }
48
+
49
+ /** Styling every page that calls demoEyebrowHtml splices into its own inline
50
+ * <style>: the links read in the eyebrow's own colour (each page sets that
51
+ * colour itself), underlining only on hover/focus. */
52
+ export const EYEBROW_LINKS_CSS = `.eyebrow-links a { color: inherit; text-decoration: none; }
53
+ .eyebrow-links a:hover, .eyebrow-links a:focus-visible { text-decoration: underline; }`;
54
+
35
55
  /** A world name read as a name in a scenario dropdown: "mud-garden" ->
36
56
  * "mud garden". The world's own hyphenated id is the only thing every caller
37
57
  * is guaranteed to have, so a scenario that wants a hand-written label passes