@polycode-projects/the-mechanical-code-talker 2.7.11 → 2.7.13
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/corpus/worlds/manifest.json +6 -6
- package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
- package/corpus/worlds/src/ashcombe-hall.jsonl +6 -0
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +42 -6
- package/src/adapters/memory/shacl.mjs +8 -6
- package/src/domain/domain.mjs +51 -6
- package/src/domain/spider-fly-world.mjs +13 -0
- package/src/domain/sprite-map.mjs +49 -1
- package/src/services/adventure-autoplay.mjs +192 -0
- package/src/services/adventure-viz.mjs +262 -0
- package/src/services/adventure.mjs +103 -35
- package/src/services/chat.mjs +19 -1
- package/src/services/spider-fly-viz.mjs +39 -9
- package/src/services/spider-fly.mjs +236 -68
- package/src/surfaces/web/adventure-browser-entry.mjs +88 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +41 -11
- package/src/surfaces/web/spider-fly-browser-entry.mjs +10 -8
|
@@ -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 · 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>▶ 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…</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
|
+
}
|
|
@@ -12,6 +12,7 @@ import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
|
|
|
12
12
|
import { parseImperative } from "../domain/grammar/ace.mjs";
|
|
13
13
|
import { createCompletionsGraphAdapter } from "../domain/completions/graph-adapter.mjs";
|
|
14
14
|
import { actionFamilies } from "../domain/router/taught.mjs";
|
|
15
|
+
import { compileDomain, precondHolds, roleBinding } from "../domain/domain.mjs";
|
|
15
16
|
import { getWorldsPackProvider } from "../adapters/corpus/worlds-pack.mjs";
|
|
16
17
|
import { appendFacts, appendRule, loadMemory, normFactTerm, readFactRows, readRuleRows } from "../adapters/memory/core.mjs";
|
|
17
18
|
import { COMPLETIONS_STORE, generateCompletion } from "./completions.mjs";
|
|
@@ -78,7 +79,12 @@ async function openAdventure(opening, { planHolder, memoryDir, sessionId, env, c
|
|
|
78
79
|
world = names[0];
|
|
79
80
|
} else if (!names.includes(world)) {
|
|
80
81
|
// An unknown "play X" is not necessarily an adventure ask at all ("play
|
|
81
|
-
//
|
|
82
|
+
// spider" is spider-fly's own opener, not a broken adventure request) —
|
|
83
|
+
// fall through so a sibling game's opener keeps first refusal.
|
|
84
|
+
// unclaimedAdventureOpening below is chat's LAST-RESORT check: once
|
|
85
|
+
// every "play X" lane (this one included) has had its turn and none
|
|
86
|
+
// claimed the line, THAT'S when an unrecognized name gets named and
|
|
87
|
+
// declined honestly, never silently.
|
|
82
88
|
return null;
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -125,6 +131,31 @@ async function openAdventure(opening, { planHolder, memoryDir, sessionId, env, c
|
|
|
125
131
|
};
|
|
126
132
|
}
|
|
127
133
|
|
|
134
|
+
/** chat's LAST-RESORT check for a named opener no lane claimed — "play
|
|
135
|
+
* atlantis" when the pack has worlds but none is called that. Called AFTER
|
|
136
|
+
* every other "play X"-shaped lane (spider-fly's own opener among them) has
|
|
137
|
+
* already had its chance, so this never steals a name a sibling game
|
|
138
|
+
* recognizes as its own (openAdventure's own fallthrough above stays
|
|
139
|
+
* silent for exactly that reason). Only once nothing else wanted the line
|
|
140
|
+
* does it get named and declined, rather than answered with an unrelated
|
|
141
|
+
* generic non-answer. Null when the line isn't a named opener at all, or
|
|
142
|
+
* the pack has no worlds (openAdventure's own first pass already gave that
|
|
143
|
+
* case its honest missingPackAnswer, before any lane got a turn), or the
|
|
144
|
+
* name IS one of the pack's — never re-decides a real hit. */
|
|
145
|
+
export async function unclaimedAdventureOpening(line, { env }) {
|
|
146
|
+
const opening = matchAdventureOpening(line);
|
|
147
|
+
if (!opening?.world) return null;
|
|
148
|
+
const provider = getWorldsPackProvider(env);
|
|
149
|
+
let names = null;
|
|
150
|
+
try { names = await provider.list(); } catch { names = null; }
|
|
151
|
+
if (!names || !names.length || names.includes(opening.world)) return null;
|
|
152
|
+
return {
|
|
153
|
+
text: `I don't know a world called "${spokenNameOf(opening.world)}" — the pack has: ${names.map(spokenNameOf).join(", ")}.`,
|
|
154
|
+
lane: "game-inform",
|
|
155
|
+
note: `ADVENTURE — last-resort opening decline: "${opening.world}" names no world in the pack (has: ${names.join(", ")}), and no other lane claimed the line either`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
128
159
|
/** A resumed game's current room: non-null only when earlier @turnN
|
|
129
160
|
* snapshots exist (the world was already played in this store), folded the
|
|
130
161
|
* same way every other reader folds them. A fresh world returns null and
|
|
@@ -264,8 +295,10 @@ const affordanceSuffix = (actions) => (actions.length ? ` You can: ${actions.joi
|
|
|
264
295
|
|
|
265
296
|
/** The effect predicate a family writes (its action-effect row's slot, with
|
|
266
297
|
* the mgx: prefix rule readers re-attach). Null when the family carries no
|
|
267
|
-
* effect row —
|
|
268
|
-
*
|
|
298
|
+
* effect row — unlock's family stays signature-only (its instrument match
|
|
299
|
+
* needs a third, externally-supplied binding no shipped rule shape covers)
|
|
300
|
+
* and rides the hand-written logic below; go/take/drop/give/open/close all
|
|
301
|
+
* carry real effect rows and never hit this null. */
|
|
269
302
|
function familyEffectPredicate(family) {
|
|
270
303
|
const effect = (family || []).find((r) => r.kind === "action-effect");
|
|
271
304
|
if (!effect?.slots?.predicate) return null;
|
|
@@ -449,6 +482,24 @@ function containerStatusPhrase(object, { state }) {
|
|
|
449
482
|
: `the ${object} is open. It's empty.`;
|
|
450
483
|
}
|
|
451
484
|
|
|
485
|
+
/** `object`'s own datatype facts (its placement predicate, its open/closed
|
|
486
|
+
* flag), as the tiny {subject,predicate,object} row set a "fact-value"
|
|
487
|
+
* precond needs — read from the already-folded CURRENT truth (state.
|
|
488
|
+
* placements/state.openness), never raw @turnN rows, so a superseded
|
|
489
|
+
* snapshot can never look current. domain.mjs's own stateFromFacts can't
|
|
490
|
+
* serve this: it keys "current" off an @stepN suffix (this world writes
|
|
491
|
+
* @turnN) and restricts rows to individuals typed into some action's
|
|
492
|
+
* SUBJECT class, which "furniture" (the container's own class) never is —
|
|
493
|
+
* a container's own facts about itself would silently vanish through it. */
|
|
494
|
+
function containerDatatypeState(state, object) {
|
|
495
|
+
const rows = [];
|
|
496
|
+
const place = state.placements.get(object);
|
|
497
|
+
if (place) rows.push({ subject: object, predicate: place.predicate, object: place.object });
|
|
498
|
+
const openness = state.openness.get(object);
|
|
499
|
+
if (openness) rows.push({ subject: object, predicate: OPEN_PREDICATE, object: openness.open ? "true" : "false" });
|
|
500
|
+
return rows;
|
|
501
|
+
}
|
|
502
|
+
|
|
452
503
|
async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
|
|
453
504
|
const memory = await loadMemory(memoryDir);
|
|
454
505
|
const rows = readFactRows(memory);
|
|
@@ -512,7 +563,8 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
|
|
|
512
563
|
);
|
|
513
564
|
}
|
|
514
565
|
|
|
515
|
-
const
|
|
566
|
+
const ruleRows = readRuleRows(memory);
|
|
567
|
+
const families = actionFamilies(ruleRows);
|
|
516
568
|
const family = families.get(cmd.verb);
|
|
517
569
|
if (!family) {
|
|
518
570
|
return answer(
|
|
@@ -619,9 +671,15 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
|
|
|
619
671
|
);
|
|
620
672
|
}
|
|
621
673
|
|
|
622
|
-
// open / unlock / close — the container verbs.
|
|
623
|
-
//
|
|
624
|
-
//
|
|
674
|
+
// open / unlock / close — the container verbs. presence and container-ness
|
|
675
|
+
// stay hand-checked here (visibility gating, not a state precondition);
|
|
676
|
+
// unlock's instrument match stays fully hand-written below it too — it
|
|
677
|
+
// needs a third, externally-supplied binding beyond subject/target, which
|
|
678
|
+
// this retrofit does not attempt. open/close's lock-state and open/closed
|
|
679
|
+
// checks, and their mgx:is-open write, are now taught "fact-value"
|
|
680
|
+
// precond/effect rows consulted through domain.mjs below; only the
|
|
681
|
+
// hidden-contents reveal (a variable-arity effect over a discovered set)
|
|
682
|
+
// stays hand-written JS, since no shipped rule shape covers that either.
|
|
625
683
|
const presentHere = place && place.predicate !== "mgx:hidden-in" && place.predicate !== "mgx:currently-in" && place.object === here;
|
|
626
684
|
if (!presentHere) {
|
|
627
685
|
return answer(`I don't see a ${object} here.`, noteFor(`${cmd.verb} — ${object} isn't in the ${here}; declined`), { miss: true });
|
|
@@ -629,40 +687,50 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
|
|
|
629
687
|
if (!isContainer(rows, object)) {
|
|
630
688
|
return answer(`the ${object} doesn't open.`, noteFor(`${cmd.verb} — no mgx:is-container fact on ${object}; declined by name`), { miss: true });
|
|
631
689
|
}
|
|
632
|
-
const open = !!state.openness.get(object)?.open;
|
|
633
690
|
|
|
634
|
-
if (cmd.verb === "open") {
|
|
635
|
-
|
|
636
|
-
|
|
691
|
+
if (cmd.verb === "open" || cmd.verb === "close") {
|
|
692
|
+
const domain = compileDomain(rows, ruleRows);
|
|
693
|
+
const taughtAction = domain.actions.find((a) => a.name === cmd.verb);
|
|
694
|
+
const effect = taughtAction?.effects.find((e) => e.predicate === OPEN_PREDICATE);
|
|
695
|
+
if (!effect) {
|
|
696
|
+
return answer(
|
|
697
|
+
`this world doesn't teach how ${cmd.verb === "open" ? "opening" : "closing"} changes the ${object}.`,
|
|
698
|
+
noteFor(`${cmd.verb} — the taught "${cmd.verb}" family carries no ${OPEN_PREDICATE} effect; honest decline`),
|
|
699
|
+
{ miss: true },
|
|
700
|
+
);
|
|
637
701
|
}
|
|
638
|
-
|
|
639
|
-
|
|
702
|
+
const factState = containerDatatypeState(state, object);
|
|
703
|
+
const failed = taughtAction.preconds.find((p) => !precondHolds(p, "player", object, factState, domain));
|
|
704
|
+
if (failed) {
|
|
705
|
+
const text = failed.predicate === "mgx:stands-locked-in"
|
|
706
|
+
? `the ${object} is locked.`
|
|
707
|
+
: cmd.verb === "open" ? `the ${object} is already open.` : `the ${object} isn't open.`;
|
|
708
|
+
return answer(text, noteFor(`${cmd.verb} — the taught "${cmd.verb}" family's ${failed.predicate} precondition declined by name`), { miss: true });
|
|
640
709
|
}
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
return answer(`the ${object} isn't open.`, noteFor("close — not open; declined"), { miss: true });
|
|
710
|
+
const effSubject = roleBinding(effect.subjectRole, "player", object, domain);
|
|
711
|
+
const writeIsOpen = { subject: `${effSubject}@turn${k}`, predicate: effect.predicate, object: effect.value };
|
|
712
|
+
|
|
713
|
+
if (cmd.verb === "open") {
|
|
714
|
+
const revealed = [...state.placements]
|
|
715
|
+
.filter(([, p]) => p.predicate === "mgx:hidden-in" && p.object === object)
|
|
716
|
+
.map(([thing]) => thing)
|
|
717
|
+
.sort();
|
|
718
|
+
return commit(
|
|
719
|
+
[
|
|
720
|
+
writeIsOpen,
|
|
721
|
+
...revealed.map((thing) => ({ subject: `${thing}@turn${k}`, predicate: "mgx:located-in", object })),
|
|
722
|
+
],
|
|
723
|
+
revealed.length
|
|
724
|
+
? `you open the ${object} — inside: the ${revealed.join(", the ")}.`
|
|
725
|
+
: `you open the ${object}. It's empty.`,
|
|
726
|
+
`open — ${object} opens${revealed.length ? `, revealing ${revealed.join(", ")}` : ""} via the taught "open" family's effect`,
|
|
727
|
+
`open the ${object}`,
|
|
728
|
+
);
|
|
661
729
|
}
|
|
662
730
|
return commit(
|
|
663
|
-
[
|
|
731
|
+
[writeIsOpen],
|
|
664
732
|
`you close the ${object}.`,
|
|
665
|
-
`close — ${object} closes`,
|
|
733
|
+
`close — ${object} closes via the taught "close" family's effect`,
|
|
666
734
|
`close the ${object}`,
|
|
667
735
|
);
|
|
668
736
|
}
|
package/src/services/chat.mjs
CHANGED
|
@@ -52,7 +52,7 @@ import { CHILD_PACK_NAME, childProvenanceTag } from "../domain/child-pack.mjs";
|
|
|
52
52
|
import { getChildPackProvider } from "../adapters/corpus/child-pack.mjs";
|
|
53
53
|
import { dialogueActForLane } from "../domain/dialogue-acts.mjs";
|
|
54
54
|
import { relatedForTerm } from "../domain/skos-view.mjs";
|
|
55
|
-
import { adventureTurn } from "./adventure.mjs";
|
|
55
|
+
import { adventureTurn, unclaimedAdventureOpening } from "./adventure.mjs";
|
|
56
56
|
import { spiderFlyTurn } from "./spider-fly-turn.mjs";
|
|
57
57
|
|
|
58
58
|
// Composition: the chat surface supplies the domain parser's default lemma/POS
|
|
@@ -12646,6 +12646,24 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
12646
12646
|
}
|
|
12647
12647
|
}
|
|
12648
12648
|
|
|
12649
|
+
// A "play X" naming no world EITHER game lane above claimed — last resort,
|
|
12650
|
+
// checked only once the adventure lane's own fallthrough and spider-fly's
|
|
12651
|
+
// own opener have both passed on the line, so this never outguesses a
|
|
12652
|
+
// sibling game's own recognized phrasing. Honest by name ("I don't know a
|
|
12653
|
+
// world called…") rather than the generic non-answer an unclaimed opener
|
|
12654
|
+
// fell into before.
|
|
12655
|
+
{
|
|
12656
|
+
const unclaimed = await unclaimedAdventureOpening(workingLine, { env });
|
|
12657
|
+
if (unclaimed) {
|
|
12658
|
+
note(trace, `lane: ${unclaimed.note}`);
|
|
12659
|
+
const result = plainTurn(workingLine, unclaimed.text, { via: "game", miss: true, focus });
|
|
12660
|
+
result.lane = unclaimed.lane;
|
|
12661
|
+
const rec = withLast(result, "play the adventure");
|
|
12662
|
+
rec.planState = planHolder.state;
|
|
12663
|
+
return rec;
|
|
12664
|
+
}
|
|
12665
|
+
}
|
|
12666
|
+
|
|
12649
12667
|
// Conversational layer next (greetings, thanks, help, bye, why/say-more) — these
|
|
12650
12668
|
// resolve no entity and carry their own preserved `last`. Bypasses withLast (a
|
|
12651
12669
|
// conversational turn is never finish()'d / never becomes a new `last`), so the
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
34
34
|
import { createTicker } from "./viz-ticker.mjs";
|
|
35
35
|
import { GRID_SIZE, WEB_HOME, WEB_RADIUS, isInWebBlock, cellId } from "../domain/spider-fly-world.mjs";
|
|
36
|
+
import { FLY_INITIAL_MASS, SPIDER_INITIAL_MASS } from "./spider-fly.mjs";
|
|
36
37
|
|
|
37
38
|
const CELL_PX = 44;
|
|
38
39
|
const BOARD_PX = CELL_PX * GRID_SIZE;
|
|
@@ -111,6 +112,8 @@ export function renderSpiderFlyHtml({ title = DEFAULT_TITLE } = {}) {
|
|
|
111
112
|
cellPx: CELL_PX,
|
|
112
113
|
previewMaxTurns: PREVIEW_MAX_TURNS,
|
|
113
114
|
tickWaitMs: TICK_WAIT_MS,
|
|
115
|
+
maxFlyMass: FLY_INITIAL_MASS,
|
|
116
|
+
maxSpiderMass: SPIDER_INITIAL_MASS,
|
|
114
117
|
});
|
|
115
118
|
|
|
116
119
|
return `<!doctype html>
|
|
@@ -154,6 +157,9 @@ ${THEME_TOKENS_CSS}
|
|
|
154
157
|
.hud-id { font-family: ${MONO_STACK}; font-size: .74rem; }
|
|
155
158
|
.hud-id.spider { color: var(--taught); } .hud-id.fly { color: var(--fly); } .hud-id.egg { color: var(--muted); }
|
|
156
159
|
.hud-goal { font-size: .85rem; }
|
|
160
|
+
.mass-track { height: 4px; margin-top: .3rem; background: var(--line); border-radius: 2px; overflow: hidden; }
|
|
161
|
+
.mass-fill { height: 100%; background: var(--taught); }
|
|
162
|
+
.mass-fill.fly { background: var(--fly); }
|
|
157
163
|
.hud-empty { color: var(--muted); font-size: .85rem; }
|
|
158
164
|
.chatlog { display: flex; flex-direction: column; gap: .4rem; max-height: 220px; overflow-y: auto; margin-bottom: .5rem; }
|
|
159
165
|
.chatlog:empty { display: none; margin-bottom: 0; }
|
|
@@ -264,6 +270,7 @@ const SPIDERFLY = ${gridData};
|
|
|
264
270
|
// ---- state shared across redraws --------------------------------------
|
|
265
271
|
let session = null;
|
|
266
272
|
let lastAgents = {};
|
|
273
|
+
let lastActiveWebs = [];
|
|
267
274
|
let lastTurn = 0;
|
|
268
275
|
const goalById = {};
|
|
269
276
|
const spriteEls = {};
|
|
@@ -324,13 +331,21 @@ const SPIDERFLY = ${gridData};
|
|
|
324
331
|
lastAgents = agents;
|
|
325
332
|
}
|
|
326
333
|
|
|
334
|
+
function massBarHtml(cls, mass) {
|
|
335
|
+
const maxMass = cls === "spider" ? SPIDERFLY.maxSpiderMass : cls === "fly" ? SPIDERFLY.maxFlyMass : null;
|
|
336
|
+
if (typeof mass !== "number" || !maxMass) return "";
|
|
337
|
+
const pct = Math.max(0, Math.min(100, (mass / maxMass) * 100));
|
|
338
|
+
return '<div class="mass-track"><div class="mass-fill ' + esc(cls) + '" style="width:' + pct + '%"></div></div>';
|
|
339
|
+
}
|
|
340
|
+
|
|
327
341
|
function renderHud() {
|
|
328
342
|
const ids = Object.keys(lastAgents).sort();
|
|
329
343
|
if (!ids.length) { hudEl.innerHTML = '<div class="hud-empty">no agents on the board.</div>'; return; }
|
|
330
344
|
hudEl.innerHTML = ids.map((id) => {
|
|
331
345
|
const cls = classOfAgentId(id);
|
|
332
346
|
return '<div class="hud-row"><span class="hud-id ' + esc(cls) + '">' + esc(id) + '</span>'
|
|
333
|
-
+ '<span class="hud-goal">' + esc(goalById[id] || "watching\\u2026") + "</span
|
|
347
|
+
+ '<span class="hud-goal">' + esc(goalById[id] || "watching\\u2026") + "</span>"
|
|
348
|
+
+ massBarHtml(cls, lastAgents[id].mass) + "</div>";
|
|
334
349
|
}).join("");
|
|
335
350
|
}
|
|
336
351
|
|
|
@@ -340,7 +355,7 @@ const SPIDERFLY = ${gridData};
|
|
|
340
355
|
directionDelta: tmctSpiderFly.DIRECTION_DELTA,
|
|
341
356
|
};
|
|
342
357
|
|
|
343
|
-
function drawBoard(agents) {
|
|
358
|
+
function drawBoard(agents, activeWebs) {
|
|
344
359
|
const w = SPIDERFLY.boardPx, h = SPIDERFLY.boardPx;
|
|
345
360
|
boardCtx.clearRect(0, 0, w, h);
|
|
346
361
|
boardCtx.fillStyle = cssVar("--taught-soft") || "rgba(46,125,79,.12)";
|
|
@@ -348,6 +363,20 @@ const SPIDERFLY = ${gridData};
|
|
|
348
363
|
const p = tmctSpiderFly.parseCellId(wc);
|
|
349
364
|
boardCtx.fillRect((p.x - 1) * cellSize, (p.y - 1) * cellSize, cellSize, cellSize);
|
|
350
365
|
}
|
|
366
|
+
// A spider-built dynamic web is a distinct color from the always-on
|
|
367
|
+
// static home zone above, plus a dashed outline — same concept
|
|
368
|
+
// (hasActiveWebAt), visually two different things on the board.
|
|
369
|
+
boardCtx.fillStyle = cssVar("--alert-soft") || "rgba(176,80,63,.12)";
|
|
370
|
+
boardCtx.strokeStyle = cssVar("--alert") || "#B0503F";
|
|
371
|
+
boardCtx.lineWidth = 1;
|
|
372
|
+
boardCtx.setLineDash([3, 2]);
|
|
373
|
+
for (const web of activeWebs || []) {
|
|
374
|
+
const p = tmctSpiderFly.parseCellId(web.cell);
|
|
375
|
+
if (!p) continue;
|
|
376
|
+
boardCtx.fillRect((p.x - 1) * cellSize, (p.y - 1) * cellSize, cellSize, cellSize);
|
|
377
|
+
boardCtx.strokeRect((p.x - 1) * cellSize + 0.5, (p.y - 1) * cellSize + 0.5, cellSize - 1, cellSize - 1);
|
|
378
|
+
}
|
|
379
|
+
boardCtx.setLineDash([]);
|
|
351
380
|
boardCtx.strokeStyle = cssVar("--line") || "#DDD9D0";
|
|
352
381
|
boardCtx.lineWidth = 1;
|
|
353
382
|
for (let i = 0; i <= SPIDERFLY.gridSize; i += 1) {
|
|
@@ -406,12 +435,13 @@ const SPIDERFLY = ${gridData};
|
|
|
406
435
|
});
|
|
407
436
|
boardFrame.addEventListener("mouseleave", () => { threadTip.style.display = "none"; });
|
|
408
437
|
|
|
409
|
-
function redraw(agents, turn) {
|
|
438
|
+
function redraw(agents, turn, activeWebs) {
|
|
410
439
|
applyAgents(agents);
|
|
411
440
|
renderHud();
|
|
412
441
|
lastTurn = turn;
|
|
442
|
+
lastActiveWebs = activeWebs || [];
|
|
413
443
|
turnLabelEl.textContent = "turn: " + turn;
|
|
414
|
-
drawBoard(agents);
|
|
444
|
+
drawBoard(agents, lastActiveWebs);
|
|
415
445
|
drawPov();
|
|
416
446
|
}
|
|
417
447
|
|
|
@@ -431,7 +461,7 @@ const SPIDERFLY = ${gridData};
|
|
|
431
461
|
const result = await session.turn(q);
|
|
432
462
|
addChatLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
|
|
433
463
|
const snap = await session.snapshot();
|
|
434
|
-
redraw(snap.agents, snap.turn);
|
|
464
|
+
redraw(snap.agents, snap.turn, snap.activeWebs);
|
|
435
465
|
});
|
|
436
466
|
});
|
|
437
467
|
|
|
@@ -447,7 +477,7 @@ const SPIDERFLY = ${gridData};
|
|
|
447
477
|
|
|
448
478
|
async function boot() {
|
|
449
479
|
session = await tmctSpiderFly.createSpiderFlySession();
|
|
450
|
-
redraw(session.initial.agents, session.initial.turn);
|
|
480
|
+
redraw(session.initial.agents, session.initial.turn, session.initial.activeWebs);
|
|
451
481
|
statusEl.textContent = session.opening;
|
|
452
482
|
chatqEl.disabled = false;
|
|
453
483
|
resetBtn.disabled = false; playBtn.disabled = false; stepBtn.disabled = false;
|
|
@@ -457,7 +487,7 @@ const SPIDERFLY = ${gridData};
|
|
|
457
487
|
const ticker = createTicker({
|
|
458
488
|
onTick: () => withLock(async () => {
|
|
459
489
|
const result = await session.tick();
|
|
460
|
-
redraw(result.agents, result.turn);
|
|
490
|
+
redraw(result.agents, result.turn, result.activeWebs);
|
|
461
491
|
}),
|
|
462
492
|
onRender: (state) => {
|
|
463
493
|
playBtn.textContent = state.playing ? "\\u23f8 pause" : "\\u25b6 play";
|
|
@@ -479,10 +509,10 @@ const SPIDERFLY = ${gridData};
|
|
|
479
509
|
|
|
480
510
|
boot().then(() => { if (preview) ticker.play(); });
|
|
481
511
|
|
|
482
|
-
new MutationObserver(() => { drawBoard(lastAgents); drawPov(); })
|
|
512
|
+
new MutationObserver(() => { drawBoard(lastAgents, lastActiveWebs); drawPov(); })
|
|
483
513
|
.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
|
|
484
514
|
if (window.matchMedia) {
|
|
485
|
-
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { drawBoard(lastAgents); drawPov(); });
|
|
515
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { drawBoard(lastAgents, lastActiveWebs); drawPov(); });
|
|
486
516
|
}
|
|
487
517
|
})();
|
|
488
518
|
</script>
|