@polycode-projects/the-mechanical-code-talker 4.0.0 → 4.1.0
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 +178 -9
- package/corpus/reference/index.json.gz +0 -0
- package/corpus/reference/manifest.json +8 -8
- package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
- package/corpus/sprites/src/sprite-facts.jsonl +401 -0
- package/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +49 -9
- package/corpus/worlds/shards/greyvale-museum.jsonl.gz +0 -0
- package/corpus/worlds/shards/lantern-cottage.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
- package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
- package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
- package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
- package/corpus/worlds/src/mud-hollow.jsonl +82 -0
- package/corpus/worlds/src/mud-warren.jsonl +124 -0
- package/corpus/worlds/src/spider-fly.jsonl +1 -1
- package/data/sprites/animal-icon.toml +11 -5
- package/data/sprites/book-icon.toml +9 -5
- package/data/sprites/cabinet-icon.toml +10 -5
- package/data/sprites/cellar-icon.toml +16 -6
- package/data/sprites/container-icon.toml +7 -3
- package/data/sprites/desk-icon.toml +8 -5
- package/data/sprites/dog-icon.toml +8 -5
- package/data/sprites/dog-with-colour-icon.toml +13 -12
- package/data/sprites/drawing-room-icon.toml +14 -5
- package/data/sprites/egg-icon.toml +6 -3
- package/data/sprites/fly-icon.toml +10 -5
- package/data/sprites/furniture-icon.toml +5 -3
- package/data/sprites/garden-icon.toml +10 -3
- package/data/sprites/key-icon.toml +3 -1
- package/data/sprites/kitchen-icon.toml +15 -6
- package/data/sprites/lamp-icon.toml +9 -4
- package/data/sprites/letter-icon.toml +7 -4
- package/data/sprites/library-icon.toml +14 -3
- package/data/sprites/pan-icon.toml +7 -3
- package/data/sprites/person-icon.toml +6 -2
- package/data/sprites/poodle-icon.toml +1 -1
- package/data/sprites/portable-icon.toml +6 -4
- package/data/sprites/portrait-icon.toml +7 -4
- package/data/sprites/room-icon.toml +11 -2
- package/data/sprites/spider-icon.toml +9 -3
- package/data/sprites/study-icon.toml +10 -2
- package/package.json +5 -4
- package/src/adapters/memory/core.mjs +20 -0
- package/src/domain/ask-vocab.mjs +71 -0
- package/src/domain/ask.mjs +168 -0
- package/src/domain/game-config.mjs +11 -0
- package/src/domain/grammar/ace.mjs +40 -4
- package/src/domain/grammar/lexicon-core.json +2 -1
- package/src/domain/grammar/lexicon.mjs +18 -0
- package/src/domain/mud-facts.mjs +15 -0
- package/src/domain/reference-pack.mjs +31 -7
- package/src/domain/router/drive.mjs +35 -9
- package/src/domain/router/registry.mjs +24 -4
- package/src/domain/router/resolver.mjs +102 -40
- package/src/domain/scene-compose.mjs +117 -0
- package/src/domain/spider-fly-world.mjs +37 -1
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +169 -20
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +209 -157
- package/src/services/adventure.mjs +526 -391
- package/src/services/chat-page-viz.mjs +69 -25
- package/src/services/chat.mjs +200 -51
- package/src/services/code-explorer-viz.mjs +102 -62
- package/src/services/extract-facts.mjs +4 -7
- package/src/services/ingest-viz.mjs +68 -82
- package/src/services/ledger-viz.mjs +136 -67
- package/src/services/memory-panel-viz.mjs +62 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +1016 -208
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +123 -64
- package/src/services/research-viz.mjs +160 -108
- package/src/services/spider-fly-turn.mjs +15 -23
- package/src/services/spider-fly-viz.mjs +146 -161
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +414 -240
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +90 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +49 -33
- package/src/surfaces/web/chat-browser-entry.mjs +30 -105
- package/src/surfaces/web/code-explorer-browser-entry.mjs +168 -24
- package/src/surfaces/web/ingest-browser-entry.mjs +3 -13
- package/src/surfaces/web/ledger-browser-entry.mjs +7 -47
- package/src/surfaces/web/memory-ask-browser.bundle.js +127 -124
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +71 -29
- package/src/surfaces/web/plan-browser-entry.mjs +22 -40
- package/src/surfaces/web/research-browser-entry.mjs +26 -41
- package/src/surfaces/web/spider-fly-browser-entry.mjs +45 -28
- package/src/surfaces/web/sprites-browser-entry.mjs +14 -27
- package/src/surfaces/web/turn-session.mjs +120 -0
- package/src/tools/definitions.mjs +30 -0
- package/src/tools/handlers/index.mjs +6 -3
- package/src/tools/handlers/kit.mjs +19 -2
- package/src/tools/handlers/tmct-ask.mjs +11 -6
- package/src/tools/handlers/tmct-ingest.mjs +5 -1
- package/src/tools/handlers/tmct-related.mjs +4 -4
- package/src/tools/handlers/tmct-sprite.mjs +147 -0
- package/src/tools/memory-fallthrough.mjs +9 -2
- package/src/tools/server.mjs +25 -1
|
@@ -164,6 +164,19 @@ export function createP2pRoom({
|
|
|
164
164
|
const seenProvenanceById = new Map();
|
|
165
165
|
let cachedRows = [];
|
|
166
166
|
|
|
167
|
+
// Store-touching work runs one job at a time, in arrival order. Every path
|
|
168
|
+
// that reads or writes memoryDir/seenProvenanceById/cachedRows crosses at
|
|
169
|
+
// least one await, so without this a rebind could swap the store while a
|
|
170
|
+
// merge is half-written into the old one — the merged rows would then
|
|
171
|
+
// baseline against the wrong store, or vanish. Jobs queued behind a rebind
|
|
172
|
+
// land on the store the rebind installed.
|
|
173
|
+
let storeChain = Promise.resolve();
|
|
174
|
+
function withStore(job) {
|
|
175
|
+
const run = storeChain.then(job);
|
|
176
|
+
storeChain = run.then(() => {}, () => {});
|
|
177
|
+
return run;
|
|
178
|
+
}
|
|
179
|
+
|
|
167
180
|
const factsListeners = new Set();
|
|
168
181
|
const stateListeners = new Set();
|
|
169
182
|
const peersListeners = new Set();
|
|
@@ -246,8 +259,8 @@ export function createP2pRoom({
|
|
|
246
259
|
|
|
247
260
|
/** Diff the store against what this room last saw, relabel each changed
|
|
248
261
|
* fact's provenance for the wire, and broadcast the batch. The page calls
|
|
249
|
-
* this once after every local turn or action. */
|
|
250
|
-
async function
|
|
262
|
+
* this (as `afterLocalChange`) once after every local turn or action. */
|
|
263
|
+
async function flushLocalChange() {
|
|
251
264
|
await ensureStarted();
|
|
252
265
|
await refreshRows();
|
|
253
266
|
const changed = [];
|
|
@@ -270,7 +283,7 @@ export function createP2pRoom({
|
|
|
270
283
|
if (!accepted.length) return { merged: 0 };
|
|
271
284
|
// Flush first: a local fact still waiting to be diffed would otherwise be
|
|
272
285
|
// recorded as merged below and never leave this browser.
|
|
273
|
-
await
|
|
286
|
+
await flushLocalChange();
|
|
274
287
|
const { ids } = await appendFacts(memoryDir, accepted.map((f) => ({
|
|
275
288
|
subject: f.subject,
|
|
276
289
|
predicate: f.predicate,
|
|
@@ -357,9 +370,11 @@ export function createP2pRoom({
|
|
|
357
370
|
return;
|
|
358
371
|
}
|
|
359
372
|
case "sync-request": {
|
|
360
|
-
await
|
|
361
|
-
|
|
362
|
-
|
|
373
|
+
const facts = await withStore(async () => {
|
|
374
|
+
await refreshRows();
|
|
375
|
+
const timestamp = now();
|
|
376
|
+
return syncableFacts(cachedRows).flatMap((row) => toWireFacts(row, displayName, timestamp));
|
|
377
|
+
});
|
|
363
378
|
send(transport, syncResponseMessage({ facts }));
|
|
364
379
|
return;
|
|
365
380
|
}
|
|
@@ -368,7 +383,7 @@ export function createP2pRoom({
|
|
|
368
383
|
// between them needs no sequencing.
|
|
369
384
|
case "sync-response":
|
|
370
385
|
case "op":
|
|
371
|
-
await mergeIncomingFacts(message.facts);
|
|
386
|
+
await withStore(() => mergeIncomingFacts(message.facts));
|
|
372
387
|
}
|
|
373
388
|
}
|
|
374
389
|
|
|
@@ -413,7 +428,7 @@ export function createP2pRoom({
|
|
|
413
428
|
/** Mint a fresh invite blob. One blob completes exactly one connection, so
|
|
414
429
|
* each call replaces whatever earlier invite was still waiting for a reply. */
|
|
415
430
|
async function startSharing() {
|
|
416
|
-
await ensureStarted
|
|
431
|
+
await withStore(ensureStarted);
|
|
417
432
|
const transport = attachTransport(transportFactory());
|
|
418
433
|
pendingShare = transport;
|
|
419
434
|
lastError = null;
|
|
@@ -425,7 +440,7 @@ export function createP2pRoom({
|
|
|
425
440
|
/** Decode someone's invite and answer it. Returns the reply blob to send
|
|
426
441
|
* back, or a named problem the page can show beside the box it came from. */
|
|
427
442
|
async function acceptInvite(blobString) {
|
|
428
|
-
await ensureStarted
|
|
443
|
+
await withStore(ensureStarted);
|
|
429
444
|
const decoded = decodeInviteBlob(blobString);
|
|
430
445
|
if (decoded.error) {
|
|
431
446
|
lastError = problem("invite", decoded.error);
|
|
@@ -453,7 +468,7 @@ export function createP2pRoom({
|
|
|
453
468
|
/** Feed the joiner's reply back into the invite it answers, completing the
|
|
454
469
|
* connection. */
|
|
455
470
|
async function completeInvite(replyBlob) {
|
|
456
|
-
await ensureStarted
|
|
471
|
+
await withStore(ensureStarted);
|
|
457
472
|
if (!pendingShare) {
|
|
458
473
|
lastError = problem("reply", "no-pending-invite");
|
|
459
474
|
return lastError;
|
|
@@ -482,20 +497,24 @@ export function createP2pRoom({
|
|
|
482
497
|
}
|
|
483
498
|
|
|
484
499
|
async function setMyDisplayName(name) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
500
|
+
return withStore(async () => {
|
|
501
|
+
await ensureStarted();
|
|
502
|
+
displayName = name;
|
|
503
|
+
await appendFacts(memoryDir, [nodeNameFact(myPeerId, name, now())]);
|
|
504
|
+
await sortFactIndividualsById();
|
|
505
|
+
return flushLocalChange();
|
|
506
|
+
});
|
|
490
507
|
}
|
|
491
508
|
|
|
492
509
|
/** Wave as `subjectId`, in `roomId` when there is one. chat.html's presence
|
|
493
510
|
* wave passes null and lands on PRESENCE_SCOPE instead. */
|
|
494
511
|
async function wave(subjectId, roomId = null) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
512
|
+
return withStore(async () => {
|
|
513
|
+
await ensureStarted();
|
|
514
|
+
await appendFacts(memoryDir, [waveFact(subjectId, roomId || PRESENCE_SCOPE, now())]);
|
|
515
|
+
await sortFactIndividualsById();
|
|
516
|
+
return flushLocalChange();
|
|
517
|
+
});
|
|
499
518
|
}
|
|
500
519
|
|
|
501
520
|
/** Whether `subjectId` is waving right now, read from the cached rows so a
|
|
@@ -517,6 +536,53 @@ export function createP2pRoom({
|
|
|
517
536
|
return peers.get(peerId)?.displayName || String(peerId).slice(0, 8);
|
|
518
537
|
}
|
|
519
538
|
|
|
539
|
+
/** Re-bind this room to a fresh store — the recast path. The peer
|
|
540
|
+
* connections are the point of keeping the room alive, so nothing about
|
|
541
|
+
* the transports or the peer map is touched. In order: flush whatever the
|
|
542
|
+
* OLD store still had undiffed (a turn landed just before the recast must
|
|
543
|
+
* not vanish silently), swap the store reference, rebuild the diff
|
|
544
|
+
* baseline against the new store, write the identity facts into it, then
|
|
545
|
+
* push the new store's syncable facts to every open channel as an
|
|
546
|
+
* ordinary op and ask each peer for its own view with an ordinary
|
|
547
|
+
* sync-request — the exact machinery a freshly opened channel uses, no
|
|
548
|
+
* new message type. Runs on the store chain, so a merge in flight when
|
|
549
|
+
* the recast happens finishes against the store it started on, and
|
|
550
|
+
* everything behind it lands on the new one. */
|
|
551
|
+
async function rebind({ memoryDir: nextMemoryDir, worldName: nextWorldName, myDisplayName: nextDisplayName } = {}) {
|
|
552
|
+
if (!nextMemoryDir) throw new Error("rebind needs the store to bind to");
|
|
553
|
+
return withStore(async () => {
|
|
554
|
+
if (closed) throw new Error("this room is closed");
|
|
555
|
+
if (started) await flushLocalChange();
|
|
556
|
+
memoryDir = nextMemoryDir;
|
|
557
|
+
if (nextWorldName) worldName = nextWorldName;
|
|
558
|
+
if (nextDisplayName) displayName = nextDisplayName;
|
|
559
|
+
started = true;
|
|
560
|
+
seenProvenanceById.clear();
|
|
561
|
+
cachedRows = [];
|
|
562
|
+
const timestamp = now();
|
|
563
|
+
const identity = [];
|
|
564
|
+
if (worldId && worldName) identity.push(worldNameFact(worldId, worldName, timestamp));
|
|
565
|
+
if (myPeerId && displayName) identity.push(nodeNameFact(myPeerId, displayName, timestamp));
|
|
566
|
+
if (identity.length) {
|
|
567
|
+
await appendFacts(memoryDir, identity);
|
|
568
|
+
await sortFactIndividualsById();
|
|
569
|
+
}
|
|
570
|
+
await refreshRows();
|
|
571
|
+
for (const row of cachedRows) seenProvenanceById.set(row.id, row.provenance);
|
|
572
|
+
const targets = connectedPeers();
|
|
573
|
+
let pushed = 0;
|
|
574
|
+
if (targets.length) {
|
|
575
|
+
const wireTimestamp = now();
|
|
576
|
+
const facts = syncableFacts(cachedRows).flatMap((row) => toWireFacts(row, displayName, wireTimestamp));
|
|
577
|
+
pushed = facts.length;
|
|
578
|
+
if (facts.length) broadcast(opMessage({ from: myPeerId, facts }));
|
|
579
|
+
broadcast(syncRequestMessage());
|
|
580
|
+
}
|
|
581
|
+
emit(factsListeners, { merged: 0, rows: cachedRows });
|
|
582
|
+
return { pushed, peers: targets.length };
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
520
586
|
function close() {
|
|
521
587
|
closed = true;
|
|
522
588
|
for (const peer of peers.values()) peer.transport.close();
|
|
@@ -534,23 +600,24 @@ export function createP2pRoom({
|
|
|
534
600
|
return {
|
|
535
601
|
peerId: myPeerId,
|
|
536
602
|
worldId,
|
|
537
|
-
worldName,
|
|
603
|
+
get worldName() { return worldName; },
|
|
538
604
|
get displayName() { return displayName; },
|
|
539
605
|
get state() { return state; },
|
|
540
606
|
get lastError() { return lastError; },
|
|
541
607
|
get droppedMessages() { return droppedMessages; },
|
|
542
608
|
peers: peerList,
|
|
543
609
|
displayNameFor,
|
|
544
|
-
start: ensureStarted,
|
|
610
|
+
start: () => withStore(ensureStarted),
|
|
545
611
|
startSharing,
|
|
546
612
|
acceptInvite,
|
|
547
613
|
completeInvite,
|
|
548
|
-
afterLocalChange,
|
|
614
|
+
afterLocalChange: () => withStore(flushLocalChange),
|
|
549
615
|
setMyDisplayName,
|
|
550
616
|
wave,
|
|
551
617
|
isWaving,
|
|
618
|
+
rebind,
|
|
552
619
|
factRows: () => cachedRows,
|
|
553
|
-
refresh: refreshRows,
|
|
620
|
+
refresh: () => withStore(refreshRows),
|
|
554
621
|
onFactsChanged: subscribe(factsListeners),
|
|
555
622
|
onStateChanged: subscribe(stateListeners),
|
|
556
623
|
onPeersChanged: subscribe(peersListeners),
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
// is itself a declared class name is a class-to-class edge; anything else is
|
|
26
26
|
// an individual-to-class edge), not a guess.
|
|
27
27
|
|
|
28
|
+
import { countLabel } from "./viz-theme.mjs";
|
|
29
|
+
|
|
28
30
|
const attachPrefix = (predicate) => {
|
|
29
31
|
const p = String(predicate ?? "").trim();
|
|
30
32
|
return p.includes(":") ? p : `mgx:${p}`;
|
|
@@ -208,7 +210,7 @@ export function planToPddl(plan, { problemName = "tmct-plan", domainName = "tmct
|
|
|
208
210
|
|
|
209
211
|
if (actions.length) {
|
|
210
212
|
lines.push("");
|
|
211
|
-
lines.push(`;; action sequence — findActionPath's own shortest path (${actions.length
|
|
213
|
+
lines.push(`;; action sequence — findActionPath's own shortest path (${countLabel(actions.length, "move")})`);
|
|
212
214
|
actions.forEach((action, i) => {
|
|
213
215
|
const before = states[i] || [];
|
|
214
216
|
const after = states[i + 1] || [];
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// inline script degrades honestly when that sibling script is absent or
|
|
19
19
|
// fails to load — the live controls disable themselves rather than pretend
|
|
20
20
|
// to work.
|
|
21
|
-
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
21
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, countLabel } from "./viz-theme.mjs";
|
|
22
22
|
import { planToPddl } from "./plan-pddl.mjs";
|
|
23
23
|
|
|
24
24
|
const BOARD_W = 640;
|
|
@@ -285,10 +285,13 @@ export function renderPlanHtml({ plan, rendersAs = {}, sizeOrder = [], title } =
|
|
|
285
285
|
const pageData = planToPageData({ plan, rendersAs, sizeOrder });
|
|
286
286
|
const embedded = embedJson(pageData);
|
|
287
287
|
const pddlText = planToPddl(plan);
|
|
288
|
-
const pageTitle = title || `tmct plan — ${actions.length
|
|
288
|
+
const pageTitle = title || `tmct plan — ${countLabel(actions.length, "move")}`;
|
|
289
289
|
|
|
290
290
|
return `<!doctype html>
|
|
291
|
-
|
|
291
|
+
<!-- data-theme is pinned to dark on purpose: this page reads as a track/
|
|
292
|
+
timeline console, and that console stays dark under a light OS or site
|
|
293
|
+
theme too, the same way a DAW's own window does. -->
|
|
294
|
+
<html lang="en" data-theme="dark">
|
|
292
295
|
<head>
|
|
293
296
|
<meta charset="utf-8">
|
|
294
297
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -309,97 +312,144 @@ export function renderPlanHtml({ plan, rendersAs = {}, sizeOrder = [], title } =
|
|
|
309
312
|
-->
|
|
310
313
|
<style>
|
|
311
314
|
${THEME_TOKENS_CSS}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
315
|
+
/* This console stays dark under either site theme (the html tag pins
|
|
316
|
+
data-theme="dark" above), so these extra surfaces are plain literals
|
|
317
|
+
rather than theme-reactive tokens — a track console has one lighting
|
|
318
|
+
rig, not two. --taught/--corpus/--entail/--alert still come from
|
|
319
|
+
THEME_TOKENS_CSS above and still carry the site's own provenance
|
|
320
|
+
language: taught (green) marks the actual current position, entail
|
|
321
|
+
(amber) marks tmct's own inferred step goal, corpus (blue) marks a
|
|
322
|
+
session parameter, alert (red) marks a failed solve. */
|
|
323
|
+
:root {
|
|
324
|
+
--rail: #101317; --well: #0A0C0E; --strip: #1A1F25; --strip2: #20262D;
|
|
325
|
+
--taught-glow: rgba(95, 190, 139, .45); --entail-glow: rgba(217, 165, 84, .4);
|
|
326
|
+
}
|
|
327
|
+
html { background: var(--rail); }
|
|
328
|
+
body { margin: 0; background: var(--rail); color: var(--ink); font-family: ${MONO_STACK}; font-size: 14px; }
|
|
329
|
+
main { max-width: 1040px; margin: 0 auto; padding: 1.3rem 1rem 3rem; }
|
|
330
|
+
button { font: inherit; }
|
|
331
|
+
|
|
332
|
+
.rack { border: 1px solid var(--line); background: var(--card); }
|
|
333
|
+
|
|
334
|
+
/* ---- head strip: the nameplate ---- */
|
|
335
|
+
.headStrip { display: flex; justify-content: space-between; align-items: flex-end; gap: .6rem; flex-wrap: wrap; padding: .9rem 1.1rem .75rem; background: linear-gradient(180deg, var(--strip2), var(--strip)); border-bottom: 1px solid var(--line); border-radius: 9px 9px 0 0; }
|
|
336
|
+
.eyebrow { font-family: ${MONO_STACK}; font-size: .64rem; letter-spacing: .16em; text-transform: uppercase; color: var(--muted); margin: 0 0 .35rem; }
|
|
337
|
+
h1 { font-family: ${SERIF_STACK}; font-size: 1.2rem; font-weight: 600; margin: 0; color: var(--ink); }
|
|
338
|
+
.chip { font-family: ${MONO_STACK}; font-size: .64rem; letter-spacing: .02em; color: var(--muted); border: 1px solid var(--line); border-radius: 3px; padding: .22rem .6rem; background: var(--well); text-align: right; }
|
|
339
|
+
|
|
340
|
+
/* ---- session strip: a channel-strip row of solve parameters ---- */
|
|
341
|
+
.sessionStrip { display: flex; align-items: stretch; flex-wrap: wrap; background: var(--strip); border-bottom: 1px solid var(--line); font-family: ${MONO_STACK}; font-size: .74rem; }
|
|
342
|
+
.sessionCell { display: flex; flex-direction: column; justify-content: center; gap: .3rem; padding: .55rem .95rem; border-right: 1px solid var(--line); }
|
|
343
|
+
.paramLabel { font-size: .6rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
|
|
344
|
+
.sessionStrip input[type="number"] { width: 3.6rem; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--well); color: var(--taught); border: 1px solid var(--line); border-radius: 4px; padding: .22rem .4rem; font-variant-numeric: tabular-nums; }
|
|
345
|
+
.sessionAction { justify-content: center; }
|
|
346
|
+
.sessionAction button { font-family: ${MONO_STACK}; font-size: .74rem; padding: .4rem .85rem; border-radius: 5px; border: 1px solid var(--line); background: var(--well); color: var(--ink); cursor: pointer; }
|
|
347
|
+
.sessionAction button:hover:not(:disabled) { border-color: var(--corpus); color: var(--corpus); }
|
|
348
|
+
.sessionAction button:focus-visible { outline: 2px solid var(--corpus); outline-offset: 2px; }
|
|
349
|
+
.sessionAction button:disabled { opacity: .4; cursor: default; }
|
|
350
|
+
.sessionStatus { display: flex; align-items: center; margin-left: auto; padding: 0 .95rem; color: var(--muted); }
|
|
351
|
+
.sessionStatus.isError { color: var(--alert); }
|
|
352
|
+
|
|
353
|
+
/* ---- transport bar ---- */
|
|
354
|
+
.transport { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; padding: .7rem 1.1rem; background: var(--rail); border-bottom: 1px solid var(--line); }
|
|
355
|
+
.transportButtons { display: flex; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
|
|
356
|
+
.transportButtons button { font-family: ${MONO_STACK}; font-size: .78rem; padding: .48rem .8rem; border: none; border-right: 1px solid var(--line); background: var(--strip); color: var(--ink); cursor: pointer; }
|
|
357
|
+
.transportButtons button:last-child { border-right: none; }
|
|
358
|
+
.transportButtons button:hover:not(:disabled) { background: var(--strip2); }
|
|
359
|
+
.transportButtons button:focus-visible { outline: 2px solid var(--taught); outline-offset: -2px; }
|
|
360
|
+
.transportButtons button[disabled] { opacity: .35; cursor: default; }
|
|
361
|
+
.transportButtons #play.isPlaying { background: var(--taught-soft); color: var(--taught); }
|
|
362
|
+
.lcd { margin-left: auto; background: var(--well); border: 1px solid var(--line); border-radius: 6px; padding: .45rem .75rem; min-width: 10rem; }
|
|
363
|
+
#stepLabel { display: block; font-family: ${MONO_STACK}; font-size: 1rem; letter-spacing: .03em; color: var(--taught); text-shadow: 0 0 8px var(--taught-glow); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
364
|
+
.lcdMeter { margin-top: .3rem; height: 3px; background: var(--line); border-radius: 2px; overflow: hidden; }
|
|
365
|
+
.lcdMeterFill { height: 100%; width: 0%; background: var(--taught); box-shadow: 0 0 6px var(--taught-glow); transition: width .18s ease; }
|
|
366
|
+
|
|
367
|
+
/* ---- the deck: board on the left, arrangement view on the right ---- */
|
|
368
|
+
.stage { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: 0; }
|
|
319
369
|
/* grid items refuse to shrink below their content's min-width by default, so
|
|
320
370
|
the 640px board would stretch the track and the whole page with it on a
|
|
321
371
|
phone — let the items shrink and the boardwrap scroll instead. */
|
|
322
372
|
.stage > * { min-width: 0; }
|
|
323
|
-
|
|
373
|
+
.deck { padding: 1rem 1.1rem 1.1rem; border-right: 1px solid var(--line); }
|
|
374
|
+
@media (max-width: 660px) { .stage { grid-template-columns: 1fr; } .deck { border-right: none; border-bottom: 1px solid var(--line); } }
|
|
324
375
|
.boardwrap { overflow-x: auto; }
|
|
325
|
-
.board { position: relative; width: ${BOARD_W}px; height: ${BOARD_H}px; background: var(--
|
|
376
|
+
.board { position: relative; width: ${BOARD_W}px; height: ${BOARD_H}px; background: var(--well); border: 1px solid var(--line); border-radius: 6px; }
|
|
326
377
|
.base { position: absolute; left: 4%; right: 4%; bottom: 22px; height: 6px; background: var(--line); border-radius: 3px; }
|
|
327
378
|
.post { position: absolute; bottom: 28px; width: 6px; height: 150px; background: var(--line); border-radius: 3px 3px 0 0; }
|
|
328
|
-
.anchorlabel { position: absolute; bottom: 2px; transform: translateX(-50%); font-family: ${MONO_STACK}; font-size: .
|
|
329
|
-
.block { position: absolute; height: ${BLOCK_H}px; border-radius:
|
|
330
|
-
.block.moving { z-index: 3; box-shadow: 0
|
|
379
|
+
.anchorlabel { position: absolute; bottom: 2px; transform: translateX(-50%); font-family: ${MONO_STACK}; font-size: .64rem; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
|
|
380
|
+
.block { position: absolute; height: ${BLOCK_H}px; border-radius: 4px; color: #fff; display: flex; align-items: center; justify-content: center; font-family: ${MONO_STACK}; font-size: .64rem; font-weight: 600; box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 2px 4px rgba(0,0,0,.4); }
|
|
381
|
+
.block.moving { z-index: 3; box-shadow: 0 0 0 2px var(--taught), 0 6px 14px rgba(0,0,0,.5); }
|
|
331
382
|
.circle { position: absolute; width: 28px; height: 28px; border-radius: 50%; border: 2px solid var(--muted); transform: translate(-50%, -100%); }
|
|
332
383
|
.circlelabel { position: absolute; transform: translateX(-50%); font-family: ${MONO_STACK}; font-size: .6rem; color: var(--muted); }
|
|
333
|
-
.
|
|
334
|
-
.
|
|
335
|
-
.controls button:hover { border-color: var(--taught); }
|
|
336
|
-
.controls button:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
|
|
337
|
-
.controls button[disabled] { opacity: .4; cursor: default; }
|
|
338
|
-
.controls .step { margin-left: auto; font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
339
|
-
.goalline { margin-top: .7rem; font-family: ${MONO_STACK}; font-size: .76rem; color: var(--taught); background: var(--taught-soft); border-radius: 6px; padding: .45rem .65rem; }
|
|
340
|
-
.facts { margin-top: .7rem; font-family: ${MONO_STACK}; font-size: .7rem; color: var(--muted); border-top: 1px dashed var(--line); padding-top: .55rem; line-height: 1.7; }
|
|
384
|
+
.goalline { margin-top: .8rem; font-family: ${MONO_STACK}; font-size: .76rem; color: var(--entail); background: var(--entail-soft); border: 1px solid var(--entail); border-radius: 6px; padding: .5rem .75rem; }
|
|
385
|
+
.facts { margin-top: .7rem; font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); border-top: 1px dashed var(--line); padding-top: .55rem; line-height: 1.7; }
|
|
341
386
|
.facts b { color: var(--ink); }
|
|
342
|
-
.movelist { list-style: none; margin: 0; padding:
|
|
343
|
-
.movelist
|
|
344
|
-
.movelist li:hover { color: var(--ink); }
|
|
345
|
-
.movelist li.done { color: var(--ink); }
|
|
346
|
-
.movelist li.current { border-left-color: var(--taught); color: var(--ink); background: var(--taught-soft); font-weight: 700; }
|
|
347
|
-
.movelist .phasehead { font-size: .64rem; letter-spacing: .06em; text-transform: uppercase; border-left: 3px solid var(--taught); margin-top: .5rem; }
|
|
387
|
+
.movelist { list-style: none; margin: 0; padding: .5rem; font-family: ${MONO_STACK}; font-size: .72rem; align-self: start; }
|
|
388
|
+
.movelist .phasehead { display: block; background: var(--strip); border: 1px solid var(--line); border-left: 3px solid var(--corpus); border-radius: 4px; padding: .35rem .55rem; margin: .55rem 0 .3rem; font-size: .62rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); cursor: pointer; }
|
|
348
389
|
.movelist .phasehead:first-child { margin-top: 0; }
|
|
349
|
-
.movelist .phasehead:hover { color: var(--
|
|
390
|
+
.movelist .phasehead:hover { color: var(--ink); border-left-color: var(--taught); }
|
|
350
391
|
.movelist .phasehead:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
|
|
351
|
-
|
|
352
|
-
.
|
|
353
|
-
.
|
|
354
|
-
.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
.
|
|
359
|
-
.liveControls .livestatus { color: var(--muted); margin-left: auto; }
|
|
360
|
-
.lower { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1rem; margin-top: 1.2rem; }
|
|
392
|
+
.movelist li:not(.phasehead) { padding: .38rem .55rem; margin: .12rem 0; border: 1px solid transparent; border-radius: 4px; color: var(--muted); cursor: pointer; font-variant-numeric: tabular-nums; opacity: .55; }
|
|
393
|
+
.movelist li:not(.phasehead):hover { color: var(--ink); border-color: var(--line); opacity: 1; }
|
|
394
|
+
.movelist li.done { color: var(--ink); background: var(--strip); opacity: 1; }
|
|
395
|
+
.movelist li.current { color: var(--ink); background: var(--taught-soft); border-color: var(--taught); box-shadow: inset 3px 0 0 var(--taught); font-weight: 700; opacity: 1; }
|
|
396
|
+
@media (prefers-reduced-motion: reduce) { .block, .lcdMeterFill { transition: none !important; } }
|
|
397
|
+
|
|
398
|
+
/* ---- lower rack: two channel strips, teach dock and PDDL artifact ---- */
|
|
399
|
+
.lower { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0; border-top: 1px solid var(--line); }
|
|
361
400
|
@media (max-width: 780px) { .lower { grid-template-columns: 1fr; } }
|
|
362
|
-
.chat, .pddlpanel { background: var(--card);
|
|
363
|
-
.chat
|
|
401
|
+
.chat, .pddlpanel { background: var(--card); padding: .65rem .8rem; min-width: 0; }
|
|
402
|
+
.chat { border-right: 1px solid var(--line); }
|
|
403
|
+
@media (max-width: 780px) { .chat { border-right: none; border-bottom: 1px solid var(--line); } }
|
|
404
|
+
.chat h2, .pddlpanel h2 { font-family: ${MONO_STACK}; font-size: .64rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); font-weight: 400; margin: 0 0 .5rem; }
|
|
364
405
|
.chatlog { display: flex; flex-direction: column; gap: .4rem; max-height: 180px; overflow-y: auto; margin-bottom: .5rem; }
|
|
365
406
|
.chatlog:empty { display: none; margin-bottom: 0; }
|
|
366
407
|
.chatlog .u { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); }
|
|
367
408
|
.chatlog .u::before { content: "tmct> "; color: var(--taught); }
|
|
368
|
-
.chatlog .a { font-size: .
|
|
409
|
+
.chatlog .a { font-family: ${SERIF_STACK}; font-size: .87rem; line-height: 1.45; white-space: pre-wrap; color: var(--ink); }
|
|
369
410
|
.chatask { display: flex; align-items: center; gap: .5rem; border-top: 1px solid var(--line); padding-top: .5rem; }
|
|
370
411
|
.chatlog:empty + .chatask { border-top: none; padding-top: 0; }
|
|
371
412
|
.chatask .prompt { color: var(--taught); font-size: .78rem; font-family: ${MONO_STACK}; }
|
|
372
|
-
.chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--
|
|
413
|
+
.chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--well); color: var(--ink); border: 1px solid var(--line); border-radius: 4px; padding: .34rem .55rem; min-width: 0; }
|
|
373
414
|
.chatask input:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
|
|
374
415
|
.chatpills { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .5rem; }
|
|
375
|
-
.pill { font-family: ${MONO_STACK}; font-size: .
|
|
376
|
-
.pill:hover { border-color: var(--taught); }
|
|
416
|
+
.pill { font-family: ${MONO_STACK}; font-size: .66rem; padding: .22rem .6rem; border: 1px solid var(--line); border-radius: 99px; background: var(--well); color: var(--ink); cursor: pointer; white-space: nowrap; }
|
|
417
|
+
.pill:hover { border-color: var(--taught); color: var(--taught); }
|
|
377
418
|
.pill:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
|
|
378
|
-
.pddlpanel pre { margin: 0; font-family: ${MONO_STACK}; font-size: .
|
|
419
|
+
.pddlpanel pre { margin: 0; font-family: ${MONO_STACK}; font-size: .67rem; line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 420px; overflow-y: auto; color: var(--ink); background: var(--well); border: 1px solid var(--line); border-radius: 4px; padding: .6rem .7rem; }
|
|
379
420
|
</style>
|
|
380
421
|
</head>
|
|
381
422
|
<body>
|
|
382
423
|
<main>
|
|
383
|
-
|
|
384
|
-
|
|
424
|
+
<div class="rack">
|
|
425
|
+
<div class="headStrip">
|
|
426
|
+
<div>
|
|
427
|
+
<div class="eyebrow">tmct · plan</div>
|
|
428
|
+
<h1 id="pageTitle">${escapeHtml(pageTitle)}</h1>
|
|
429
|
+
</div>
|
|
385
430
|
<span class="chip">blocks archetype · ${pageData.layouts.length} snapshots · plan: findActionPath</span>
|
|
386
431
|
</div>
|
|
387
|
-
<div class="
|
|
388
|
-
<label for="diskCount">disks
|
|
389
|
-
<label for="maxDepth">max
|
|
390
|
-
<button id="resolveBtn" type="button">solve a fresh puzzle</button>
|
|
391
|
-
<span class="
|
|
432
|
+
<div class="sessionStrip" id="liveControls">
|
|
433
|
+
<label class="sessionCell" for="diskCount"><span class="paramLabel">disks</span><input id="diskCount" type="number" min="1" max="7" value="3"></label>
|
|
434
|
+
<label class="sessionCell" for="maxDepth"><span class="paramLabel">max depth</span><input id="maxDepth" type="number" min="1" max="999" value="300"></label>
|
|
435
|
+
<div class="sessionCell sessionAction"><button id="resolveBtn" type="button">solve a fresh puzzle</button></div>
|
|
436
|
+
<span class="sessionStatus" id="liveStatus"></span>
|
|
437
|
+
</div>
|
|
438
|
+
<div class="transport">
|
|
439
|
+
<div class="transportButtons">
|
|
440
|
+
<button id="reset" aria-label="Reset to start">⏮ reset</button>
|
|
441
|
+
<button id="back" aria-label="Step back">◀ back</button>
|
|
442
|
+
<button id="play" aria-label="Play">▶ play</button>
|
|
443
|
+
<button id="next" aria-label="Step forward">step ▶</button>
|
|
444
|
+
</div>
|
|
445
|
+
<div class="lcd">
|
|
446
|
+
<span id="stepLabel"></span>
|
|
447
|
+
<div class="lcdMeter"><div class="lcdMeterFill" id="stepMeter"></div></div>
|
|
448
|
+
</div>
|
|
392
449
|
</div>
|
|
393
450
|
<div class="stage">
|
|
394
|
-
<div>
|
|
451
|
+
<div class="deck">
|
|
395
452
|
<div class="boardwrap"><div class="board" id="board" aria-label="plan board"></div></div>
|
|
396
|
-
<div class="controls">
|
|
397
|
-
<button id="reset" aria-label="Reset to start">⏮ reset</button>
|
|
398
|
-
<button id="back" aria-label="Step back">◀ back</button>
|
|
399
|
-
<button id="play" aria-label="Play">▶ play</button>
|
|
400
|
-
<button id="next" aria-label="Step forward">step ▶</button>
|
|
401
|
-
<span class="step" id="stepLabel"></span>
|
|
402
|
-
</div>
|
|
403
453
|
<div class="goalline" id="goalline" hidden></div>
|
|
404
454
|
<div class="facts" id="facts"></div>
|
|
405
455
|
</div>
|
|
@@ -423,6 +473,7 @@ h1 { font-size: 1.15rem; margin: 0 0 .8rem; }
|
|
|
423
473
|
<pre id="pddlOut">${escapeHtml(pddlText)}</pre>
|
|
424
474
|
</div>
|
|
425
475
|
</div>
|
|
476
|
+
</div>
|
|
426
477
|
</main>
|
|
427
478
|
<script>
|
|
428
479
|
const PLAN = ${embedded};
|
|
@@ -431,6 +482,8 @@ const PLAN = ${embedded};
|
|
|
431
482
|
<script>
|
|
432
483
|
(function () {
|
|
433
484
|
"use strict";
|
|
485
|
+
const escapeHtml = ${escapeHtml.toString()};
|
|
486
|
+
const countLabel = ${countLabel.toString()};
|
|
434
487
|
// Best-effort: a copy of this page opened without the sibling worker file
|
|
435
488
|
// (a tmct --render plan --output file, a file:// open) just swallows the
|
|
436
489
|
// registration failure and works exactly as before.
|
|
@@ -438,6 +491,7 @@ const PLAN = ${embedded};
|
|
|
438
491
|
const pageTitleEl = document.getElementById("pageTitle");
|
|
439
492
|
const board = document.getElementById("board");
|
|
440
493
|
const stepLabel = document.getElementById("stepLabel");
|
|
494
|
+
const stepMeterEl = document.getElementById("stepMeter");
|
|
441
495
|
const goalline = document.getElementById("goalline");
|
|
442
496
|
const factsEl = document.getElementById("facts");
|
|
443
497
|
const movelist = document.getElementById("movelist");
|
|
@@ -511,6 +565,7 @@ const PLAN = ${embedded};
|
|
|
511
565
|
function render() {
|
|
512
566
|
const phase = phaseFor(step);
|
|
513
567
|
stepLabel.textContent = "step " + step + " / " + N + (phase ? " · " + phase : "");
|
|
568
|
+
if (stepMeterEl) stepMeterEl.style.width = (N ? Math.min(step, N) / N * 100 : 0) + "%";
|
|
514
569
|
if (plan.stepGoals) {
|
|
515
570
|
goalline.hidden = false;
|
|
516
571
|
goalline.textContent = step < N
|
|
@@ -520,7 +575,7 @@ const PLAN = ${embedded};
|
|
|
520
575
|
goalline.hidden = true;
|
|
521
576
|
}
|
|
522
577
|
factsEl.innerHTML = "<b>board@step" + step + "</b> — " +
|
|
523
|
-
plan.facts[step].map((f) => f
|
|
578
|
+
plan.facts[step].map((f) => escapeHtml(f)).join(" · ") +
|
|
524
579
|
' <span style="opacity:.7">(plan: findActionPath)</span>';
|
|
525
580
|
[...movelist.querySelectorAll("li:not(.phasehead)")].forEach((li, i) => {
|
|
526
581
|
li.classList.toggle("done", i < step);
|
|
@@ -529,6 +584,7 @@ const PLAN = ${embedded};
|
|
|
529
584
|
btn.back.disabled = step === 0 || animating;
|
|
530
585
|
btn.next.disabled = step === N || animating;
|
|
531
586
|
btn.play.textContent = playing ? "⏸ pause" : (step === N ? "▶ replay" : "▶ play");
|
|
587
|
+
btn.play.classList.toggle("isPlaying", playing);
|
|
532
588
|
}
|
|
533
589
|
async function forward() {
|
|
534
590
|
if (animating || step >= N) return;
|
|
@@ -625,6 +681,7 @@ const PLAN = ${embedded};
|
|
|
625
681
|
const liveAvailable = typeof tmctPlan !== "undefined" && typeof tmctPlan.createPlanSession === "function";
|
|
626
682
|
if (!liveAvailable) {
|
|
627
683
|
liveStatusEl.textContent = "live re-solve unavailable here — showing the baked-in replay only.";
|
|
684
|
+
liveStatusEl.classList.add("isError");
|
|
628
685
|
resolveBtn.disabled = true;
|
|
629
686
|
} else {
|
|
630
687
|
let session = null;
|
|
@@ -698,13 +755,15 @@ const PLAN = ${embedded};
|
|
|
698
755
|
const d = Math.max(1, parseInt(maxDepthEl.value, 10) || 300);
|
|
699
756
|
diskCountEl.value = n; maxDepthEl.value = d;
|
|
700
757
|
liveStatusEl.textContent = "solving a " + n + "-disk puzzle…";
|
|
758
|
+
liveStatusEl.classList.remove("isError");
|
|
701
759
|
await tryLoadWink();
|
|
702
760
|
session = await tmctPlan.createPlanSession({ diskCount: n, maxDepth: d });
|
|
703
761
|
if (session.plan) {
|
|
704
762
|
applyPlan(session.plan);
|
|
705
|
-
liveStatusEl.textContent = "live — " + n
|
|
763
|
+
liveStatusEl.textContent = "live — " + countLabel(n, "disk", "disks") + ", max depth " + d + ".";
|
|
706
764
|
} else {
|
|
707
765
|
liveStatusEl.textContent = "no plan found within " + d + " moves — raise max search depth and try again.";
|
|
766
|
+
liveStatusEl.classList.add("isError");
|
|
708
767
|
}
|
|
709
768
|
resolveBtn.disabled = false;
|
|
710
769
|
}));
|