@polycode-projects/the-mechanical-code-talker 2.8.0 → 2.8.3
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 +2 -1
- package/src/adapters/corpus/sprite-large-template-files.mjs +6 -2
- package/src/domain/game-config.mjs +10 -4
- package/src/domain/hanoi-lesson.mjs +53 -0
- package/src/domain/spider-fly-world.mjs +16 -0
- package/src/domain/sprite-expressions.mjs +120 -0
- package/src/domain/sprite-templates.mjs +31 -9
- package/src/services/adventure-editor.mjs +361 -0
- package/src/services/adventure-viz.mjs +422 -51
- package/src/services/chat-page-viz.mjs +398 -0
- package/src/services/plan-pddl.mjs +245 -0
- package/src/services/plan-viz.mjs +324 -67
- package/src/services/spider-fly-turn.mjs +120 -3
- package/src/services/spider-fly-viz.mjs +341 -22
- package/src/services/spider-fly.mjs +337 -143
- package/src/services/sprite-catalog-viz.mjs +395 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +34 -3
- package/src/surfaces/web/memory-ask-browser.bundle.js +10 -4
- package/src/surfaces/web/plan-browser-entry.mjs +114 -0
- package/src/surfaces/web/spider-fly-browser-entry.mjs +33 -1
|
@@ -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
|
+
}
|
|
@@ -31,14 +31,16 @@
|
|
|
31
31
|
// like a real CLI session.
|
|
32
32
|
import { runTurn } from "../../services/chat.mjs";
|
|
33
33
|
import {
|
|
34
|
-
createInMemoryStore, appendFacts, appendRule, loadMemory, readFactRows,
|
|
34
|
+
createInMemoryStore, appendFacts, appendRule, loadMemory, readFactRows, removeFacts,
|
|
35
35
|
} from "../../adapters/memory/core.mjs";
|
|
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
39
|
import { runAdventureAutoplayTick, exposedFacts } from "../../services/adventure-autoplay.mjs";
|
|
40
|
-
import {
|
|
40
|
+
import { parseWorldEditorText, planWorldEditorSync } from "../../services/adventure-editor.mjs";
|
|
41
|
+
import { resolveSpriteForClass, SPRITE_REGISTRY, classAncestorChain } from "../../domain/sprite-map.mjs";
|
|
41
42
|
import { resolveSpriteAsset } from "../../domain/sprite-templates.mjs";
|
|
43
|
+
import { relatedForTerm } from "../../domain/skos-view.mjs";
|
|
42
44
|
|
|
43
45
|
/** A live in-memory adventure this page's ticker AND chat dock can both
|
|
44
46
|
* drive. Returns `{ memoryDir, autoplayTick, turn, snapshot }`.
|
|
@@ -137,6 +139,30 @@ export async function createAdventureSession(worldPayload) {
|
|
|
137
139
|
const here = state.placements.get("player")?.object ?? null;
|
|
138
140
|
return { rows, state, here, turn: state.turnCount, visitedRoomIds: [...visitedRoomIds] };
|
|
139
141
|
},
|
|
142
|
+
|
|
143
|
+
/** The world editor's own store sync: parse `text` (adventure-editor.mjs's
|
|
144
|
+
* own parseWorldEditorText), plan the implied writes (planWorldEditorSync),
|
|
145
|
+
* and apply them — scoped to THIS world's own provenance tag only, never
|
|
146
|
+
* the default persona's background corpus that shares the same live
|
|
147
|
+
* memory store (an unscoped diff would read every unrelated background
|
|
148
|
+
* fact as "not in this text" and try to retract it). Retractions
|
|
149
|
+
* (removeFacts) only ever run when the WHOLE document parsed cleanly —
|
|
150
|
+
* see adventure-editor.mjs's own header for why a typo must never be
|
|
151
|
+
* read as "this fact is gone". Returns `{ unrecognized, added, removed }`. */
|
|
152
|
+
async applyEdit(text) {
|
|
153
|
+
const allRows = readFactRows(await loadMemory(memoryDir));
|
|
154
|
+
const worldRows = allRows.filter((r) => typeof r.provenance === "string" && r.provenance.indexOf(tag) === 0);
|
|
155
|
+
const state = foldWorldState(worldRows);
|
|
156
|
+
const { triples, unrecognized } = parseWorldEditorText(text, worldRows);
|
|
157
|
+
const { toAppend, toRemoveIds } = planWorldEditorSync(worldRows, state, triples);
|
|
158
|
+
if (toAppend.length) {
|
|
159
|
+
await appendFacts(memoryDir, toAppend.map((f) => ({ subject: f.subject, predicate: f.predicate, object: f.object, provenance: tag })));
|
|
160
|
+
}
|
|
161
|
+
const removedCount = unrecognized.length === 0 && toRemoveIds.length ? (await removeFacts(memoryDir, toRemoveIds)).removed.length : 0;
|
|
162
|
+
const here = foldWorldState(readFactRows(await loadMemory(memoryDir))).placements.get("player")?.object ?? null;
|
|
163
|
+
if (here) visitedRoomIds.add(here);
|
|
164
|
+
return { unrecognized, added: toAppend.length, removed: removedCount };
|
|
165
|
+
},
|
|
140
166
|
};
|
|
141
167
|
}
|
|
142
168
|
|
|
@@ -145,8 +171,13 @@ export async function createAdventureSession(worldPayload) {
|
|
|
145
171
|
// affordances the chat dock's own pills read from, or (foldWorldState,
|
|
146
172
|
// exposedFacts) the exposure-filtered fold the goal-status panel mirrors —
|
|
147
173
|
// the same posture spider-fly-browser-entry.mjs's own
|
|
148
|
-
// globalThis.tmctSpiderFly re-export takes.
|
|
174
|
+
// globalThis.tmctSpiderFly re-export takes. `relatedForTerm`/
|
|
175
|
+
// `classAncestorChain` back the edit mode's own cursor-suggestion pills
|
|
176
|
+
// (adventure-viz.mjs's suggestionsForTerm mirrors this same pairing against
|
|
177
|
+
// the global, the same reach-through-the-global pattern captionFor/pillsFor
|
|
178
|
+
// already use for their own adventure.mjs calls).
|
|
149
179
|
globalThis.tmctAdventure = {
|
|
150
180
|
createAdventureSession, resolveSpriteForClass, SPRITE_REGISTRY, resolveSpriteAsset,
|
|
151
181
|
worldDigestRows, roomAffordances, foldWorldState, exposedFacts,
|
|
182
|
+
relatedForTerm, classAncestorChain,
|
|
152
183
|
};
|
|
@@ -23594,10 +23594,13 @@ ${JSON.stringify(envelope, null, 2)}`;
|
|
|
23594
23594
|
spiderMassDecrementPerTurn: 0.5,
|
|
23595
23595
|
flyInitialMass: 10,
|
|
23596
23596
|
flyMassDecrementPerTurn: 1,
|
|
23597
|
-
|
|
23597
|
+
spiderVisionRadius: 4,
|
|
23598
|
+
flyVisionRadius: 4,
|
|
23598
23599
|
eggHatchDelayTurns: 3,
|
|
23599
23600
|
flySpawnIntervalTurns: 3,
|
|
23600
|
-
|
|
23601
|
+
eggLayMassThreshold: 25,
|
|
23602
|
+
eggHatchCount: 2,
|
|
23603
|
+
minHatchlingMass: 3,
|
|
23601
23604
|
webDurationTurns: 10
|
|
23602
23605
|
}),
|
|
23603
23606
|
guessNumber: Object.freeze({
|
|
@@ -23614,10 +23617,13 @@ ${JSON.stringify(envelope, null, 2)}`;
|
|
|
23614
23617
|
spider_mass_decrement_per_turn: "spiderMassDecrementPerTurn",
|
|
23615
23618
|
fly_initial_mass: "flyInitialMass",
|
|
23616
23619
|
fly_mass_decrement_per_turn: "flyMassDecrementPerTurn",
|
|
23617
|
-
|
|
23620
|
+
spider_vision_radius: "spiderVisionRadius",
|
|
23621
|
+
fly_vision_radius: "flyVisionRadius",
|
|
23618
23622
|
egg_hatch_delay_turns: "eggHatchDelayTurns",
|
|
23619
23623
|
fly_spawn_interval_turns: "flySpawnIntervalTurns",
|
|
23620
|
-
|
|
23624
|
+
egg_lay_mass_threshold: "eggLayMassThreshold",
|
|
23625
|
+
egg_hatch_count: "eggHatchCount",
|
|
23626
|
+
min_hatchling_mass: "minHatchlingMass",
|
|
23621
23627
|
web_duration_turns: "webDurationTurns"
|
|
23622
23628
|
});
|
|
23623
23629
|
var GUESS_NUMBER_KEY_MAP = Object.freeze({
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// plan-browser-entry.mjs — the esbuild entry for the "it plans, and shows
|
|
2
|
+
// the work" page's live session (public/plan-browser.bundle.js, built by
|
|
3
|
+
// scripts/build-plan-bundle.mjs), mirroring spider-fly-browser-entry.mjs's
|
|
4
|
+
// and adventure-browser-entry.mjs's own session-factory shape.
|
|
5
|
+
//
|
|
6
|
+
// Unlike spider-fly's board or Ashcombe Hall's world, hanoi has no
|
|
7
|
+
// structured fact/rule corpus to bootstrap from — its canonical definition
|
|
8
|
+
// (data/games/hanoi-3.txt) IS taught English, one sentence per teach frame.
|
|
9
|
+
// So this session seeds itself the same way `tmct import --file` teaches
|
|
10
|
+
// that file (src/services/import-file.mjs): every sentence
|
|
11
|
+
// hanoi-lesson.mjs's hanoiLessonSentences() generates runs as its own
|
|
12
|
+
// `turn()`, over the exact same runTurn the CLI and every other viz page's
|
|
13
|
+
// chat dock run — not raw appendFacts/appendRule, since there is no
|
|
14
|
+
// structured fact list to append here, only taught English.
|
|
15
|
+
//
|
|
16
|
+
// createPlanSession({ diskCount, maxDepth }) teaches a fresh N-disk puzzle
|
|
17
|
+
// and solves it once (mirroring the "disk-1 rests on disk-2. … the goal is
|
|
18
|
+
// that every disk rests on peg-c. solve it." prompt scripts/build-demo-
|
|
19
|
+
// site.mjs used to shell out to the CLI for), returning `{ plan, turn, ... }`
|
|
20
|
+
// — `plan` is the freshly solved plan (or null on an honest miss, e.g. a
|
|
21
|
+
// max-depth too low to find one), `turn(line, { maxDepth })` is the SAME
|
|
22
|
+
// chat-dock entry point adventure/spider-fly expose, so a visitor's typed
|
|
23
|
+
// fact and a visitor's typed "solve it" both dispatch through the real
|
|
24
|
+
// engine. `maxDepth` is overridable PER CALL (not just at session creation)
|
|
25
|
+
// so the page's own max-search-depth control can re-run "solve it" on the
|
|
26
|
+
// CURRENT board without tearing down and re-teaching the whole puzzle.
|
|
27
|
+
import { runTurn } from "../../services/chat.mjs";
|
|
28
|
+
import { createInMemoryStore } from "../../adapters/memory/core.mjs";
|
|
29
|
+
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
30
|
+
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
31
|
+
import { DEFAULT_GAME_CONFIG } from "../../domain/game-config.mjs";
|
|
32
|
+
import { hanoiLessonSentences } from "../../domain/hanoi-lesson.mjs";
|
|
33
|
+
import { computeBlocksLayout, planToPageData, renderInputsFromPlan } from "../../services/plan-viz.mjs";
|
|
34
|
+
import { planToPddl } from "../../services/plan-pddl.mjs";
|
|
35
|
+
// Re-exported so the page can register a CDN-loaded wink-nlp pair before the
|
|
36
|
+
// first teach, the same seam chat-browser-entry.mjs exposes as
|
|
37
|
+
// tmctChat.registerWinkModel — see wink-model.mjs's own header. The hanoi
|
|
38
|
+
// lesson's own "moving a disk onto a target makes the disk rest on the
|
|
39
|
+
// target" sentence needs a REAL lemmatiser (verbLemma reduces "moving" to
|
|
40
|
+
// "move" to match the taught "move onto" action family) — without it, that
|
|
41
|
+
// one sentence honestly declines ("the lemmatizer isn't available"), the
|
|
42
|
+
// action rule never gets its effect, and every later locative fact for the
|
|
43
|
+
// puzzle fails to teach in turn. spider-fly/adventure never register a wink
|
|
44
|
+
// model because their own gameplay never asks a taught rule to reduce a
|
|
45
|
+
// verb; the hanoi lesson is the first live session here that does.
|
|
46
|
+
import { registerWinkModel } from "../../adapters/wink-model.mjs";
|
|
47
|
+
|
|
48
|
+
/** A live in-memory towers-of-hanoi session this page's live controls AND
|
|
49
|
+
* chat dock can both drive. Returns `{ memoryDir, sessionId, diskCount,
|
|
50
|
+
* maxDepth, plan, turn }`. `plan` is the puzzle's freshly solved plan (the
|
|
51
|
+
* same shape chat.mjs's planLaneAnswer returns, enriched with
|
|
52
|
+
* `becauseText` — see `turn()` below), or null when `maxDepth` was too low
|
|
53
|
+
* to find one (an honest miss, not an error: `turn()`'s own answer text
|
|
54
|
+
* names it). */
|
|
55
|
+
export async function createPlanSession({ diskCount = 3, maxDepth = DEFAULT_GAME_CONFIG.planning.maxDepth } = {}) {
|
|
56
|
+
const memoryDir = createInMemoryStore();
|
|
57
|
+
const graph = parseEntities({ individuals: [], objectProperties: [] });
|
|
58
|
+
const lexicon = loadLexicon();
|
|
59
|
+
const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
|
60
|
+
const planHolder = { state: null };
|
|
61
|
+
let focus = null;
|
|
62
|
+
let last = null;
|
|
63
|
+
|
|
64
|
+
/** One dispatched chat turn — the SAME runTurn the CLI and every other
|
|
65
|
+
* viz page's own chat dock run, over this session's own memoryDir. A
|
|
66
|
+
* throwing runTurn must never kill the session — the page has no other
|
|
67
|
+
* chance to show this turn's answer. `maxDepth` overrides the session's
|
|
68
|
+
* own default for just this one call (the page's own max-search-depth
|
|
69
|
+
* control threads it on every call, including a plain typed "solve
|
|
70
|
+
* it"), so raising or lowering it never requires re-teaching the board.
|
|
71
|
+
* A returned `plan` carries `becauseText` folded in from the session's
|
|
72
|
+
* own plan slot — the plan-lane contract's returned object never carries
|
|
73
|
+
* it itself (only planHolder.state does), and the PDDL panel's own
|
|
74
|
+
* "because —" line needs it. */
|
|
75
|
+
async function turn(line, { maxDepth: maxDepthOverride } = {}) {
|
|
76
|
+
const gameConfig = {
|
|
77
|
+
...DEFAULT_GAME_CONFIG,
|
|
78
|
+
planning: { ...DEFAULT_GAME_CONFIG.planning, maxDepth: maxDepthOverride ?? maxDepth },
|
|
79
|
+
};
|
|
80
|
+
let result;
|
|
81
|
+
try {
|
|
82
|
+
result = await runTurn(line, {
|
|
83
|
+
config: null, source: null, graph, focus, last, memoryDir, sessionId,
|
|
84
|
+
env: {}, lexicon, vocabHint: "", planState: planHolder.state, gameConfig,
|
|
85
|
+
});
|
|
86
|
+
} catch (e) {
|
|
87
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
88
|
+
return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, record: null, plan: null };
|
|
89
|
+
}
|
|
90
|
+
focus = result.focus;
|
|
91
|
+
last = result.last;
|
|
92
|
+
if ("planState" in result) planHolder.state = result.planState;
|
|
93
|
+
const plan = result.plan
|
|
94
|
+
? { ...result.plan, becauseText: planHolder.state?.becauseText ?? null }
|
|
95
|
+
: null;
|
|
96
|
+
return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null, plan };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let plan = null;
|
|
100
|
+
for (const sentence of hanoiLessonSentences(diskCount)) {
|
|
101
|
+
const r = await turn(sentence);
|
|
102
|
+
if (r.plan) plan = r.plan;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { memoryDir, sessionId, diskCount, maxDepth, plan, turn };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Re-exported so the page's own rendering script (plan-viz.mjs's inlined
|
|
109
|
+
// script) never has to duplicate board layout or PDDL/OWL-RDF formatting —
|
|
110
|
+
// the same posture adventure-browser-entry.mjs/spider-fly-browser-entry.mjs
|
|
111
|
+
// take re-exporting their own engines' pure helpers.
|
|
112
|
+
globalThis.tmctPlan = {
|
|
113
|
+
createPlanSession, computeBlocksLayout, planToPageData, renderInputsFromPlan, planToPddl, registerWinkModel,
|
|
114
|
+
};
|