@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.
- package/README.md +72 -19
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +53 -0
- package/src/domain/ask.mjs +2 -4
- package/src/domain/codegraph.mjs +23 -9
- package/src/domain/game-config.mjs +18 -0
- package/src/domain/memory/causal-stability.mjs +82 -0
- package/src/domain/town-square-world.mjs +36 -0
- package/src/services/adventure-viz.mjs +5 -2
- package/src/services/chat-page-viz.mjs +5 -2
- package/src/services/chat.mjs +104 -16
- package/src/services/code-explorer-viz.mjs +3 -2
- package/src/services/ingest-viz.mjs +5 -4
- package/src/services/ledger-viz.mjs +5 -2
- package/src/services/mud-viz.mjs +4 -3
- package/src/services/mudiii-scene.mjs +136 -12
- package/src/services/mudiii-turn.mjs +250 -1
- package/src/services/mudiii-viz.mjs +599 -108
- package/src/services/p2p-room.mjs +1 -1
- package/src/services/plan-viz.mjs +3 -2
- package/src/services/predator-prey.mjs +133 -17
- package/src/services/research-viz.mjs +5 -4
- package/src/services/spider-fly-viz.mjs +3 -2
- package/src/services/sprite-catalog-viz.mjs +3 -2
- package/src/services/viz-theme.mjs +20 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +91 -91
- package/src/surfaces/web/mudiii-browser-entry.mjs +154 -34
package/src/services/chat.mjs
CHANGED
|
@@ -1413,6 +1413,35 @@ export function isConversational(query) {
|
|
|
1413
1413
|
return expandContractions(q).split(/\s+/).filter(Boolean).length <= 3 && !codeish;
|
|
1414
1414
|
}
|
|
1415
1415
|
|
|
1416
|
+
/** The words that, in the slot of a short "what is X" / "who is X", name the
|
|
1417
|
+
* SESSION rather than a term to look up ("what is this", "what is up").
|
|
1418
|
+
* Closed and hand-curated, the same discipline every small-talk set in this
|
|
1419
|
+
* file uses: a word listed here keeps today's orientation card, and anything
|
|
1420
|
+
* else is a question about a term. */
|
|
1421
|
+
const SESSION_REFERENT_TERMS = new Set([
|
|
1422
|
+
"this", "that", "it", "they", "them", "you", "u", "yours", "tmct",
|
|
1423
|
+
"up", "new", "next", "now", "here", "there", "left", "wrong", "more", "else",
|
|
1424
|
+
"all", "everything", "anything", "something", "nothing",
|
|
1425
|
+
"going", "happening", "possible", "available", "supported", "included",
|
|
1426
|
+
]);
|
|
1427
|
+
|
|
1428
|
+
/** The term a short "what is X" / "who is X" asks about, or null when the line
|
|
1429
|
+
* isn't that shape or names the session itself.
|
|
1430
|
+
*
|
|
1431
|
+
* isConversational's catch-all counts words, so "what is grelb" (three) took
|
|
1432
|
+
* the orientation card while "what is a grelb" (four, one article apart)
|
|
1433
|
+
* refused — the same question answered two different ways, and the shorter
|
|
1434
|
+
* one answered with a paragraph about tmct that reads as if the term had been
|
|
1435
|
+
* looked up. A term question that resolves to nothing belongs on the miss
|
|
1436
|
+
* wall; only the closed set above is really about the product. */
|
|
1437
|
+
function shortTermQuestionTerm(query) {
|
|
1438
|
+
const m = String(query).trim().replace(/[?.!]+\s*$/, "")
|
|
1439
|
+
.match(/^(?:what|who)\s+(?:is|are|was|were)\s+([a-z][\w'-]*)$/i);
|
|
1440
|
+
if (!m) return null;
|
|
1441
|
+
const term = m[1].toLowerCase();
|
|
1442
|
+
return SESSION_REFERENT_TERMS.has(term) ? null : term;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1416
1445
|
/** The tmct tools dispatchTool can back (the set a tool-emitting caller may use).
|
|
1417
1446
|
* A declared tool outside this set is never emitted — the request falls through
|
|
1418
1447
|
* to a text answer. The COMMANDS map names the richer graph tools; TOOLS names
|
|
@@ -2959,6 +2988,23 @@ function singularizeSurface(word) {
|
|
|
2959
2988
|
|
|
2960
2989
|
export { singularizeSurface };
|
|
2961
2990
|
|
|
2991
|
+
/** Does the complement of this payload wear its own article ("all foxes are A
|
|
2992
|
+
* SPECIES")? An article marks the complement singular already, so the plural
|
|
2993
|
+
* fold below must leave it alone. */
|
|
2994
|
+
const objectCarriesArticle = (payload) => /\s(?:is|are)\s+(?:an?\s+)/i.test(String(payload || ""));
|
|
2995
|
+
|
|
2996
|
+
/** The complement as it should be STORED. The subject of a plural membership
|
|
2997
|
+
* sentence has always folded ("all foxes are mammals" stores "fox"); the
|
|
2998
|
+
* object had not, so the pair landed as fox ⊑ mammals and the next turn's
|
|
2999
|
+
* "is a fox a mammal" looked up a class spelled a different way and missed
|
|
3000
|
+
* the fact it had just been told. Gated exactly as the subject fold is: only
|
|
3001
|
+
* a genuinely plural "are" phrasing, and only where no article already marks
|
|
3002
|
+
* the complement singular. */
|
|
3003
|
+
function storedObjectTerm(objectRaw, { verb, payload, lex, lookupNoun }) {
|
|
3004
|
+
if (!/^are$/i.test(verb) || objectCarriesArticle(payload)) return objectRaw;
|
|
3005
|
+
return lookupNoun(lex, objectRaw)?.lemma || singularizeSurface(objectRaw);
|
|
3006
|
+
}
|
|
3007
|
+
|
|
2962
3008
|
/** "(every|each|all|a|an )?X is/are (a|an )?Y" — the shape the unknown-subject
|
|
2963
3009
|
* fallback recognizes (group 2 = X, group 4 = Y); group 1 (when present)
|
|
2964
3010
|
* names the determiner, so the caller can tell a genuine "every" universal
|
|
@@ -3103,19 +3149,25 @@ async function ungroundedPairHint(payload, lexicon, memoryDir, cache = null, gra
|
|
|
3103
3149
|
if (!memoryDir) return "";
|
|
3104
3150
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
3105
3151
|
if (!m) return "";
|
|
3106
|
-
const [, , subjectRaw, , objectRaw] = m;
|
|
3107
|
-
const { loadLexicon } = await import("../domain/grammar/lexicon.mjs");
|
|
3152
|
+
const [, , subjectRaw, verb, objectRaw] = m;
|
|
3153
|
+
const { loadLexicon, lookupNoun } = await import("../domain/grammar/lexicon.mjs");
|
|
3108
3154
|
const lex = lexicon || loadLexicon();
|
|
3109
3155
|
if (await isGroundedTerm(subjectRaw, lex, memoryDir, cache, graph)) return "";
|
|
3110
3156
|
if (await isGroundedTerm(objectRaw, lex, memoryDir, cache, graph)) return "";
|
|
3157
|
+
// The sentences suggested here have to be the ones that actually store, so
|
|
3158
|
+
// both sides fold to the singular a plural surface named ("all zorps are
|
|
3159
|
+
// florbs" → "every zorp is a thing"). Following the plural verbatim teaches
|
|
3160
|
+
// a second class under a spelling nothing else uses.
|
|
3161
|
+
const subject = /^are$/i.test(verb) ? singularOf(subjectRaw, lex, lookupNoun) : subjectRaw;
|
|
3162
|
+
const object = storedObjectTerm(objectRaw, { verb, payload, lex, lookupNoun });
|
|
3111
3163
|
// Chaining the second term UNDER the first's now-grounded proper name
|
|
3112
3164
|
// ("every man is a john") is technically accepted by the grammar (once
|
|
3113
3165
|
// "john" is grounded, ANY term can be taught as a kind of it), but reads as
|
|
3114
3166
|
// nonsense to a human, since a proper name is never a category. Ground both
|
|
3115
3167
|
// sides independently instead — two clear, parallel, semantically sane
|
|
3116
3168
|
// suggestions, not a confusing chain through an arbitrary first term.
|
|
3117
|
-
return ` I don't know "${
|
|
3118
|
-
+ `"every ${
|
|
3169
|
+
return ` I don't know "${subject}" or "${object}" yet. Try grounding each one first, e.g. `
|
|
3170
|
+
+ `"every ${subject} is a thing" and "every ${object} is a thing", then re-teach the`
|
|
3119
3171
|
+ ` original fact.`;
|
|
3120
3172
|
}
|
|
3121
3173
|
|
|
@@ -3192,7 +3244,12 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon,
|
|
|
3192
3244
|
if (lookupNoun(lex, objectRaw) || GENERIC_ANCHOR_NOUNS.has(String(objectRaw).toLowerCase())
|
|
3193
3245
|
|| (await isGroundedByFact(objectRaw, memoryDir, cache))) {
|
|
3194
3246
|
return teachFact(memoryDir, sessionId, {
|
|
3195
|
-
subject,
|
|
3247
|
+
subject,
|
|
3248
|
+
predicate: SUBCLASS_PREDICATE,
|
|
3249
|
+
object: storedObjectTerm(objectRaw, { verb, payload, lex, lookupNoun }),
|
|
3250
|
+
quantifier,
|
|
3251
|
+
observedAt,
|
|
3252
|
+
dateText,
|
|
3196
3253
|
});
|
|
3197
3254
|
}
|
|
3198
3255
|
if (lookupAdjective(lex, objectRaw)) {
|
|
@@ -3321,7 +3378,12 @@ async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, c
|
|
|
3321
3378
|
? (lookupNoun(lex, subjectRaw)?.lemma || singularizeSurface(subjectRaw))
|
|
3322
3379
|
: subjectRaw;
|
|
3323
3380
|
return teachFact(memoryDir, sessionId, {
|
|
3324
|
-
subject,
|
|
3381
|
+
subject,
|
|
3382
|
+
predicate: SUBCLASS_PREDICATE,
|
|
3383
|
+
object: storedObjectTerm(objectRaw, { verb, payload, lex, lookupNoun }),
|
|
3384
|
+
quantifier,
|
|
3385
|
+
observedAt,
|
|
3386
|
+
dateText,
|
|
3325
3387
|
});
|
|
3326
3388
|
}
|
|
3327
3389
|
|
|
@@ -4160,9 +4222,20 @@ function matchBareCanTeach(text) {
|
|
|
4160
4222
|
function teachSuggestion(payload) {
|
|
4161
4223
|
const m = String(payload).match(/^(?:every |each |all |a |an )?([\w-]+) (is|are) (a |an )?([\w-]+)[.!?]*$/i);
|
|
4162
4224
|
if (!m) return null;
|
|
4163
|
-
const
|
|
4164
|
-
const
|
|
4165
|
-
|
|
4225
|
+
const plural = /^are$/i.test(m[2]);
|
|
4226
|
+
const subject = (plural ? singularizeSurface(m[1]) : m[1]).toLowerCase();
|
|
4227
|
+
let object = m[4].toLowerCase();
|
|
4228
|
+
let articled = !!m[3];
|
|
4229
|
+
// A plural complement with no article of its own ("all lynxes are mammals")
|
|
4230
|
+
// folds like the subject does, and the fold itself is what proves it was a
|
|
4231
|
+
// plural NOUN rather than a bare adjective — so it earns the article the
|
|
4232
|
+
// singular membership sentence needs. Without this the repair suggested the
|
|
4233
|
+
// very spelling the same sentence had just called unrecognized.
|
|
4234
|
+
if (plural && !articled) {
|
|
4235
|
+
const folded = singularizeSurface(object);
|
|
4236
|
+
if (folded !== object) { object = folded; articled = true; }
|
|
4237
|
+
}
|
|
4238
|
+
if (!articled) return `every ${subject} is ${object}`;
|
|
4166
4239
|
const articleRule = grammarRules().find((r) => r.kind === "article");
|
|
4167
4240
|
const article = articleRule && beginsWithVowelSound(object, articleRule) ? "an" : "a";
|
|
4168
4241
|
return `every ${subject} is ${article} ${object}`;
|
|
@@ -4799,9 +4872,11 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4799
4872
|
// the SHAPE of a retraction sentence — it says nothing about whether
|
|
4800
4873
|
// subject⊑object was ever actually taught, or taught by THIS session.
|
|
4801
4874
|
// retractSubClassOf is asked for real and is the only thing that decides:
|
|
4802
|
-
// - found:false → subject⊑object was never a stored fact,
|
|
4803
|
-
//
|
|
4804
|
-
//
|
|
4875
|
+
// - found:false → subject⊑object was never a stored fact, so there is
|
|
4876
|
+
// nothing to withdraw and the turn says exactly that. It must NOT
|
|
4877
|
+
// fall through to the rest of the cascade: the teach frames below
|
|
4878
|
+
// read the unconsumed sentence from the top, take "forget bertha" as
|
|
4879
|
+
// a two-word subject, and confirm a fact the user asked to destroy.
|
|
4805
4880
|
// - found:true, ownRecord:false → some OTHER source taught it; this
|
|
4806
4881
|
// session has nothing of its own to withdraw.
|
|
4807
4882
|
// - found:true, stillStands:true → this session's own record is gone,
|
|
@@ -4849,7 +4924,10 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4849
4924
|
via: "retract", miss: false,
|
|
4850
4925
|
};
|
|
4851
4926
|
}
|
|
4852
|
-
|
|
4927
|
+
return {
|
|
4928
|
+
text: `"${retractSubject} is a kind of ${retractObject}" isn't stored, so there's nothing to forget.`,
|
|
4929
|
+
via: "retract", miss: true,
|
|
4930
|
+
};
|
|
4853
4931
|
}
|
|
4854
4932
|
}
|
|
4855
4933
|
|
|
@@ -13336,9 +13414,11 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13336
13414
|
dialogueLaneOverride = "game-inform";
|
|
13337
13415
|
note(trace, "lane: (2) MID-GAME NUDGE — an unparsed short turn stayed inside the live game frame instead of the identity card");
|
|
13338
13416
|
note(trace, "goal: keep the running guess-the-number game on track");
|
|
13339
|
-
} else if (isConversationalCandidate) {
|
|
13417
|
+
} else if (isConversationalCandidate && !shortTermQuestionTerm(gateQuery) && !shortTermQuestionTerm(query)) {
|
|
13340
13418
|
// A conversational miss (a greeting, "what can you do", a very short non-code
|
|
13341
13419
|
// line) gets the friendly orientation (module-aware: empty → --repo/tmct init).
|
|
13420
|
+
// A short question naming a TERM is excluded: nothing above resolved it, so
|
|
13421
|
+
// it belongs on the miss wall its four-word twin already reaches.
|
|
13342
13422
|
// This branch carries via:"template" and never reaches the composed-only
|
|
13343
13423
|
// wall-shortening gate below, so it needs its own repeat collapse,
|
|
13344
13424
|
// mirroring WALL_REPEAT_ONELINER.
|
|
@@ -13530,7 +13610,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13530
13610
|
// all, so it would otherwise be confidently WRONG or silently absent.
|
|
13531
13611
|
// Every successfully-recognized teach attempt gets this consistent goal
|
|
13532
13612
|
// line instead.
|
|
13533
|
-
|
|
13613
|
+
// A retraction reaches this lane too, and its goal is the opposite one:
|
|
13614
|
+
// saying "teach/remember a new fact" under a sentence asking for a fact
|
|
13615
|
+
// to go describes the turn backwards.
|
|
13616
|
+
deduced = taught.via === "retract" ? "withdraw a remembered fact" : "teach/remember a new fact";
|
|
13534
13617
|
note(trace, `goal: ${deduced} (revised — the teach lane recognized this shape where the raw structural parse never should have)`);
|
|
13535
13618
|
// A canonical whose verb only matched through the fuzzy edit-distance
|
|
13536
13619
|
// tier ("disk-1 rests on peg-a." read as an ask about "tests") restates
|
|
@@ -13791,7 +13874,12 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13791
13874
|
// does. Both shapes below anchor on the written-out copula.
|
|
13792
13875
|
const offerSrc = expandContractions(String(query).trim());
|
|
13793
13876
|
const knowAboutTerm = offerSrc.match(KNOW_ABOUT_RE)?.[1]?.trim();
|
|
13794
|
-
|
|
13877
|
+
// "who is grelb" asks about a term the same way "what is grelb" does, and
|
|
13878
|
+
// a name is exactly what a person teaches next — without this the who-form
|
|
13879
|
+
// dead-ends on the bare grammar wall while its what-form twin offers the
|
|
13880
|
+
// teach phrasing.
|
|
13881
|
+
const whoTerm = shortTermQuestionTerm(offerSrc) ? offerSrc.match(WHO_IS_BARE_RE)?.[1]?.trim() : null;
|
|
13882
|
+
const offerTerm = knowAboutTerm || metaTermOf(offerSrc, envelope) || whoTerm;
|
|
13795
13883
|
// A term that LEADS with a bindable anaphor ("it used for", from an
|
|
13796
13884
|
// unresolved "what is it used for") is a pronoun that failed to bind,
|
|
13797
13885
|
// not a teachable subject — offering to learn facts about it would echo
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// sidebar/centre grid, so a later panel is one more <section class="panel">,
|
|
20
20
|
// not a re-architecture.
|
|
21
21
|
|
|
22
|
-
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText } from "./viz-theme.mjs";
|
|
22
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, demoEyebrowHtml, EYEBROW_LINKS_CSS } from "./viz-theme.mjs";
|
|
23
23
|
import { generateCodeHints } from "../domain/code-explorer-hints.mjs";
|
|
24
24
|
import { phraseForRelation } from "../domain/ask-vocab.mjs";
|
|
25
25
|
import { fetchWithProgress } from "./memory-panel-viz.mjs";
|
|
@@ -483,6 +483,7 @@ body { margin: 0; overflow: hidden; background: var(--bg); color: var(--ink); fo
|
|
|
483
483
|
.titlebar { display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap; padding: 0.5rem 0.9rem; background: var(--card); border-bottom: 1px solid var(--line); box-shadow: inset 0 -2px 0 var(--entail-soft); }
|
|
484
484
|
.titlebar .mark { width: 15px; height: 15px; color: var(--entail); flex: none; }
|
|
485
485
|
.titlebar h1 { font-size: 0.95rem; margin: 0; font-weight: 600; letter-spacing: 0.02em; }
|
|
486
|
+
${EYEBROW_LINKS_CSS}
|
|
486
487
|
.titlebar .sub { color: var(--muted); font-size: 0.76rem; font-family: ${MONO_STACK}; }
|
|
487
488
|
.titlebar .sub a { color: var(--corpus); }
|
|
488
489
|
/* the fact count sits in the titlebar, not the footer status bar: it is what
|
|
@@ -544,7 +545,7 @@ ul.rows { list-style: none; margin: 0; padding: 0; }
|
|
|
544
545
|
<div class="shell">
|
|
545
546
|
<header class="titlebar">
|
|
546
547
|
<svg class="mark" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 0l1 2.3a5.8 5.8 0 0 1 1.9.8L13.3 2l.7.7-1.1 2.4c.4.6.6 1.2.8 1.9L16 8l-2.3 1a5.8 5.8 0 0 1-.8 1.9l1.1 2.4-.7.7-2.4-1.1a5.8 5.8 0 0 1-1.9.8L8 16l-1-2.3a5.8 5.8 0 0 1-1.9-.8L2.7 14l-.7-.7 1.1-2.4a5.8 5.8 0 0 1-.8-1.9L0 8l2.3-1c.2-.7.4-1.3.8-1.9L2 2.7l.7-.7 2.4 1.1A5.8 5.8 0 0 1 7 2.3L8 0zm0 5.2A2.8 2.8 0 1 0 8 10.8 2.8 2.8 0 0 0 8 5.2z"/></svg>
|
|
547
|
-
<h1
|
|
548
|
+
<h1>${demoEyebrowHtml("code", "code explorer")}</h1>
|
|
548
549
|
<span class="sub">source: <span id="source-name">${escapeHtml(sourceName)}</span></span>
|
|
549
550
|
<span class="factpill" id="fact-total" aria-live="polite" title="every fact this page's chat can draw on — the code graph's own edges plus the general-knowledge seed once it lands">
|
|
550
551
|
<span class="factpill-value" id="fact-total-value">—</span> facts loaded
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// renderIngestHtml() is pure: no I/O, deterministic output for identical
|
|
26
26
|
// input. scripts/build-demo-site.mjs calls it directly and writes the result
|
|
27
27
|
// to public/ingest.html, after ingest-browser.bundle.js already exists.
|
|
28
|
-
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
|
|
28
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, demoEyebrowHtml, EYEBROW_LINKS_CSS } from "./viz-theme.mjs";
|
|
29
29
|
import {
|
|
30
30
|
bandLabelFor,
|
|
31
31
|
statsSummaryLine,
|
|
@@ -75,7 +75,8 @@ ${THEME_TOKENS_CSS}
|
|
|
75
75
|
header.topbar { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: .7rem 1.1rem; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
|
|
76
76
|
.brand { display: flex; flex-direction: column; gap: .1rem; }
|
|
77
77
|
.eyebrow { font-family: ${MONO_STACK}; font-size: .78rem; letter-spacing: .08em; color: var(--muted); }
|
|
78
|
-
|
|
78
|
+
${EYEBROW_LINKS_CSS}
|
|
79
|
+
.subtitle { margin: 0; font-size: .82rem; font-weight: 400; color: var(--muted); }
|
|
79
80
|
|
|
80
81
|
/* the live memory count, in the topbar rather than the status line: it is
|
|
81
82
|
the one number that says what this page's memory actually holds, and the
|
|
@@ -181,8 +182,8 @@ ${THEME_TOKENS_CSS}
|
|
|
181
182
|
<div class="ingestCol">
|
|
182
183
|
<header class="topbar">
|
|
183
184
|
<div class="brand">
|
|
184
|
-
<span class="eyebrow"
|
|
185
|
-
<
|
|
185
|
+
<span class="eyebrow">${demoEyebrowHtml("ingest", "ingest")}</span>
|
|
186
|
+
<h1 class="subtitle">ingest — paste or drop text. It keeps the facts it can ground and skips the rest.</h1>
|
|
186
187
|
</div>
|
|
187
188
|
<div class="topbar-right">
|
|
188
189
|
<span class="fact-pill" id="factPill" aria-live="polite"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// doesn't do: it reads the checked-in chat-dock engine bundle.
|
|
16
16
|
|
|
17
17
|
import { loadMemory, readFactRows, findContradictions, normFactTerm } from "../adapters/memory/core.mjs";
|
|
18
|
-
import { THEME_TOKENS_CSS, MONO_STACK, escapeHtml, embedJson, countLabel, TOKENS } from "./viz-theme.mjs";
|
|
18
|
+
import { THEME_TOKENS_CSS, MONO_STACK, escapeHtml, embedJson, countLabel, TOKENS, demoEyebrowHtml, EYEBROW_LINKS_CSS } from "./viz-theme.mjs";
|
|
19
19
|
import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
|
|
20
20
|
import { bfsLevels } from "../domain/planning.mjs";
|
|
21
21
|
import { pluralOf } from "../domain/inflect.mjs";
|
|
@@ -644,7 +644,9 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
644
644
|
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${DASH_SANS_STACK}; font-size: 15px; line-height: 1.5; }
|
|
645
645
|
.mono { font-family: ${MONO_STACK}; }
|
|
646
646
|
main { max-width: 1200px; margin: 0 auto; padding: 1.4rem 1.2rem 3rem; }
|
|
647
|
+
.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
|
647
648
|
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); display: flex; flex-wrap: wrap; gap: .4em 1.2em; margin-bottom: .9rem; }
|
|
649
|
+
${EYEBROW_LINKS_CSS}
|
|
648
650
|
button { font: inherit; color: inherit; background: none; border: none; padding: 0; cursor: pointer; }
|
|
649
651
|
button:focus-visible, input:focus-visible { outline: 2px solid var(--corpus); outline-offset: 2px; border-radius: 4px; }
|
|
650
652
|
.topbar { display: flex; flex-wrap: wrap; align-items: center; gap: .8rem; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: .5rem .7rem; margin-bottom: 1.1rem; }
|
|
@@ -789,7 +791,8 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
789
791
|
</head>
|
|
790
792
|
<body>
|
|
791
793
|
<main>
|
|
792
|
-
<
|
|
794
|
+
<h1 class="visually-hidden">memory ledger</h1>
|
|
795
|
+
<div class="eyebrow"><span>${demoEyebrowHtml("ledger", "memory ledger")}</span><span id="counts"></span></div>
|
|
793
796
|
${dashboardHtml(stats)}
|
|
794
797
|
<div class="topbar">
|
|
795
798
|
<nav class="crumbs" id="crumbs" aria-label="Focus trail"></nav>
|
package/src/services/mud-viz.mjs
CHANGED
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
// actually executed, never a race between panes.
|
|
77
77
|
import {
|
|
78
78
|
THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, scenarioLabel,
|
|
79
|
-
wordBeforeCursor, rowsForWorld, appendLogLine,
|
|
79
|
+
wordBeforeCursor, rowsForWorld, appendLogLine, demoEyebrowHtml, EYEBROW_LINKS_CSS,
|
|
80
80
|
} from "./viz-theme.mjs";
|
|
81
81
|
import { createTicker, createSerialQueue } from "./viz-ticker.mjs";
|
|
82
82
|
import { directedGridLayout, roomGraphSvg, levelsOf, EXIT_DELTA } from "./viz-room-graph.mjs";
|
|
@@ -392,7 +392,7 @@ ${MUD_SHARE_SKIN}
|
|
|
392
392
|
<body>
|
|
393
393
|
<main>
|
|
394
394
|
<header class="mud-topbar">
|
|
395
|
-
<h1 class="eyebrow"
|
|
395
|
+
<h1 class="eyebrow">${demoEyebrowHtml("mud", "mud")}</h1>
|
|
396
396
|
<div class="mud-topbar-actions">
|
|
397
397
|
<button type="button" class="state-pill" id="statePill" aria-expanded="false" aria-controls="netPanel"
|
|
398
398
|
title="whether this burrow is shared with anyone"><i class="state-dot"></i><span id="statePillWord">not shared</span></button>
|
|
@@ -411,7 +411,7 @@ ${scenarioList.length > 1 ? ` <select id="scenarioSelect" class="deck-sel
|
|
|
411
411
|
${scenarioList.map((s, i) => ` <option value="${i}"${i === 0 ? " selected" : ""}>${escapeHtml(s.label || scenarioLabel(s.worldPayload?.name))}</option>`).join("\n")}
|
|
412
412
|
</select>` : ""}
|
|
413
413
|
<button type="button" id="editModeBtn" aria-pressed="false">edit</button>
|
|
414
|
-
<label class="deck-teach" title="With this on, a sentence like "
|
|
414
|
+
<label class="deck-teach" title="With this on, a sentence like "Pebble lies in the garden." writes a fact into the world instead of running as a command.">
|
|
415
415
|
<input type="checkbox" id="teachToggle">
|
|
416
416
|
teach
|
|
417
417
|
</label>
|
|
@@ -590,6 +590,7 @@ const MUD_STYLE = `
|
|
|
590
590
|
.mono { font-family: ${MONO_STACK}; }
|
|
591
591
|
main { max-width: 1280px; margin: 0 auto; padding: 1.1rem 1.2rem 2.4rem; }
|
|
592
592
|
.eyebrow { font-family: ${MONO_STACK}; font-weight: 500; font-size: .72rem; letter-spacing: .16em; text-transform: uppercase; color: var(--parchment); opacity: .9; margin: 0 0 .8rem; }
|
|
593
|
+
${EYEBROW_LINKS_CSS}
|
|
593
594
|
|
|
594
595
|
/* ---- the header: brand on the left, the sharing chrome on the right ----
|
|
595
596
|
The same arrangement chat.html's topbar holds — sharing is page chrome,
|
|
@@ -42,12 +42,10 @@
|
|
|
42
42
|
// agent or item, never the mid-lerp position. This is what an e2e
|
|
43
43
|
// assertion reads, and what a later screenshot ready-check reads too
|
|
44
44
|
// (see test-e2e/pages-mudiii.test.mjs's own header).
|
|
45
|
+
// .flashCell(cellId) / .showRoute(cells) / .clearRoute() — the clicked
|
|
46
|
+
// cell and the route the followed agent is walking to it, drawn along
|
|
47
|
+
// the cells the world's own exit search returned.
|
|
45
48
|
// .ready() — whether boot() has finished at least once.
|
|
46
|
-
// These three calls are not yet wired into mudiii-viz.mjs's own
|
|
47
|
-
// boot()/applyTickResult()/camera handlers — that file is owned by the viz
|
|
48
|
-
// track, not this one; the coordinator's own report names the exact call
|
|
49
|
-
// sites needed, mirroring how window.mudiiiHandleSceneClick was already
|
|
50
|
-
// added for the reverse direction.
|
|
51
49
|
//
|
|
52
50
|
// Reused from mudiii-viz.mjs rather than re-derived, off its own frozen
|
|
53
51
|
// exports: `roleOfAgentId`, `cellToWorld`, `cellFromGroundPoint`,
|
|
@@ -366,6 +364,13 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
366
364
|
// removal (source "ecology") apart from applyTick's own diff no-op
|
|
367
365
|
// (source "diff") without guessing from a vanished mesh alone.
|
|
368
366
|
var removalLog = [];
|
|
367
|
+
var routeLine = null, routeCells = [];
|
|
368
|
+
var flashMesh = null, flashUntil = 0;
|
|
369
|
+
var FLASH_MS = 600;
|
|
370
|
+
// The floor a one-shot flourish is held for when its own clip is shorter,
|
|
371
|
+
// long enough that a bite reads as a bite at the deck's fastest tick.
|
|
372
|
+
var ONE_SHOT_HOLD_MIN_MS = 450;
|
|
373
|
+
var tickRungs = {};
|
|
369
374
|
var cameraState = { mode: "overhead", selectedId: null };
|
|
370
375
|
var cameraTween = null, lookAtTween = null;
|
|
371
376
|
var lastFrameTs = null;
|
|
@@ -557,10 +562,8 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
557
562
|
}
|
|
558
563
|
}
|
|
559
564
|
|
|
560
|
-
// ---- items: crumbs and morsels, the committed hay bale at
|
|
561
|
-
//
|
|
562
|
-
// matching the on-ground footprint the primitive spheres they replace were
|
|
563
|
-
// already tuned to. ---------------------------------------------------------
|
|
565
|
+
// ---- items: crumbs and morsels, both the committed hay bale, each at
|
|
566
|
+
// whatever targetHeight its own data/mudiii-assets.json row names. ----------
|
|
564
567
|
function itemAssetKeyFor(kind) { return kind === "morsel" ? "food-morsel" : "food-crumb"; }
|
|
565
568
|
|
|
566
569
|
function animateScaleTo(object3D, target, now) {
|
|
@@ -635,21 +638,56 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
635
638
|
// correctly clone a SkinnedMesh, and at this roster size — a handful of
|
|
636
639
|
// foxes and goblins — the extra memory of a separate load per agent is
|
|
637
640
|
// trivial next to that whole class of bug). ---------------------------------
|
|
641
|
+
// The id, drawn to a canvas and hung above the agent as a sprite. Parenting
|
|
642
|
+
// it to the agent's own group means it rides the movement tween with no
|
|
643
|
+
// per-frame bookkeeping, and a sprite always faces the camera, so there is
|
|
644
|
+
// no facing maths either. sizeAttenuation off holds it at a constant size on
|
|
645
|
+
// screen — an id that shrinks to nothing as the camera pulls back is worse
|
|
646
|
+
// than no id at all — which is why the scale below is in screen fractions
|
|
647
|
+
// rather than world units.
|
|
648
|
+
var LABEL_SCREEN_WIDTH = 0.13;
|
|
649
|
+
function makeAgentLabel(id, height) {
|
|
650
|
+
var canvas = document.createElement("canvas");
|
|
651
|
+
canvas.width = 256;
|
|
652
|
+
canvas.height = 64;
|
|
653
|
+
var ctx = canvas.getContext("2d");
|
|
654
|
+
ctx.font = "bold 38px ui-monospace, SFMono-Regular, Menlo, monospace";
|
|
655
|
+
ctx.textAlign = "center";
|
|
656
|
+
ctx.textBaseline = "middle";
|
|
657
|
+
ctx.lineWidth = 8;
|
|
658
|
+
ctx.lineJoin = "round";
|
|
659
|
+
ctx.strokeStyle = "rgba(20,16,10,.85)";
|
|
660
|
+
ctx.strokeText(id, 128, 34);
|
|
661
|
+
ctx.fillStyle = "#F3ECDD";
|
|
662
|
+
ctx.fillText(id, 128, 34);
|
|
663
|
+
var material = new THREE.SpriteMaterial({
|
|
664
|
+
map: new THREE.CanvasTexture(canvas), transparent: true, sizeAttenuation: false, depthWrite: false,
|
|
665
|
+
});
|
|
666
|
+
var sprite = new THREE.Sprite(material);
|
|
667
|
+
sprite.scale.set(LABEL_SCREEN_WIDTH, LABEL_SCREEN_WIDTH / 4, 1);
|
|
668
|
+
sprite.position.y = height + 0.3;
|
|
669
|
+
sprite.name = "label-" + id;
|
|
670
|
+
return sprite;
|
|
671
|
+
}
|
|
672
|
+
|
|
638
673
|
function ensureAgent(id, agent) {
|
|
639
674
|
if (agentGroups[id]) return agentGroups[id];
|
|
640
675
|
var kind = roleOfAgentId(id);
|
|
641
676
|
var entry = {
|
|
642
677
|
group: new THREE.Group(), tween: null, cell: null, facing: agent.facing, role: agent.role,
|
|
643
678
|
kind: kind, mixer: null, actions: {}, currentClip: null, clipMap: null, oneShotAction: null,
|
|
679
|
+
oneShotUntil: 0, model: null,
|
|
644
680
|
};
|
|
645
681
|
entry.group.visible = false;
|
|
646
682
|
scene.add(entry.group);
|
|
647
683
|
agentGroups[id] = entry;
|
|
648
684
|
var asset = manifestByKind[kind];
|
|
685
|
+
entry.group.add(makeAgentLabel(id, asset ? asset.targetHeight : 1));
|
|
649
686
|
if (asset) {
|
|
650
687
|
loadGlbRaw(modelUrlFor(asset.destPath)).then(function (gltf) {
|
|
651
688
|
normalizeToHeight(gltf.scene, asset.targetHeight);
|
|
652
689
|
entry.group.add(gltf.scene);
|
|
690
|
+
entry.model = gltf.scene;
|
|
653
691
|
entry.group.visible = true;
|
|
654
692
|
entry.clipMap = asset.clips || {};
|
|
655
693
|
entry.mixer = new THREE.AnimationMixer(gltf.scene);
|
|
@@ -671,6 +709,12 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
671
709
|
// keep blending into every clip after it, forever). Fade it out before
|
|
672
710
|
// any of the guards below can skip the rest of this call.
|
|
673
711
|
if (entry.oneShotAction) {
|
|
712
|
+
// A flourish holds for its own clip length, floored, before anything may
|
|
713
|
+
// fade it out. At the deck's 220ms default the next tick otherwise
|
|
714
|
+
// landed during the wind-up and a bite read as a twitch. This holds the
|
|
715
|
+
// ANIMATION only: the tick that arrived has already moved the agent, so
|
|
716
|
+
// the simulation never waits on a flourish.
|
|
717
|
+
if (performance.now() < entry.oneShotUntil) return;
|
|
674
718
|
entry.oneShotAction.fadeOut(0.15);
|
|
675
719
|
entry.oneShotAction = null;
|
|
676
720
|
}
|
|
@@ -702,6 +746,9 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
702
746
|
action.fadeIn(0.15).play();
|
|
703
747
|
if (prev) prev.fadeOut(0.15);
|
|
704
748
|
entry.oneShotAction = action;
|
|
749
|
+
var clip = typeof action.getClip === "function" ? action.getClip() : null;
|
|
750
|
+
entry.oneShotUntil = performance.now()
|
|
751
|
+
+ Math.max(ONE_SHOT_HOLD_MIN_MS, clip && clip.duration ? clip.duration * 1000 : 0);
|
|
705
752
|
entry.currentClip = null;
|
|
706
753
|
}
|
|
707
754
|
|
|
@@ -731,7 +778,10 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
731
778
|
}
|
|
732
779
|
if (entry.clipMap) {
|
|
733
780
|
var moving = singleHop;
|
|
734
|
-
|
|
781
|
+
// A hand-driven step is a walk, whatever the agent believes: it did not
|
|
782
|
+
// choose to chase or flee, the visitor chose for it.
|
|
783
|
+
var action = moving && tickRungs[id] === "driven" ? "driven" : currentActionFor(id, lastAgentsById, moving);
|
|
784
|
+
playClip(entry, clipForAction(agent.role, action, entry.clipMap));
|
|
735
785
|
}
|
|
736
786
|
}
|
|
737
787
|
|
|
@@ -766,6 +816,61 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
766
816
|
}
|
|
767
817
|
}
|
|
768
818
|
|
|
819
|
+
// ---- the clicked cell, and the route to it --------------------------------
|
|
820
|
+
// The line follows the cells the world's own exit search returned, never a
|
|
821
|
+
// straight segment from agent to target: a straight one cuts through
|
|
822
|
+
// buildings and promises a walk the board would refuse.
|
|
823
|
+
function clearRoute() {
|
|
824
|
+
routeCells = [];
|
|
825
|
+
if (!routeLine) return;
|
|
826
|
+
if (routeLine.parent) routeLine.parent.remove(routeLine);
|
|
827
|
+
routeLine.geometry.dispose();
|
|
828
|
+
routeLine.material.dispose();
|
|
829
|
+
routeLine = null;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function showRoute(cells) {
|
|
833
|
+
clearRoute();
|
|
834
|
+
if (!scene || !cells || cells.length < 2) return;
|
|
835
|
+
var points = [];
|
|
836
|
+
for (var i = 0; i < cells.length; i += 1) {
|
|
837
|
+
var world = cellToWorld(cells[i], GRID_SIZE, CELL_SIZE);
|
|
838
|
+
if (!world) return;
|
|
839
|
+
points.push(new THREE.Vector3(world.x, 0.09, world.z));
|
|
840
|
+
}
|
|
841
|
+
routeLine = new THREE.Line(
|
|
842
|
+
new THREE.BufferGeometry().setFromPoints(points),
|
|
843
|
+
new THREE.LineBasicMaterial({ color: 0xd98a2b }),
|
|
844
|
+
);
|
|
845
|
+
routeLine.name = "route";
|
|
846
|
+
scene.add(routeLine);
|
|
847
|
+
routeCells = cells.slice();
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function clearFlash() {
|
|
851
|
+
if (!flashMesh) return;
|
|
852
|
+
if (flashMesh.parent) flashMesh.parent.remove(flashMesh);
|
|
853
|
+
flashMesh.geometry.dispose();
|
|
854
|
+
flashMesh.material.dispose();
|
|
855
|
+
flashMesh = null;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function flashCell(cell) {
|
|
859
|
+
if (!scene) return;
|
|
860
|
+
var world = cellToWorld(cell, GRID_SIZE, CELL_SIZE);
|
|
861
|
+
if (!world) return;
|
|
862
|
+
clearFlash();
|
|
863
|
+
flashMesh = new THREE.Mesh(
|
|
864
|
+
new THREE.PlaneGeometry(CELL_SIZE, CELL_SIZE),
|
|
865
|
+
new THREE.MeshBasicMaterial({ color: 0xd98a2b, transparent: true, opacity: 0.75 }),
|
|
866
|
+
);
|
|
867
|
+
flashMesh.name = "cell-flash";
|
|
868
|
+
flashMesh.rotation.x = -Math.PI / 2;
|
|
869
|
+
flashMesh.position.set(world.x, 0.03, world.z);
|
|
870
|
+
scene.add(flashMesh);
|
|
871
|
+
flashUntil = performance.now() + FLASH_MS;
|
|
872
|
+
}
|
|
873
|
+
|
|
769
874
|
// ---- camera ---------------------------------------------------------------
|
|
770
875
|
function agentSnapshotFor(id) {
|
|
771
876
|
var entry = agentGroups[id];
|
|
@@ -817,6 +922,9 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
817
922
|
itemMeshes = {};
|
|
818
923
|
lastAgentsById = {};
|
|
819
924
|
lastItemsById = {};
|
|
925
|
+
tickRungs = {};
|
|
926
|
+
clearRoute();
|
|
927
|
+
clearFlash();
|
|
820
928
|
removalLog = [];
|
|
821
929
|
manifestByKind = buildManifestByKind(input && input.assetManifest);
|
|
822
930
|
await placeProps((input && input.propPlacements) || []);
|
|
@@ -829,6 +937,7 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
829
937
|
var now = performance.now();
|
|
830
938
|
var agents = (tick && tick.agents) || {};
|
|
831
939
|
var items = (tick && tick.items) || {};
|
|
940
|
+
tickRungs = (tick && tick.rungs) || {};
|
|
832
941
|
for (var id in agents) if (Object.prototype.hasOwnProperty.call(agents, id)) applyAgentTick(id, agents[id], now);
|
|
833
942
|
for (var itemId in items) if (Object.prototype.hasOwnProperty.call(items, itemId)) applyItemTick(itemId, items[itemId], now);
|
|
834
943
|
// Ecology first: an eaten agent/item is already gone from this tick's own
|
|
@@ -859,6 +968,11 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
859
968
|
}
|
|
860
969
|
if (entry.mixer) entry.mixer.update(deltaSec);
|
|
861
970
|
}
|
|
971
|
+
if (flashMesh) {
|
|
972
|
+
var flashLeft = flashUntil - performance.now();
|
|
973
|
+
if (flashLeft <= 0) clearFlash();
|
|
974
|
+
else flashMesh.material.opacity = 0.75 * (flashLeft / FLASH_MS);
|
|
975
|
+
}
|
|
862
976
|
if (cameraTween) { var cp = tweenStep(cameraTween, ts); camera3.position.set(cp.x, cp.y, cp.z); }
|
|
863
977
|
if (lookAtTween) { var lp = tweenStep(lookAtTween, ts); camera3.lookAt(lp.x, lp.y, lp.z); }
|
|
864
978
|
if (orbitControls && orbitControls.enabled) orbitControls.update();
|
|
@@ -869,6 +983,13 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
869
983
|
boot: boot,
|
|
870
984
|
applyTick: applyTick,
|
|
871
985
|
setCamera: setCamera,
|
|
986
|
+
flashCell: flashCell,
|
|
987
|
+
showRoute: showRoute,
|
|
988
|
+
clearRoute: clearRoute,
|
|
989
|
+
// The route currently drawn, cell by cell — an e2e assertion's read, so
|
|
990
|
+
// it can check the line follows the board's own exits rather than
|
|
991
|
+
// counting pixels on a software renderer.
|
|
992
|
+
routeCellsDrawn: function () { return routeCells.slice(); },
|
|
872
993
|
cellOf: function (id) {
|
|
873
994
|
if (agentGroups[id]) return agentGroups[id].cell;
|
|
874
995
|
if (itemMeshes[id]) return itemMeshes[id].cell;
|
|
@@ -886,10 +1007,13 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
886
1007
|
// manifest's own targetHeight to compare it against — an e2e assertion's
|
|
887
1008
|
// read, so it goes through the group actually in the scene rather than a
|
|
888
1009
|
// second, locally invented measurement.
|
|
1010
|
+
// Measured through the loaded MODEL, never the whole group: the group
|
|
1011
|
+
// also carries the id label, whose sprite geometry would widen the box
|
|
1012
|
+
// and report a height nobody rendered.
|
|
889
1013
|
meshHeightOf: function (id) {
|
|
890
1014
|
var entry = agentGroups[id];
|
|
891
|
-
if (!entry || !entry.
|
|
892
|
-
var box = new THREE.Box3().setFromObject(entry.
|
|
1015
|
+
if (!entry || !entry.model) return null;
|
|
1016
|
+
var box = new THREE.Box3().setFromObject(entry.model);
|
|
893
1017
|
var size = new THREE.Vector3();
|
|
894
1018
|
box.getSize(size);
|
|
895
1019
|
var asset = manifestByKind[entry.kind];
|