@polycode-projects/the-mechanical-code-talker 2.7.12 → 2.7.14

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.
@@ -0,0 +1,262 @@
1
+ // adventure-viz.mjs — the adventure's own full-screen/home-page hero
2
+ // (PLAN_GAMES_UPLIFT_V2.md Part B), styled directly after
3
+ // spider-fly-viz.mjs's own self-contained page-builder: one inlined <style>
4
+ // importing viz-theme.mjs's shared tokens, behaviour as an inlined IIFE, the
5
+ // shared `createTicker` primitive spliced in via `.toString()` exactly the
6
+ // way that page splices its own render-glue helpers.
7
+ //
8
+ // Where this page's data-loading DIVERGES from spider-fly's, on purpose:
9
+ // spider-fly-world.mjs is itself a plain, dependency-free JS module (no I/O),
10
+ // so its browser entry can call it directly to bootstrap a board. Ashcombe
11
+ // Hall's canonical definition is a JSONL corpus source
12
+ // (corpus/worlds/src/ashcombe-hall.jsonl), read through a Node fs/gzip
13
+ // provider the browser cannot run. Rather than hand-duplicating the world as
14
+ // a second, hardcoded JS copy (exactly the kind of drift this project's
15
+ // worlds-pack build step exists to prevent), the real facts+rules are read
16
+ // ONCE at build time (scripts/build-demo-site.mjs, the same Node path
17
+ // test/services/adventure.test.mjs's own loadShippedWorldInto uses) and
18
+ // embedded into this page as plain JSON — the same posture ledger.html
19
+ // already takes with its own precomputed memory payload, just applied to a
20
+ // second kind of build-time data.
21
+ //
22
+ // Two pure, `.toString()`-splice-safe pieces are exported as real functions
23
+ // (not raw inline-script text) so they can be pinned directly by tests, the
24
+ // same discipline spider-fly-viz.mjs holds classOfAgentId/
25
+ // threadCellsForSpiderPlan to: `spriteClassForObject` (an object's sprite
26
+ // class, from its own rdf:type or mgx:is-container fact — NOT from
27
+ // adventure.mjs's private isContainer/isTyped, which this module cannot
28
+ // import without duplicating adventure.mjs's own closed vocabulary reading)
29
+ // and `roomSceneObjects` (every subject actually visible in a room, mirrored
30
+ // from adventure.mjs's private `visibleRoomOf` the same way
31
+ // adventure-autoplay.mjs's own `roomOfSubject` already has to). `roomCaptionText`
32
+ // is a third pure helper, exported for testing, but NOT spliced — it calls
33
+ // `worldDigestRows` via a real ES import, since Node/test callers have one,
34
+ // while the in-page script instead calls the browser bundle's own exposed
35
+ // copy (mirroring how the inline script calls `tmctSpiderFly.*` rather than
36
+ // re-importing spider-fly-world.mjs).
37
+ import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
38
+ import { createTicker } from "./viz-ticker.mjs";
39
+ import { worldDigestRows } from "./adventure.mjs";
40
+
41
+ const DEFAULT_TITLE = "tmct — the adventure";
42
+ const PREVIEW_MAX_TICKS = 30;
43
+ const TICK_WAIT_MS = 900;
44
+
45
+ /** An object's sprite class: `container` when it carries mgx:is-container
46
+ * (a distinct icon from plain furniture, since Ashcombe's own cabinet and
47
+ * portrait are typed "furniture" but read more clearly as a container on
48
+ * screen), else its own rdf:type object, else the generic "portable"
49
+ * fallback for anything a world places with no type fact at all. Pure,
50
+ * self-contained — no reference to adventure.mjs's own private isContainer/
51
+ * isTyped, since those aren't exported. */
52
+ export function spriteClassForObject(rows, subject) {
53
+ const isContainer = (rows || []).some(
54
+ (r) => r.subject === subject && r.predicate === "mgx:is-container" && r.object === "true",
55
+ );
56
+ if (isContainer) return "container";
57
+ const typeRow = (rows || []).find((r) => r.subject === subject && r.predicate === "rdf:type");
58
+ return typeRow ? typeRow.object : "portable";
59
+ }
60
+
61
+ /** Every subject actually visible in `here`, sorted, each with its sprite
62
+ * class — mirroring adventure.mjs's own private `visibleRoomOf` walk (one
63
+ * containment hop through an OPEN container) so this can never draw a
64
+ * hidden or carried object the text digest wouldn't also mention. `player`
65
+ * is excluded; the caller draws the player's own adventurer sprite
66
+ * separately. Pure. */
67
+ export function roomSceneObjects(rows, state, here) {
68
+ const isTypedRoom = (subject) =>
69
+ (rows || []).some((r) => r.subject === subject && r.predicate === "rdf:type" && r.object === "room");
70
+ const visibleRoomOf = (subject) => {
71
+ const place = state.placements.get(subject);
72
+ if (!place || place.predicate === "mgx:hidden-in") return null;
73
+ if (place.predicate === "mgx:currently-in" || isTypedRoom(place.object)) return place.object;
74
+ const holder = place.object;
75
+ if (holder === "player") return null;
76
+ if (!state.openness.get(holder)?.open) return null;
77
+ const holderPlace = state.placements.get(holder);
78
+ return holderPlace && holderPlace.predicate !== "mgx:hidden-in" ? holderPlace.object : null;
79
+ };
80
+ const out = [];
81
+ for (const subject of [...state.placements.keys()].sort()) {
82
+ if (subject === "player") continue;
83
+ if (visibleRoomOf(subject) !== here) continue;
84
+ out.push({ subject, spriteClass: spriteClassForObject(rows, subject) });
85
+ }
86
+ return out;
87
+ }
88
+
89
+ /** A short caption for `here`, built ONLY from the rows worldDigestRows
90
+ * itself already produces (the exact same view the chat reply's own digest
91
+ * reads) — every row already reads as a plain sentence
92
+ * (`${subject} ${predicate} ${object}.`), so this never invents a phrase
93
+ * the text digest doesn't already carry. Filters to rows about the room
94
+ * itself (its own exits) or about something placed IN it — the same
95
+ * "visible here" boundary `roomSceneObjects` draws from. The player's own
96
+ * "is in the" row is excluded: the room frame already IS the current room,
97
+ * so restating "you are here" is redundant, never informative. */
98
+ export function roomCaptionText(rows, state, here) {
99
+ const hereCased = here.charAt(0).toUpperCase() + here.slice(1);
100
+ const lines = worldDigestRows(rows, state)
101
+ .filter((row) => row.subject !== "Player" && (row.object === here || row.subject === hereCased))
102
+ .map((row) => `${row.subject} ${row.predicate} ${row.object}.`);
103
+ return lines.length ? lines.join(" ") : `Nothing more about the ${here} is written down yet.`;
104
+ }
105
+
106
+ /** The self-contained adventure page. Pure given `worldPayload` (the build
107
+ * step's own read of the real Ashcombe Hall world — `{ facts, rules,
108
+ * opening }`), the same "byte-identical for identical input" invariant
109
+ * every other viz page in this project holds. `?preview=1` switches into
110
+ * the small, auto-playing, non-interactive mode the home page's hero iframe
111
+ * embeds, matching spider-fly.html's own dual-purpose file. */
112
+ export function renderAdventureHtml({ title = DEFAULT_TITLE, worldPayload = { facts: [], rules: [], opening: "" } } = {}) {
113
+ const pageData = embedJson({
114
+ world: worldPayload,
115
+ previewMaxTicks: PREVIEW_MAX_TICKS,
116
+ tickWaitMs: TICK_WAIT_MS,
117
+ });
118
+
119
+ return `<!doctype html>
120
+ <html lang="en">
121
+ <head>
122
+ <meta charset="utf-8">
123
+ <meta name="viewport" content="width=device-width, initial-scale=1">
124
+ <title>${escapeHtml(title)}</title>
125
+ <style>
126
+ ${THEME_TOKENS_CSS}
127
+ html { background: var(--bg); }
128
+ body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
129
+ .mono { font-family: ${MONO_STACK}; }
130
+ main { max-width: 860px; margin: 0 auto; padding: 1.4rem 1.2rem 2.2rem; }
131
+ .eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
132
+ h1 { font-size: 1.4rem; margin: .3rem 0 .9rem; text-wrap: balance; }
133
+ button { font: inherit; color: inherit; background: none; cursor: pointer; }
134
+ button:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
135
+ .room-frame { position: relative; min-height: 210px; background: var(--taught-soft); border: 1px solid var(--line); padding: 1rem; display: flex; flex-direction: column; gap: .8rem; justify-content: flex-end; }
136
+ .sprite-row { display: flex; flex-wrap: wrap; gap: .6rem; align-items: flex-end; }
137
+ .sprite { width: 44px; height: 44px; }
138
+ .sprite svg { width: 100%; height: 100%; display: block; }
139
+ .sprite[data-cls="adventurer"] { color: var(--taught); }
140
+ .sprite[data-cls="person"] { color: var(--corpus); }
141
+ .sprite[data-cls="container"], .sprite[data-cls="furniture"] { color: var(--entail); }
142
+ .sprite[data-cls="portable"] { color: var(--alert); }
143
+ .sprite[data-cls="room"] { color: var(--muted); }
144
+ .sprite-label { font-family: ${MONO_STACK}; font-size: .62rem; text-align: center; color: var(--muted); margin-top: .15rem; }
145
+ .caption { background: var(--card); border: 1px solid var(--line); padding: .6rem .75rem; font-size: .9rem; }
146
+ .controls-row { display: flex; align-items: center; gap: .6rem; margin-top: 1rem; flex-wrap: wrap; }
147
+ .controls-row button { font-family: ${MONO_STACK}; font-size: .78rem; padding: .3rem .7rem; border: 1px solid var(--line); background: var(--card); color: var(--ink); }
148
+ .controls-row button:hover:not(:disabled) { border-color: var(--taught); }
149
+ .controls-row button:disabled { opacity: .4; cursor: default; }
150
+ .controls-row .turn { margin-left: auto; font-family: ${MONO_STACK}; font-size: .78rem; color: var(--muted); font-variant-numeric: tabular-nums; }
151
+ .goal-line { font-family: ${MONO_STACK}; font-size: .78rem; color: var(--muted); margin-top: .5rem; }
152
+ .status { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); margin-top: .3rem; }
153
+ body.preview .controls-row, body.preview .status { display: none; }
154
+ body.preview main { padding: 0; max-width: none; }
155
+ body.preview .eyebrow, body.preview h1 { display: none; }
156
+ </style>
157
+ </head>
158
+ <body>
159
+ <main>
160
+ <div class="eyebrow">tmct &middot; the adventure</div>
161
+ <h1>A room, drawn from exactly what the text already says is there</h1>
162
+ <div class="room-frame" id="roomFrame">
163
+ <div class="sprite-row" id="spriteRow"></div>
164
+ </div>
165
+ <div class="caption" id="caption"></div>
166
+ <div class="goal-line" id="goalLine"></div>
167
+ <div class="controls-row">
168
+ <button id="resetBtn" type="button" disabled>reset</button>
169
+ <button id="playBtn" type="button" disabled>&#9654; play</button>
170
+ <button id="stepBtn" type="button" disabled>step</button>
171
+ <span class="turn mono" id="turnLabel">turn: 0</span>
172
+ </div>
173
+ <div class="status" id="status">loading the engine&hellip;</div>
174
+ </main>
175
+ <script>
176
+ const ADVENTURE = ${pageData};
177
+ </script>
178
+ <script src="./adventure-browser.bundle.js"></script>
179
+ <script>
180
+ (function () {
181
+ "use strict";
182
+ const createTicker = ${createTicker.toString()};
183
+ const spriteClassForObject = ${spriteClassForObject.toString()};
184
+ const roomSceneObjects = ${roomSceneObjects.toString()};
185
+ const esc = ${escapeHtml.toString()};
186
+ const el = (id) => document.getElementById(id);
187
+ const spriteRow = el("spriteRow");
188
+ const captionEl = el("caption");
189
+ const goalLineEl = el("goalLine");
190
+ const statusEl = el("status");
191
+ const turnLabelEl = el("turnLabel");
192
+ const resetBtn = el("resetBtn");
193
+ const playBtn = el("playBtn");
194
+ const stepBtn = el("stepBtn");
195
+
196
+ const params = new URLSearchParams(location.search);
197
+ const preview = params.get("preview") === "1";
198
+ document.body.classList.toggle("preview", preview);
199
+
200
+ let session = null;
201
+ let lastTicks = 0;
202
+
203
+ function captionFor(rows, state, here) {
204
+ const hereCased = here.charAt(0).toUpperCase() + here.slice(1);
205
+ const lines = tmctAdventure.worldDigestRows(rows, state)
206
+ .filter((row) => row.subject !== "Player" && (row.object === here || row.subject === hereCased))
207
+ .map((row) => row.subject + " " + row.predicate + " " + row.object + ".");
208
+ return lines.length ? lines.join(" ") : "Nothing more about the " + here + " is written down yet.";
209
+ }
210
+
211
+ function redraw(snap) {
212
+ const objects = roomSceneObjects(snap.rows, snap.state, snap.here);
213
+ const sprites = [{ subject: "you", spriteClass: "adventurer" }, ...objects];
214
+ spriteRow.innerHTML = sprites.map((s) => {
215
+ const svg = tmctAdventure.resolveSpriteForClass(s.spriteClass, [], tmctAdventure.SPRITE_REGISTRY);
216
+ return '<div><div class="sprite" data-cls="' + esc(s.spriteClass) + '">' + svg + '</div>'
217
+ + '<div class="sprite-label">' + esc(s.subject) + "</div></div>";
218
+ }).join("");
219
+ captionEl.textContent = captionFor(snap.rows, snap.state, snap.here);
220
+ turnLabelEl.textContent = "turn: " + snap.turn;
221
+ }
222
+
223
+ async function boot() {
224
+ session = await tmctAdventure.createAdventureSession(ADVENTURE.world);
225
+ lastTicks = 0;
226
+ const snap = await session.snapshot();
227
+ redraw(snap);
228
+ goalLineEl.textContent = "";
229
+ statusEl.textContent = ADVENTURE.world.opening || "";
230
+ resetBtn.disabled = false; playBtn.disabled = false; stepBtn.disabled = false;
231
+ }
232
+
233
+ const ticker = createTicker({
234
+ onTick: async () => {
235
+ const result = await session.autoplayTick();
236
+ lastTicks += 1;
237
+ const snap = await session.snapshot();
238
+ redraw(snap);
239
+ goalLineEl.textContent = result.goal || "";
240
+ if (result.done || result.stalled) ticker.pause();
241
+ },
242
+ onRender: (state) => {
243
+ playBtn.textContent = state.playing ? "\\u23f8 pause" : "\\u25b6 play";
244
+ playBtn.disabled = state.animating;
245
+ stepBtn.disabled = state.animating || state.playing;
246
+ resetBtn.disabled = state.animating;
247
+ },
248
+ onReset: () => boot(),
249
+ hasNext: () => !preview || lastTicks < ADVENTURE.previewMaxTicks,
250
+ waitMs: ADVENTURE.tickWaitMs,
251
+ });
252
+ playBtn.addEventListener("click", () => ticker.play());
253
+ stepBtn.addEventListener("click", () => ticker.stepOnce());
254
+ resetBtn.addEventListener("click", () => ticker.reset());
255
+
256
+ boot().then(() => { if (preview) ticker.play(); });
257
+ })();
258
+ </script>
259
+ </body>
260
+ </html>
261
+ `;
262
+ }