@polycode-projects/the-mechanical-code-talker 2.7.26 → 2.8.1
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 +30 -16
- package/corpus/tier2/generate.mjs +1 -0
- package/corpus/tier2/human.jsonl +1 -0
- package/corpus/tier2/manifest.json +3 -3
- package/package.json +1 -1
- package/src/adapters/corpus/sprite-large-template-files.mjs +6 -2
- package/src/domain/sprite-expressions.mjs +120 -0
- package/src/domain/sprite-templates.mjs +31 -9
- package/src/services/adventure-viz.mjs +266 -36
- package/src/services/chat-page-viz.mjs +398 -0
- package/src/services/sprite-catalog-viz.mjs +395 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +38 -12
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
// sprite-catalog-viz.mjs — `public/sprites.html` (PLAN_GAMES_UPLIFT_V3.md
|
|
2
|
+
// Part C.2's "Sprite library" link-card item): every class the sprite
|
|
3
|
+
// library actually resolves a sprite for, at both tiers (data/sprites/
|
|
4
|
+
// *-icon.toml, 44px; data/sprites-large/*.toml, 400px), grouped for
|
|
5
|
+
// browsing, each swatch carrying its own real ontology mapping — the class
|
|
6
|
+
// name, its resolved template, and the rdfs:subClassOf ancestor chain the
|
|
7
|
+
// engine would actually walk to reach it — computed through the SAME
|
|
8
|
+
// resolver code the product itself runs (src/domain/sprite-map.mjs's
|
|
9
|
+
// classAncestorChain, src/domain/sprite-templates.mjs's resolveSpriteAsset),
|
|
10
|
+
// never a hand-simulated stand-in.
|
|
11
|
+
//
|
|
12
|
+
// Three pure/impure-separated pieces, mirroring ledger-viz.mjs's own
|
|
13
|
+
// computeLedgerData / computeLedgerDataFromPayload / renderLedgerHtml split:
|
|
14
|
+
// - loadSpriteOntologyFactRows() — I/O: the real ancestor-fact source
|
|
15
|
+
// - buildSpriteCatalogEntries(...) — pure derivation over templates+facts
|
|
16
|
+
// - renderSpriteCatalogHtml(...) — pure string builder
|
|
17
|
+
//
|
|
18
|
+
// ---- Ancestor facts: real, not invented ----
|
|
19
|
+
// classAncestorChain needs a flat {subject, predicate:"rdfs:subClassOf",
|
|
20
|
+
// object} row set to walk. Two REAL, already-committed sources are combined:
|
|
21
|
+
// - the spider-and-fly world's own SEED_TAXONOMY (src/domain/
|
|
22
|
+
// spider-fly-world.mjs) — poodle/dog/animal, spider/arachnid/animal,
|
|
23
|
+
// fly/insect/animal — the exact worked example sprite-map.mjs's own
|
|
24
|
+
// header names.
|
|
25
|
+
// - corpus/wordnet/wordnet-xl.jsonl (23,805 rows), the SAME opt-in
|
|
26
|
+
// "wordnet-xl" corpus extension the product itself ships (src/services/
|
|
27
|
+
// extensions.mjs), converted to rdfs:subClassOf facts through the
|
|
28
|
+
// existing src/adapters/corpus/conceptnet.mjs loader (loadSlice/loadMap/
|
|
29
|
+
// toFacts) — no bespoke parsing invented for this page.
|
|
30
|
+
// corpus/wordnet/wordnet-full.jsonl (192k rows, every WordNet sense
|
|
31
|
+
// unfiltered) was tried and rejected: with no word-sense disambiguation its
|
|
32
|
+
// hypernym graph conflates a word's every sense onto one node, so a class as
|
|
33
|
+
// ordinary as "poodle" walks into 3000+ unrelated ancestors and the walk
|
|
34
|
+
// itself takes minutes. wordnet-xl's own "prioritized subset" curation
|
|
35
|
+
// avoids most of that; what's left is capped for DISPLAY (see
|
|
36
|
+
// MAX_CHAIN_DISPLAY below) rather than hidden — still the real chain,
|
|
37
|
+
// just not printed to its full, occasionally very long, length.
|
|
38
|
+
//
|
|
39
|
+
// Every class catalogued here already carries its own template (that's
|
|
40
|
+
// what put it in the catalog), so live sprite resolution always stops at
|
|
41
|
+
// the chain's own first link — the fuller ancestor chain this page prints
|
|
42
|
+
// is real ancestry ON RECORD in the corpus, not a claim that resolution
|
|
43
|
+
// walks that far for THESE classes (it would, for an unregistered subtype
|
|
44
|
+
// like "sheepdog" — sprite-map.mjs's own worked example — which is exactly
|
|
45
|
+
// why the mechanism exists, just not exercised by any class shown here).
|
|
46
|
+
|
|
47
|
+
import { classAncestorChain, SPRITE_REGISTRY } from "../domain/sprite-map.mjs";
|
|
48
|
+
import { resolveSpriteAsset } from "../domain/sprite-templates.mjs";
|
|
49
|
+
import { MATERIAL_PALETTE } from "../domain/sprite-materials.mjs";
|
|
50
|
+
import { SEED_TAXONOMY } from "../domain/spider-fly-world.mjs";
|
|
51
|
+
import { loadSlice, loadMap, toFacts, WORDNET_DIR } from "../adapters/corpus/conceptnet.mjs";
|
|
52
|
+
import { join } from "node:path";
|
|
53
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
54
|
+
|
|
55
|
+
const DEFAULT_TITLE = "tmct — the sprite library";
|
|
56
|
+
const MAX_CHAIN_DISPLAY = 6;
|
|
57
|
+
|
|
58
|
+
/** The real rdfs:subClassOf fact rows this catalog's ancestor chains walk —
|
|
59
|
+
* see this module's own header for why these two sources and not a third.
|
|
60
|
+
* I/O (reads corpus/wordnet/wordnet-xl.jsonl + its relation map); never
|
|
61
|
+
* called from renderSpriteCatalogHtml itself, which stays pure. */
|
|
62
|
+
export async function loadSpriteOntologyFactRows() {
|
|
63
|
+
const seedRows = SEED_TAXONOMY.map(([subject, object]) => ({ subject, predicate: "rdfs:subClassOf", object }));
|
|
64
|
+
const assertions = await loadSlice(join(WORDNET_DIR, "wordnet-xl.jsonl"));
|
|
65
|
+
const map = await loadMap();
|
|
66
|
+
const wordnetRows = toFacts(assertions, map, "corpus:wordnet-xl")
|
|
67
|
+
.filter((f) => f.predicate === "rdfs:subClassOf")
|
|
68
|
+
.map((f) => ({ subject: f.subject, predicate: f.predicate, object: f.object }));
|
|
69
|
+
return [...seedRows, ...wordnetRows];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- grouping (presentation only — every class still resolves through the
|
|
73
|
+
// real resolver above; this only decides which section of the page a
|
|
74
|
+
// class's card lands in) ----
|
|
75
|
+
|
|
76
|
+
// The icon tier's own two world families (sprite-map.mjs's header): the
|
|
77
|
+
// spider-and-fly board's creatures share the flat SPRITE_REGISTRY with the
|
|
78
|
+
// adventure world's own props. The creatures fold into "physical objects,
|
|
79
|
+
// creatures & places" below with every other animal; what's left after
|
|
80
|
+
// removing them (and "person", generic to both worlds) is Ashcombe Hall's
|
|
81
|
+
// own unique cast and furniture — never shared with another game.
|
|
82
|
+
const SPIDER_FLY_CREATURE_CLASSES = Object.freeze(["spider", "fly", "egg", "poodle", "dog", "animal"]);
|
|
83
|
+
|
|
84
|
+
// A curated closed list (the SOURCE_PRIOR/SPRITE_REGISTRY flat-table idiom
|
|
85
|
+
// this project already uses elsewhere) of every data/sprites-large/ class
|
|
86
|
+
// that reads as a person, a family relation, a social role/occupation, or a
|
|
87
|
+
// collective of people. A class named neither here nor an icon-tier
|
|
88
|
+
// adventure prop nor detected as an emoji-fallback class (both below) falls
|
|
89
|
+
// through to "physical objects, creatures & places" by default, so a future
|
|
90
|
+
// added class is never left uncategorized — it just lands in the generic
|
|
91
|
+
// bucket rather than vanishing.
|
|
92
|
+
export const PERSON_ROLE_CLASSES = Object.freeze([
|
|
93
|
+
"person", "human", "adult", "baby", "child", "boy", "girl", "man", "woman",
|
|
94
|
+
"mother", "father", "parent", "grandfather", "grandmother", "brother", "sister",
|
|
95
|
+
"son", "daughter", "husband", "wife", "family", "friend", "neighbor", "stranger",
|
|
96
|
+
"guest", "visitor", "customer", "employee", "boss", "manager", "leader",
|
|
97
|
+
"president", "king", "queen", "judge", "lawyer", "priest", "doctor", "nurse",
|
|
98
|
+
"teacher", "student", "engineer", "artist", "writer", "farmer", "driver",
|
|
99
|
+
"soldier", "officer", "servant", "worker", "volunteer", "citizen", "resident",
|
|
100
|
+
"champion", "crowd", "audience", "team",
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
export const GROUP_ADVENTURE = "adventure";
|
|
104
|
+
export const GROUP_PERSON = "person";
|
|
105
|
+
export const GROUP_OBJECT = "object";
|
|
106
|
+
export const GROUP_EMOJI = "emoji";
|
|
107
|
+
|
|
108
|
+
export const CATALOG_GROUPS = Object.freeze([
|
|
109
|
+
Object.freeze({ id: GROUP_ADVENTURE, label: "Ashcombe Hall's own adventure props", note: "the icon tier's own named cast and furniture — a dedicated 44px sprite exists for each" }),
|
|
110
|
+
Object.freeze({ id: GROUP_PERSON, label: "Person roles" }),
|
|
111
|
+
Object.freeze({ id: GROUP_OBJECT, label: "Physical objects, creatures & places" }),
|
|
112
|
+
Object.freeze({ id: GROUP_EMOJI, label: "Emotions & events", note: "abstract concepts with no honest single physical picture — rendered as the ubiquitous emoji instead" }),
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
/** Which catalog section `cls` belongs in. Pure. `isIconTierClass`/`isEmoji`
|
|
116
|
+
* are handed in rather than recomputed here so this stays a one-line
|
|
117
|
+
* decision over already-known facts about the class. */
|
|
118
|
+
export function groupForClass(cls, { isIconTierClass, isEmoji }) {
|
|
119
|
+
if (isEmoji) return GROUP_EMOJI;
|
|
120
|
+
if (isIconTierClass && !SPIDER_FLY_CREATURE_CLASSES.includes(cls) && cls !== "person") return GROUP_ADVENTURE;
|
|
121
|
+
if (PERSON_ROLE_CLASSES.includes(cls)) return GROUP_PERSON;
|
|
122
|
+
return GROUP_OBJECT;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function templatesForClass(cls, templates) {
|
|
126
|
+
return (templates || []).filter((t) => Array.isArray(t?.classes) && t.classes.includes(cls));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The MATERIAL_PALETTE treatment key whose {light,base,dark} triple exactly
|
|
130
|
+
* matches `value` — the reverse of sprite-materials.mjs's own
|
|
131
|
+
* expandMaterialReferences, so a swatch can show e.g. "gold -> metal
|
|
132
|
+
* treatment" (sprite-materials.mjs's own header: "gold and metal both read
|
|
133
|
+
* as the same warm shiny-metal treatment"). null for a plain single-
|
|
134
|
+
* placeholder colour value (a string, not an object) or a one-off hand-
|
|
135
|
+
* authored triple that matches no shared treatment. Pure. */
|
|
136
|
+
export function paletteTreatmentFor(value) {
|
|
137
|
+
if (!value || typeof value !== "object") return null;
|
|
138
|
+
for (const [name, triple] of Object.entries(MATERIAL_PALETTE)) {
|
|
139
|
+
if (triple.light === value.light && triple.base === value.base && triple.dark === value.dark) return name;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Every real value a template's own [parameters.*] declares, as
|
|
145
|
+
* {paramName, property, rawValue, treatment} rows — one row per value key,
|
|
146
|
+
* read directly off the template's own data, never invented. Pure. */
|
|
147
|
+
export function parameterVariantsFor(template) {
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const [paramName, param] of Object.entries(template?.parameters || {})) {
|
|
150
|
+
for (const [rawValue, value] of Object.entries(param?.values || {})) {
|
|
151
|
+
out.push({ paramName, property: param.property, rawValue, treatment: paletteTreatmentFor(value) });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Swatches for one tier's template set: a plain swatch ONLY when a real
|
|
158
|
+
* `{class}.toml`-shaped plain template exists (no [parameters]/[match] at
|
|
159
|
+
* all) — never a synthesized "plain" label for a material-only class,
|
|
160
|
+
* since with zero taught facts that class's real resolved output is the
|
|
161
|
+
* generic root-fallback shape, not its own silhouette (this module's own
|
|
162
|
+
* header names the 19 large-tier classes this applies to). Every
|
|
163
|
+
* [parameters.*] value and every [match] variant renders through the real
|
|
164
|
+
* resolveSpriteAsset with the exact property fact it declares. A class
|
|
165
|
+
* with templates but no plain among them also gets one extra swatch,
|
|
166
|
+
* labeled `fallback: true`, showing exactly what the real resolver returns
|
|
167
|
+
* for that class with NO taught fact — honest engine behaviour, never
|
|
168
|
+
* hidden, just never mislabeled "plain". Pure given `templates` (already
|
|
169
|
+
* loaded) and `registry` (SPRITE_REGISTRY). */
|
|
170
|
+
export function tierSwatchesFor(cls, templates, registry, tier) {
|
|
171
|
+
const forClass = templatesForClass(cls, templates);
|
|
172
|
+
if (!forClass.length) return [];
|
|
173
|
+
const plain = forClass.find((t) => !t.parameters && !t.match);
|
|
174
|
+
const swatches = [];
|
|
175
|
+
if (plain) {
|
|
176
|
+
swatches.push({
|
|
177
|
+
tier, label: "plain", kind: "plain",
|
|
178
|
+
svg: resolveSpriteAsset(cls, [], [], templates, registry, { instanceKey: `${cls}-${tier}-plain` }),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
for (const t of forClass) {
|
|
182
|
+
if (t.match) {
|
|
183
|
+
const propertyFacts = [{ predicate: t.match.property, object: t.match.value }];
|
|
184
|
+
swatches.push({
|
|
185
|
+
tier, label: t.match.value, kind: "variant", property: t.match.property,
|
|
186
|
+
svg: resolveSpriteAsset(cls, [], propertyFacts, templates, registry, { instanceKey: `${cls}-${tier}-match-${t.match.value}` }),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
for (const v of parameterVariantsFor(t)) {
|
|
190
|
+
const propertyFacts = [{ predicate: v.property, object: v.rawValue }];
|
|
191
|
+
swatches.push({
|
|
192
|
+
tier, label: v.rawValue, kind: "material", property: v.property, treatment: v.treatment,
|
|
193
|
+
svg: resolveSpriteAsset(cls, [], propertyFacts, templates, registry, { instanceKey: `${cls}-${tier}-${v.paramName}-${v.rawValue}` }),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!plain && swatches.length) {
|
|
198
|
+
swatches.push({
|
|
199
|
+
tier, label: "no material taught", kind: "fallback", fallback: true,
|
|
200
|
+
svg: resolveSpriteAsset(cls, [], [], templates, registry, { instanceKey: `${cls}-${tier}-fallback` }),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return swatches;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The full catalog: one entry per class the icon and/or large tier
|
|
207
|
+
* actually carries a template for, each with its real ancestor chain and
|
|
208
|
+
* every real tier/material swatch. Pure given the three loaded inputs.
|
|
209
|
+
* `factRows` defaults to `[]` (every chain then reads as just the class's
|
|
210
|
+
* own name — an honest, if less illustrative, chain rather than a crash)
|
|
211
|
+
* so a caller that hasn't loaded the ontology facts yet still gets a
|
|
212
|
+
* working catalog. */
|
|
213
|
+
export function buildSpriteCatalogEntries({ iconTemplates = [], largeTemplates = [], factRows = [] } = {}) {
|
|
214
|
+
const iconClasses = new Set(iconTemplates.flatMap((t) => t?.classes || []));
|
|
215
|
+
const largeClasses = new Set(largeTemplates.flatMap((t) => t?.classes || []));
|
|
216
|
+
const allClasses = [...new Set([...iconClasses, ...largeClasses])].sort();
|
|
217
|
+
|
|
218
|
+
return allClasses.map((cls) => {
|
|
219
|
+
const chain = classAncestorChain(cls, factRows);
|
|
220
|
+
const iconSwatches = tierSwatchesFor(cls, iconTemplates, SPRITE_REGISTRY, "icon");
|
|
221
|
+
const largeSwatches = tierSwatchesFor(cls, largeTemplates, SPRITE_REGISTRY, "large");
|
|
222
|
+
const isEmoji = [...iconSwatches, ...largeSwatches].some((s) => s.svg.includes("<text"));
|
|
223
|
+
const group = groupForClass(cls, { isIconTierClass: iconClasses.has(cls), isEmoji });
|
|
224
|
+
return { className: cls, group, chain, iconSwatches, largeSwatches };
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---- rendering ----
|
|
229
|
+
|
|
230
|
+
function chainHtml(chain) {
|
|
231
|
+
const shown = chain.slice(0, MAX_CHAIN_DISPLAY);
|
|
232
|
+
const rest = chain.length - shown.length;
|
|
233
|
+
const links = shown
|
|
234
|
+
.map((term, i) => `<span class="chain-link${i === 0 ? " own" : ""}">${escapeHtml(term)}</span>`)
|
|
235
|
+
.join('<span class="chain-arrow">›</span>');
|
|
236
|
+
const more = rest > 0 ? `<span class="chain-more">+${rest} more on record</span>` : "";
|
|
237
|
+
return `<div class="chain">${links}${more}</div>`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function swatchHtml(s) {
|
|
241
|
+
const parts = [`<span class="swatch-label">${escapeHtml(s.label)}</span>`];
|
|
242
|
+
if (s.treatment) parts.push(`<span class="swatch-treat">→ ${escapeHtml(s.treatment)} treatment</span>`);
|
|
243
|
+
const title = s.property ? `${s.property} = ${s.label}` : s.label;
|
|
244
|
+
const cls = ["swatch", s.tier, s.kind].filter(Boolean).join(" ");
|
|
245
|
+
return `<div class="${cls}" title="${escapeHtml(title)}"><div class="swatch-img">${s.svg}</div><div class="swatch-caption">${parts.join("")}</div></div>`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function tierRowHtml(tierName, swatches) {
|
|
249
|
+
if (!swatches.length) return "";
|
|
250
|
+
return `<div class="tier-row" data-tier="${tierName}">
|
|
251
|
+
<span class="tier-label">${tierName === "icon" ? "icon · 44px" : "sprite · 400px"}</span>
|
|
252
|
+
<div class="swatches">${swatches.map(swatchHtml).join("")}</div>
|
|
253
|
+
</div>`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function cardHtml(entry) {
|
|
257
|
+
return `<article class="card" data-cls="${escapeHtml(entry.className)}" data-group="${escapeHtml(entry.group)}">
|
|
258
|
+
<h3 class="card-name">${escapeHtml(entry.className)}</h3>
|
|
259
|
+
${chainHtml(entry.chain)}
|
|
260
|
+
${tierRowHtml("icon", entry.iconSwatches)}
|
|
261
|
+
${tierRowHtml("large", entry.largeSwatches)}
|
|
262
|
+
</article>`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function sectionHtml(group, entries) {
|
|
266
|
+
const rows = entries.filter((e) => e.group === group.id);
|
|
267
|
+
if (!rows.length) return "";
|
|
268
|
+
const note = group.note ? `<p class="section-note">${escapeHtml(group.note)}</p>` : "";
|
|
269
|
+
return `<section class="group" id="g-${group.id}" aria-label="${escapeHtml(group.label)}">
|
|
270
|
+
<h2>${escapeHtml(group.label)} <span class="count">${rows.length}</span></h2>
|
|
271
|
+
${note}
|
|
272
|
+
<div class="cards">${rows.map(cardHtml).join("")}</div>
|
|
273
|
+
</section>`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The self-contained sprite-catalog page. Pure given `iconTemplates`
|
|
277
|
+
* (readSpriteTemplateFiles' own output), `largeTemplates`
|
|
278
|
+
* (readSpriteLargeTemplateFiles' own output) and `factRows`
|
|
279
|
+
* (loadSpriteOntologyFactRows' own output) — the same "byte-identical for
|
|
280
|
+
* identical input" invariant every other viz page in this project holds.
|
|
281
|
+
* All three default to `[]` so a caller mid-migration (no ontology facts
|
|
282
|
+
* loaded yet, say) still gets a page that renders, just with plainer
|
|
283
|
+
* ancestor chains. */
|
|
284
|
+
export function renderSpriteCatalogHtml({ title = DEFAULT_TITLE, iconTemplates = [], largeTemplates = [], factRows = [] } = {}) {
|
|
285
|
+
const entries = buildSpriteCatalogEntries({ iconTemplates, largeTemplates, factRows });
|
|
286
|
+
const totalSwatches = entries.reduce((n, e) => n + e.iconSwatches.length + e.largeSwatches.length, 0);
|
|
287
|
+
const pageData = embedJson({ classCount: entries.length, swatchCount: totalSwatches });
|
|
288
|
+
const navHtml = CATALOG_GROUPS
|
|
289
|
+
.map((g) => `<a class="jump" href="#g-${g.id}">${escapeHtml(g.label)} <span class="count">${entries.filter((e) => e.group === g.id).length}</span></a>`)
|
|
290
|
+
.join("");
|
|
291
|
+
|
|
292
|
+
return `<!doctype html>
|
|
293
|
+
<html lang="en">
|
|
294
|
+
<head>
|
|
295
|
+
<meta charset="utf-8">
|
|
296
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
297
|
+
<title>${escapeHtml(title)}</title>
|
|
298
|
+
<style>
|
|
299
|
+
${THEME_TOKENS_CSS}
|
|
300
|
+
html { background: var(--bg); }
|
|
301
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
|
|
302
|
+
.mono { font-family: ${MONO_STACK}; }
|
|
303
|
+
main { max-width: 1180px; margin: 0 auto; padding: 1.4rem 1.2rem 3rem; }
|
|
304
|
+
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
305
|
+
h1 { font-size: 1.4rem; margin: .3rem 0 .6rem; text-wrap: balance; }
|
|
306
|
+
.intro { max-width: 74ch; color: var(--muted); font-size: .92rem; }
|
|
307
|
+
.topbar { position: sticky; top: 0; z-index: 2; background: var(--bg); display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .9rem; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); padding: .6rem 0; margin: 1rem 0 1.2rem; }
|
|
308
|
+
.jump { font-family: ${MONO_STACK}; font-size: .72rem; padding: .18rem .55rem; border: 1px solid var(--line); border-radius: 99px; background: var(--card); color: var(--ink); text-decoration: none; }
|
|
309
|
+
.jump:hover { border-color: var(--taught); }
|
|
310
|
+
.jump .count { color: var(--muted); }
|
|
311
|
+
.filter { margin-left: auto; display: flex; align-items: center; gap: .4rem; }
|
|
312
|
+
.filter input { font-family: ${MONO_STACK}; font-size: .8rem; background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .32rem .6rem; width: 200px; }
|
|
313
|
+
.filter .n { font-family: ${MONO_STACK}; font-size: .7rem; color: var(--muted); white-space: nowrap; }
|
|
314
|
+
.group { margin: 2rem 0; content-visibility: auto; contain-intrinsic-size: 800px; }
|
|
315
|
+
.group h2 { font-size: 1.05rem; margin: 0 0 .2rem; display: flex; align-items: baseline; gap: .5rem; }
|
|
316
|
+
.group h2 .count { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); font-weight: 400; }
|
|
317
|
+
.section-note { color: var(--muted); font-size: .82rem; margin: 0 0 .8rem; max-width: 68ch; }
|
|
318
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: .7rem; }
|
|
319
|
+
.card { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: .6rem .7rem .7rem; content-visibility: auto; contain-intrinsic-size: 220px; }
|
|
320
|
+
.card[hidden] { display: none; }
|
|
321
|
+
.card-name { font-size: .92rem; margin: 0 0 .3rem; font-weight: 600; }
|
|
322
|
+
.chain { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); margin-bottom: .5rem; display: flex; flex-wrap: wrap; align-items: center; gap: .15rem; }
|
|
323
|
+
.chain-link { padding: .04rem .35rem; border: 1px solid var(--line); border-radius: 99px; }
|
|
324
|
+
.chain-link.own { border-color: var(--taught); color: var(--taught); }
|
|
325
|
+
.chain-arrow { color: var(--muted); opacity: .6; }
|
|
326
|
+
.chain-more { font-style: italic; opacity: .75; }
|
|
327
|
+
.tier-row { margin-top: .4rem; }
|
|
328
|
+
.tier-row:first-of-type { margin-top: 0; }
|
|
329
|
+
.tier-label { font-family: ${MONO_STACK}; font-size: .62rem; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
|
|
330
|
+
.swatches { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .25rem; }
|
|
331
|
+
.swatch { width: 64px; text-align: center; }
|
|
332
|
+
.swatch.large .swatch-img { width: 64px; height: 64px; }
|
|
333
|
+
.swatch.icon .swatch-img { width: 34px; height: 34px; margin: 0 auto; }
|
|
334
|
+
.swatch-img svg { width: 100%; height: 100%; display: block; }
|
|
335
|
+
.swatch.fallback { opacity: .55; }
|
|
336
|
+
.swatch.fallback .swatch-img { outline: 1px dashed var(--line); outline-offset: 2px; }
|
|
337
|
+
.swatch-caption { font-family: ${MONO_STACK}; font-size: .58rem; color: var(--muted); line-height: 1.25; margin-top: .15rem; word-break: break-word; }
|
|
338
|
+
.swatch-treat { display: block; opacity: .8; }
|
|
339
|
+
footer.page { max-width: 74ch; margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--line); font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); }
|
|
340
|
+
@media (prefers-reduced-motion: no-preference) { .jump, .swatch { transition: border-color .12s ease, opacity .12s ease; } }
|
|
341
|
+
</style>
|
|
342
|
+
</head>
|
|
343
|
+
<body>
|
|
344
|
+
<main>
|
|
345
|
+
<div class="eyebrow">tmct · the sprite library</div>
|
|
346
|
+
<h1>Every shape the sprite library can draw, and why</h1>
|
|
347
|
+
<p class="intro">Two tiers share one resolver: a 44px icon set for the live games, a 400px
|
|
348
|
+
gradient-shaded set for a closer look. Each card below shows a class's real
|
|
349
|
+
<span class="mono">rdfs:subClassOf</span> ancestor chain and every swatch the real resolver
|
|
350
|
+
(<span class="mono">resolveSpriteAsset</span>) actually returns for it — a material-bearing
|
|
351
|
+
class shows its real taught-material variants, never an invented one. Every class shown here
|
|
352
|
+
already carries its own template, so live resolution always stops at the chain's own first
|
|
353
|
+
link (marked); the fuller chain is real ancestry on record in this catalog's corpus slice, kept
|
|
354
|
+
for context.</p>
|
|
355
|
+
<div class="topbar">
|
|
356
|
+
<nav aria-label="Jump to group">${navHtml}</nav>
|
|
357
|
+
<div class="filter">
|
|
358
|
+
<input id="q" type="text" placeholder="filter by class or group…" aria-label="Filter the catalog">
|
|
359
|
+
<span class="n mono" id="qcount"></span>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
${CATALOG_GROUPS.map((g) => sectionHtml(g, entries)).join("")}
|
|
363
|
+
<footer class="page">${entries.length} classes · ${totalSwatches} swatches · icon tier 44px, sprite tier 400px</footer>
|
|
364
|
+
</main>
|
|
365
|
+
<script>
|
|
366
|
+
const SPRITE_CATALOG = ${pageData};
|
|
367
|
+
</script>
|
|
368
|
+
<script>
|
|
369
|
+
(function () {
|
|
370
|
+
"use strict";
|
|
371
|
+
const q = document.getElementById("q");
|
|
372
|
+
const qcount = document.getElementById("qcount");
|
|
373
|
+
const cards = Array.from(document.querySelectorAll(".card"));
|
|
374
|
+
function apply() {
|
|
375
|
+
const needle = q.value.trim().toLowerCase();
|
|
376
|
+
let shown = 0;
|
|
377
|
+
for (const card of cards) {
|
|
378
|
+
const hit = !needle || card.dataset.cls.includes(needle) || card.dataset.group.includes(needle);
|
|
379
|
+
card.hidden = !hit;
|
|
380
|
+
if (hit) shown += 1;
|
|
381
|
+
}
|
|
382
|
+
for (const section of document.querySelectorAll(".group")) {
|
|
383
|
+
const anyShown = section.querySelectorAll(".card:not([hidden])").length > 0;
|
|
384
|
+
section.style.display = anyShown ? "" : "none";
|
|
385
|
+
}
|
|
386
|
+
qcount.textContent = needle ? shown + " / " + cards.length : "";
|
|
387
|
+
}
|
|
388
|
+
q.addEventListener("input", apply);
|
|
389
|
+
apply();
|
|
390
|
+
})();
|
|
391
|
+
</script>
|
|
392
|
+
</body>
|
|
393
|
+
</html>
|
|
394
|
+
`;
|
|
395
|
+
}
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
37
37
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
38
38
|
import { foldWorldState, worldDigestRows, roomAffordances } from "../../services/adventure.mjs";
|
|
39
|
-
import { runAdventureAutoplayTick } from "../../services/adventure-autoplay.mjs";
|
|
39
|
+
import { runAdventureAutoplayTick, exposedFacts } from "../../services/adventure-autoplay.mjs";
|
|
40
40
|
import { resolveSpriteForClass, SPRITE_REGISTRY } from "../../domain/sprite-map.mjs";
|
|
41
41
|
import { resolveSpriteAsset } from "../../domain/sprite-templates.mjs";
|
|
42
42
|
|
|
@@ -59,10 +59,26 @@ export async function createAdventureSession(worldPayload) {
|
|
|
59
59
|
|
|
60
60
|
const planHolder = { state: { adventure: { world: worldPayload.name } } };
|
|
61
61
|
const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
|
62
|
-
|
|
62
|
+
// visitedRoomIds is the ONE exposure set both `turn()` and `autoplayTick()`
|
|
63
|
+
// grow — a deliberate merge, not two separate histories. Both entry points
|
|
64
|
+
// move the SAME player through the SAME shared memoryDir: `here` is always
|
|
65
|
+
// read fresh off the live fact store (state.placements.get("player")), not
|
|
66
|
+
// off whichever path last ran, so by the time either call finishes, the
|
|
67
|
+
// room the player is standing in is genuinely known to a visitor looking
|
|
68
|
+
// at the page regardless of which control moved them there. Keeping two
|
|
69
|
+
// separate sets would make the map/goal panels UNDER-report a room a
|
|
70
|
+
// visitor plainly just walked autoplay through (or vice versa) — a false
|
|
71
|
+
// conservatism, not real honesty, since nothing about the OTHER path's
|
|
72
|
+
// moves is hidden from this same session. runAdventureAutoplayTick's own
|
|
73
|
+
// returned `exposedRoomIds` already folds forward whatever it was handed,
|
|
74
|
+
// so feeding it this merged set on every tick, and folding its result back
|
|
75
|
+
// into the same variable, is enough: manual moves feed autoplay's own
|
|
76
|
+
// reasoning, autoplay's moves feed the panels, with no separate
|
|
77
|
+
// bookkeeping either way.
|
|
78
|
+
let visitedRoomIds = new Set();
|
|
63
79
|
const openingRows = readFactRows(await loadMemory(memoryDir));
|
|
64
80
|
const openingHere = foldWorldState(openingRows).placements.get("player")?.object ?? null;
|
|
65
|
-
if (openingHere)
|
|
81
|
+
if (openingHere) visitedRoomIds = new Set([openingHere]);
|
|
66
82
|
|
|
67
83
|
const graph = parseEntities({ individuals: [], objectProperties: [] });
|
|
68
84
|
const lexicon = loadLexicon();
|
|
@@ -78,16 +94,18 @@ export async function createAdventureSession(worldPayload) {
|
|
|
78
94
|
* `{ turn, goal, plan, done, stalled }` unmodified. */
|
|
79
95
|
async autoplayTick() {
|
|
80
96
|
const result = await runAdventureAutoplayTick(memoryDir, {
|
|
81
|
-
exposedRoomIds, planHolder, sessionId, env: {},
|
|
97
|
+
exposedRoomIds: visitedRoomIds, planHolder, sessionId, env: {},
|
|
82
98
|
});
|
|
83
|
-
|
|
99
|
+
visitedRoomIds = result.exposedRoomIds;
|
|
84
100
|
return result;
|
|
85
101
|
},
|
|
86
102
|
|
|
87
103
|
/** One dispatched chat turn — the SAME runTurn the CLI and every other
|
|
88
104
|
* viz page's own chat dock run, over this session's own memoryDir. A
|
|
89
105
|
* throwing runTurn must never kill the session — the page has no other
|
|
90
|
-
* chance to show this turn's answer.
|
|
106
|
+
* chance to show this turn's answer. Grows `visitedRoomIds` with the
|
|
107
|
+
* player's post-turn room (a no-op add when the command didn't move
|
|
108
|
+
* anyone) — the manual-play half of the merged exposure set. */
|
|
91
109
|
async turn(line) {
|
|
92
110
|
let result;
|
|
93
111
|
try {
|
|
@@ -102,25 +120,33 @@ export async function createAdventureSession(worldPayload) {
|
|
|
102
120
|
focus = result.focus;
|
|
103
121
|
last = result.last;
|
|
104
122
|
if ("planState" in result) planHolder.state = result.planState;
|
|
123
|
+
const here = foldWorldState(readFactRows(await loadMemory(memoryDir))).placements.get("player")?.object ?? null;
|
|
124
|
+
if (here) visitedRoomIds.add(here);
|
|
105
125
|
return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null, plan: result.plan ?? null };
|
|
106
126
|
},
|
|
107
127
|
|
|
108
128
|
/** A read-only fold of the current room — no engine advance — for the
|
|
109
129
|
* page's own redraw after boot, after every tick, and after every
|
|
110
|
-
* manual chat turn.
|
|
130
|
+
* manual chat turn. `visitedRoomIds` travels as a plain array (the same
|
|
131
|
+
* merged exposure set `turn()`/`autoplayTick()` both grow) so the
|
|
132
|
+
* carrying/map/goal panels can read it without holding a live
|
|
133
|
+
* reference into this closure's own Set. */
|
|
111
134
|
async snapshot() {
|
|
112
135
|
const rows = readFactRows(await loadMemory(memoryDir));
|
|
113
136
|
const state = foldWorldState(rows);
|
|
114
137
|
const here = state.placements.get("player")?.object ?? null;
|
|
115
|
-
return { rows, state, here, turn: state.turnCount };
|
|
138
|
+
return { rows, state, here, turn: state.turnCount, visitedRoomIds: [...visitedRoomIds] };
|
|
116
139
|
},
|
|
117
140
|
};
|
|
118
141
|
}
|
|
119
142
|
|
|
120
143
|
// Re-exported so the page's own rendering script (adventure-viz.mjs) never
|
|
121
|
-
// has to duplicate sprite resolution, the digest reader,
|
|
122
|
-
// affordances the chat dock's own pills read from
|
|
123
|
-
//
|
|
144
|
+
// has to duplicate sprite resolution, the digest reader, the room
|
|
145
|
+
// affordances the chat dock's own pills read from, or (foldWorldState,
|
|
146
|
+
// exposedFacts) the exposure-filtered fold the goal-status panel mirrors —
|
|
147
|
+
// the same posture spider-fly-browser-entry.mjs's own
|
|
148
|
+
// globalThis.tmctSpiderFly re-export takes.
|
|
124
149
|
globalThis.tmctAdventure = {
|
|
125
|
-
createAdventureSession, resolveSpriteForClass, SPRITE_REGISTRY, resolveSpriteAsset,
|
|
150
|
+
createAdventureSession, resolveSpriteForClass, SPRITE_REGISTRY, resolveSpriteAsset,
|
|
151
|
+
worldDigestRows, roomAffordances, foldWorldState, exposedFacts,
|
|
126
152
|
};
|