@polycode-projects/the-mechanical-code-talker 2.7.3 → 2.7.5
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/package.json +2 -1
- package/src/adapters/memory/core.mjs +1 -0
- package/src/domain/router/drive.mjs +43 -17
- package/src/domain/router/planner.mjs +54 -4
- package/src/domain/router/resolver.mjs +88 -14
- package/src/domain/sprite-map.mjs +131 -0
- package/src/services/adventure.mjs +7 -0
- package/src/services/chat.mjs +25 -0
- package/src/services/spider-fly-turn.mjs +352 -0
- package/src/services/spider-fly-viz.mjs +492 -0
- package/src/services/spider-fly.mjs +491 -0
- package/src/services/viz-ticker.mjs +119 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +123 -85
- package/src/surfaces/web/spider-fly-browser-entry.mjs +152 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// spider-fly-viz.mjs — the spider-and-fly full-screen page (PLAN_SPIDER_FLY.md
|
|
2
|
+
// §8/§9/§11): a self-contained document shaped exactly like ledger-viz.mjs/
|
|
3
|
+
// plan-viz.mjs — one inlined <style> (importing viz-theme.mjs's shared
|
|
4
|
+
// tokens), behaviour as an inlined IIFE — but unlike those two, almost
|
|
5
|
+
// nothing is embedded as build-time data: the whole game is LIVE client-side
|
|
6
|
+
// state (spider-fly-browser-entry.mjs's createSpiderFlySession), so
|
|
7
|
+
// renderSpiderFlyHtml only needs the page's own static grid geometry (which
|
|
8
|
+
// never changes) plus a title. The engine, sprite resolver and chat turn
|
|
9
|
+
// engine all arrive via ./spider-fly-browser.bundle.js, referenced with a
|
|
10
|
+
// plain same-origin <script src> — the same sibling-file arrangement
|
|
11
|
+
// index.html already uses for chat-browser.bundle.js (both are the FULL
|
|
12
|
+
// turn engine, both generated fresh per build, neither meant to be
|
|
13
|
+
// committed), not memory-ask-browser.bundle.js's inlined-text arrangement
|
|
14
|
+
// (that bundle is small, committed, and inlined specifically so ledger.html
|
|
15
|
+
// stays portable on its own — neither reason applies here).
|
|
16
|
+
//
|
|
17
|
+
// renderSpiderFlyHtml() is pure: no I/O, deterministic output for identical
|
|
18
|
+
// input. scripts/build-demo-site.mjs calls it directly and writes the result
|
|
19
|
+
// to public/spider-fly.html, after building the sibling bundle.
|
|
20
|
+
//
|
|
21
|
+
// Three runtime pieces are spliced into the page's own inlined script via
|
|
22
|
+
// `.toString()` — exactly ledger-viz.mjs's own `facetCounts`/
|
|
23
|
+
// `resolveAnsweredTerm` pattern — because they are genuinely UI-only, with no
|
|
24
|
+
// reason to live inside the engine bundle: `createTicker` (viz-ticker.mjs,
|
|
25
|
+
// the shared play/pause/step/reset primitive), `classOfAgentId` and
|
|
26
|
+
// `threadCellsForSpiderPlan` (below — the silk-thread reconstruction, kept as
|
|
27
|
+
// real, independently-tested exports rather than raw inline-script text, the
|
|
28
|
+
// same discipline ledger-viz.mjs holds its own spliced helpers to). Sprite
|
|
29
|
+
// resolution, grid geometry and the chat turn engine all come from the
|
|
30
|
+
// bundle's own real ES exports instead, since (unlike ledger-viz, which
|
|
31
|
+
// reuses a FIXED shared bundle it can't extend for one page's own needs) this
|
|
32
|
+
// page ships its own dedicated bundle and can just export what it needs.
|
|
33
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
34
|
+
import { createTicker } from "./viz-ticker.mjs";
|
|
35
|
+
import { GRID_SIZE, WEB_HOME, WEB_RADIUS, isInWebBlock, cellId } from "../domain/spider-fly-world.mjs";
|
|
36
|
+
|
|
37
|
+
const CELL_PX = 44;
|
|
38
|
+
const BOARD_PX = CELL_PX * GRID_SIZE;
|
|
39
|
+
const DEFAULT_TITLE = "tmct — the spider and the fly";
|
|
40
|
+
const PREVIEW_MAX_TURNS = 40;
|
|
41
|
+
const TICK_WAIT_MS = 700;
|
|
42
|
+
|
|
43
|
+
function webCellIds() {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (let y = 1; y <= GRID_SIZE; y += 1) {
|
|
46
|
+
for (let x = 1; x <= GRID_SIZE; x += 1) {
|
|
47
|
+
if (isInWebBlock(x, y)) out.push(cellId(x, y));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** An agent id's class for sprite/HUD purposes — "spider-2" -> "spider",
|
|
54
|
+
* "fly-10" -> "fly", "egg-1" -> "egg". Self-contained (no outer refs),
|
|
55
|
+
* `.toString()`-splice safe. */
|
|
56
|
+
export function classOfAgentId(id) {
|
|
57
|
+
return String(id).replace(/-\d+$/, "");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reconstruct the spider's remaining silk-thread path — the sequence of
|
|
62
|
+
* cell ids from its CURRENT cell (after this tick's one executed step) to
|
|
63
|
+
* wherever `findActionPath` was aiming — from spider-fly.mjs's own
|
|
64
|
+
* `agents[spiderId]` shape ({ cell, plan }), where `plan` is the FULL
|
|
65
|
+
* direction list `findActionPath` returned (the step already taken this
|
|
66
|
+
* tick, `plan[0]`, plus every step still to come). Only a spider with a
|
|
67
|
+
* REAL multi-step plan draws a thread at all: spider-fly.mjs's own
|
|
68
|
+
* `planSpiderPath` only ever returns one when the believed fly cell sits
|
|
69
|
+
* inside the web block (its `isGoal`'s own requirement) — most ticks the
|
|
70
|
+
* spider is greedily closing distance with no such plan, and this
|
|
71
|
+
* correctly returns null then, same as "no proof chain to draw" elsewhere
|
|
72
|
+
* in this project's viz pages.
|
|
73
|
+
*
|
|
74
|
+
* `geometry` is `{ parseCellId, cellId, directionDelta }` — the exact three
|
|
75
|
+
* grid-geometry primitives spider-fly-world.mjs exports, injected rather
|
|
76
|
+
* than imported so this function stays `.toString()`-splice safe (the
|
|
77
|
+
* inlined page calls it with `window.tmctSpiderFly`'s own re-exports of the
|
|
78
|
+
* same three). Returns the ordered cell-id array (length > 1) or null.
|
|
79
|
+
*/
|
|
80
|
+
export function threadCellsForSpiderPlan(agents, geometry) {
|
|
81
|
+
const { parseCellId, cellId: toCellId, directionDelta } = geometry;
|
|
82
|
+
for (const [id, agent] of Object.entries(agents || {})) {
|
|
83
|
+
if (!/^spider-/.test(id) || !agent.plan || !agent.plan.length) continue;
|
|
84
|
+
const cells = [agent.cell];
|
|
85
|
+
let cur = parseCellId(agent.cell);
|
|
86
|
+
for (const direction of agent.plan.slice(1)) {
|
|
87
|
+
const delta = directionDelta[direction];
|
|
88
|
+
if (!delta || !cur) break;
|
|
89
|
+
cur = { x: cur.x + delta.dx, y: cur.y + delta.dy };
|
|
90
|
+
cells.push(toCellId(cur.x, cur.y));
|
|
91
|
+
}
|
|
92
|
+
if (cells.length > 1) return cells;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The self-contained spider-and-fly page. Pure — the same output for the
|
|
98
|
+
* same `title` every time; every other piece of state this page shows is
|
|
99
|
+
* computed live in the browser once the sibling bundle loads. `?preview=1`
|
|
100
|
+
* on the page's own URL switches it into the small, auto-playing,
|
|
101
|
+
* non-interactive mode the home page's hero iframe embeds (§11) — one file
|
|
102
|
+
* serves both the hero and the "open full-screen" link, matching how
|
|
103
|
+
* ledger.html/plan.html are each one file embedded two ways. */
|
|
104
|
+
export function renderSpiderFlyHtml({ title = DEFAULT_TITLE } = {}) {
|
|
105
|
+
const gridData = embedJson({
|
|
106
|
+
gridSize: GRID_SIZE,
|
|
107
|
+
webCells: webCellIds(),
|
|
108
|
+
webHome: WEB_HOME,
|
|
109
|
+
webRadius: WEB_RADIUS,
|
|
110
|
+
boardPx: BOARD_PX,
|
|
111
|
+
cellPx: CELL_PX,
|
|
112
|
+
previewMaxTurns: PREVIEW_MAX_TURNS,
|
|
113
|
+
tickWaitMs: TICK_WAIT_MS,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return `<!doctype html>
|
|
117
|
+
<html lang="en">
|
|
118
|
+
<head>
|
|
119
|
+
<meta charset="utf-8">
|
|
120
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
121
|
+
<title>${escapeHtml(title)}</title>
|
|
122
|
+
<style>
|
|
123
|
+
${THEME_TOKENS_CSS}
|
|
124
|
+
:root { --fly: #A6791F; }
|
|
125
|
+
@media (prefers-color-scheme: dark) { :root { --fly: #D9A94B; } }
|
|
126
|
+
:root[data-theme="dark"] { --fly: #D9A94B; }
|
|
127
|
+
:root[data-theme="light"] { --fly: #A6791F; }
|
|
128
|
+
html { background: var(--bg); }
|
|
129
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
|
|
130
|
+
.mono { font-family: ${MONO_STACK}; }
|
|
131
|
+
main { max-width: 980px; margin: 0 auto; padding: 1.4rem 1.2rem 2.2rem; }
|
|
132
|
+
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
133
|
+
h1 { font-size: 1.4rem; margin: .3rem 0 .9rem; text-wrap: balance; }
|
|
134
|
+
button { font: inherit; color: inherit; background: none; cursor: pointer; }
|
|
135
|
+
button:focus-visible, input:focus-visible, .sprite:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
136
|
+
.stage { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: 1rem; align-items: start; }
|
|
137
|
+
@media (max-width: 760px) { .stage { grid-template-columns: 1fr; } }
|
|
138
|
+
.board-frame { position: relative; width: ${BOARD_PX}px; max-width: 100%; aspect-ratio: 1 / 1; background: var(--card); border: 1px solid var(--line); }
|
|
139
|
+
.board-frame canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
140
|
+
.sprite-layer { position: absolute; inset: 0; }
|
|
141
|
+
.sprite { position: absolute; width: 7.6%; height: 7.6%; transform: translate(-50%, -50%); transition: left .25s ease, top .25s ease; }
|
|
142
|
+
@media (prefers-reduced-motion: reduce) { .sprite { transition: none; } }
|
|
143
|
+
.sprite svg { width: 100%; height: 100%; display: block; }
|
|
144
|
+
.sprite[data-cls="spider"] { color: var(--taught); }
|
|
145
|
+
.sprite[data-cls="fly"] { color: var(--fly); }
|
|
146
|
+
.sprite[data-cls="egg"] { color: var(--muted); }
|
|
147
|
+
.sprite.dimmed { opacity: .28; }
|
|
148
|
+
.thread-tip { position: absolute; transform: translate(-50%, -130%); font-family: ${MONO_STACK}; font-size: .68rem; background: var(--ink); color: var(--bg); padding: .1rem .4rem; border-radius: 3px; pointer-events: none; white-space: nowrap; display: none; }
|
|
149
|
+
.side { display: flex; flex-direction: column; gap: .8rem; min-width: 0; }
|
|
150
|
+
.hud, .chat { background: var(--card); border: 1px solid var(--line); padding: .6rem .75rem; }
|
|
151
|
+
.hud h2, .chat h2 { font-family: ${MONO_STACK}; font-size: .66rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-weight: 400; margin: 0 0 .5rem; }
|
|
152
|
+
.hud-row { display: flex; flex-direction: column; gap: .1rem; padding: .3rem 0; border-top: 1px solid var(--line); }
|
|
153
|
+
.hud-row:first-of-type { border-top: none; }
|
|
154
|
+
.hud-id { font-family: ${MONO_STACK}; font-size: .74rem; }
|
|
155
|
+
.hud-id.spider { color: var(--taught); } .hud-id.fly { color: var(--fly); } .hud-id.egg { color: var(--muted); }
|
|
156
|
+
.hud-goal { font-size: .85rem; }
|
|
157
|
+
.hud-empty { color: var(--muted); font-size: .85rem; }
|
|
158
|
+
.chatlog { display: flex; flex-direction: column; gap: .4rem; max-height: 220px; overflow-y: auto; margin-bottom: .5rem; }
|
|
159
|
+
.chatlog:empty { display: none; margin-bottom: 0; }
|
|
160
|
+
.chatlog .u { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); }
|
|
161
|
+
.chatlog .u::before { content: "tmct> "; color: var(--taught); }
|
|
162
|
+
.chatlog .a { font-size: .88rem; line-height: 1.4; white-space: pre-wrap; }
|
|
163
|
+
.chatask { display: flex; align-items: center; gap: .5rem; border-top: 1px solid var(--line); padding-top: .5rem; }
|
|
164
|
+
.chatlog:empty + .chatask { border-top: none; padding-top: 0; }
|
|
165
|
+
.chatask .prompt { color: var(--taught); font-size: .78rem; font-family: ${MONO_STACK}; }
|
|
166
|
+
.chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); padding: .32rem .55rem; min-width: 0; }
|
|
167
|
+
.chatask input:disabled { opacity: .5; }
|
|
168
|
+
.controls-row { display: flex; align-items: center; gap: .6rem; margin-top: 1rem; flex-wrap: wrap; }
|
|
169
|
+
.controls-row button { font-family: ${MONO_STACK}; font-size: .78rem; padding: .3rem .7rem; border: 1px solid var(--line); background: var(--card); color: var(--ink); }
|
|
170
|
+
.controls-row button:hover:not(:disabled) { border-color: var(--taught); }
|
|
171
|
+
.controls-row button:disabled { opacity: .4; cursor: default; }
|
|
172
|
+
.controls-row .turn { margin-left: auto; font-family: ${MONO_STACK}; font-size: .78rem; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
173
|
+
.status { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); margin-top: .5rem; }
|
|
174
|
+
body.preview .side, body.preview .controls-row, body.preview .status { display: none; }
|
|
175
|
+
body.preview main { padding: 0; max-width: none; }
|
|
176
|
+
body.preview .stage { display: block; }
|
|
177
|
+
body.preview .eyebrow, body.preview h1 { display: none; }
|
|
178
|
+
</style>
|
|
179
|
+
</head>
|
|
180
|
+
<body>
|
|
181
|
+
<main>
|
|
182
|
+
<div class="eyebrow">tmct · spider and fly</div>
|
|
183
|
+
<h1>A spider in its web, a fly on the board — each planning against the other</h1>
|
|
184
|
+
<div class="stage">
|
|
185
|
+
<div class="board-frame" id="boardFrame">
|
|
186
|
+
<canvas id="board" width="${BOARD_PX}" height="${BOARD_PX}" aria-label="the 10x10 board"></canvas>
|
|
187
|
+
<canvas id="pov" width="${BOARD_PX}" height="${BOARD_PX}" aria-hidden="true"></canvas>
|
|
188
|
+
<div class="sprite-layer" id="spriteLayer"></div>
|
|
189
|
+
<div class="thread-tip" id="threadTip"></div>
|
|
190
|
+
</div>
|
|
191
|
+
<aside class="side" aria-label="Agents and chat">
|
|
192
|
+
<div class="hud">
|
|
193
|
+
<h2>agents</h2>
|
|
194
|
+
<div id="hud"></div>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="chat">
|
|
197
|
+
<h2>tell the spider or the fly something</h2>
|
|
198
|
+
<div class="chatlog" id="chatlog" aria-live="polite"></div>
|
|
199
|
+
<form class="chatask" id="chatform">
|
|
200
|
+
<span class="prompt mono">tmct></span>
|
|
201
|
+
<input id="chatq" type="text" placeholder="@spider the fly is east" aria-label="Address the spider or the fly" disabled>
|
|
202
|
+
</form>
|
|
203
|
+
</div>
|
|
204
|
+
</aside>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="controls-row">
|
|
207
|
+
<button id="resetBtn" type="button" disabled>reset</button>
|
|
208
|
+
<button id="playBtn" type="button" disabled>▶ play</button>
|
|
209
|
+
<button id="stepBtn" type="button" disabled>step</button>
|
|
210
|
+
<span class="turn mono" id="turnLabel">turn: 0</span>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="status" id="status">loading the engine…</div>
|
|
213
|
+
</main>
|
|
214
|
+
<script>
|
|
215
|
+
const SPIDERFLY = ${gridData};
|
|
216
|
+
</script>
|
|
217
|
+
<script src="./spider-fly-browser.bundle.js"></script>
|
|
218
|
+
<script>
|
|
219
|
+
(function () {
|
|
220
|
+
"use strict";
|
|
221
|
+
const createTicker = ${createTicker.toString()};
|
|
222
|
+
const classOfAgentId = ${classOfAgentId.toString()};
|
|
223
|
+
const threadCellsForSpiderPlan = ${threadCellsForSpiderPlan.toString()};
|
|
224
|
+
const esc = ${escapeHtml.toString()};
|
|
225
|
+
const el = (id) => document.getElementById(id);
|
|
226
|
+
const boardFrame = el("boardFrame");
|
|
227
|
+
const boardCanvas = el("board");
|
|
228
|
+
const povCanvas = el("pov");
|
|
229
|
+
const spriteLayer = el("spriteLayer");
|
|
230
|
+
const threadTip = el("threadTip");
|
|
231
|
+
const hudEl = el("hud");
|
|
232
|
+
const chatlogEl = el("chatlog");
|
|
233
|
+
const chatformEl = el("chatform");
|
|
234
|
+
const chatqEl = el("chatq");
|
|
235
|
+
const statusEl = el("status");
|
|
236
|
+
const turnLabelEl = el("turnLabel");
|
|
237
|
+
const resetBtn = el("resetBtn");
|
|
238
|
+
const playBtn = el("playBtn");
|
|
239
|
+
const stepBtn = el("stepBtn");
|
|
240
|
+
|
|
241
|
+
const params = new URLSearchParams(location.search);
|
|
242
|
+
const preview = params.get("preview") === "1";
|
|
243
|
+
document.body.classList.toggle("preview", preview);
|
|
244
|
+
|
|
245
|
+
const dpr = window.devicePixelRatio || 1;
|
|
246
|
+
const boardCtx = boardCanvas.getContext("2d");
|
|
247
|
+
const povCtx = povCanvas.getContext("2d");
|
|
248
|
+
function sizeCanvas(canvas, ctx) {
|
|
249
|
+
canvas.width = SPIDERFLY.boardPx * dpr;
|
|
250
|
+
canvas.height = SPIDERFLY.boardPx * dpr;
|
|
251
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
252
|
+
}
|
|
253
|
+
sizeCanvas(boardCanvas, boardCtx);
|
|
254
|
+
sizeCanvas(povCanvas, povCtx);
|
|
255
|
+
|
|
256
|
+
const cssVar = (name) => getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
257
|
+
|
|
258
|
+
// ---- board geometry ---------------------------------------------------
|
|
259
|
+
const cellSize = SPIDERFLY.boardPx / SPIDERFLY.gridSize;
|
|
260
|
+
function cellCenterPct(x, y) {
|
|
261
|
+
return { leftPct: ((x - 0.5) / SPIDERFLY.gridSize) * 100, topPct: ((y - 0.5) / SPIDERFLY.gridSize) * 100 };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---- state shared across redraws --------------------------------------
|
|
265
|
+
let session = null;
|
|
266
|
+
let lastAgents = {};
|
|
267
|
+
let lastTurn = 0;
|
|
268
|
+
const goalById = {};
|
|
269
|
+
const spriteEls = {};
|
|
270
|
+
let povAgentId = null;
|
|
271
|
+
let threadHits = [];
|
|
272
|
+
|
|
273
|
+
function removeStaleSprites(agents) {
|
|
274
|
+
for (const id of Object.keys(spriteEls)) {
|
|
275
|
+
if (!agents[id]) { spriteEls[id].remove(); delete spriteEls[id]; delete goalById[id]; }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function togglePov(id) {
|
|
280
|
+
povAgentId = povAgentId === id ? null : id;
|
|
281
|
+
if (!povAgentId) for (const node of Object.values(spriteEls)) node.classList.remove("dimmed");
|
|
282
|
+
drawPov();
|
|
283
|
+
}
|
|
284
|
+
document.addEventListener("keydown", (e) => {
|
|
285
|
+
if (e.key === "Escape" && povAgentId) togglePov(povAgentId);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
function ensureSpriteEl(id, cls) {
|
|
289
|
+
let node = spriteEls[id];
|
|
290
|
+
if (node) return node;
|
|
291
|
+
node = document.createElement("div");
|
|
292
|
+
node.className = "sprite";
|
|
293
|
+
node.dataset.cls = cls;
|
|
294
|
+
const sprite = window.tmctSpiderFly
|
|
295
|
+
? tmctSpiderFly.resolveSpriteForClass(cls, (session && session.taxonomyRows) || [], tmctSpiderFly.SPRITE_REGISTRY)
|
|
296
|
+
: "";
|
|
297
|
+
node.innerHTML = sprite;
|
|
298
|
+
if (!preview) {
|
|
299
|
+
node.tabIndex = 0;
|
|
300
|
+
node.setAttribute("role", "button");
|
|
301
|
+
node.setAttribute("aria-label", "show " + id + "\\u2019s point of view");
|
|
302
|
+
node.addEventListener("click", () => togglePov(id));
|
|
303
|
+
node.addEventListener("keydown", (e) => {
|
|
304
|
+
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); togglePov(id); }
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
spriteLayer.appendChild(node);
|
|
308
|
+
spriteEls[id] = node;
|
|
309
|
+
return node;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function applyAgents(agents) {
|
|
313
|
+
removeStaleSprites(agents);
|
|
314
|
+
for (const [id, a] of Object.entries(agents)) {
|
|
315
|
+
const cls = classOfAgentId(id);
|
|
316
|
+
const node = ensureSpriteEl(id, cls);
|
|
317
|
+
const parsed = tmctSpiderFly.parseCellId(a.cell);
|
|
318
|
+
if (!parsed) continue;
|
|
319
|
+
const pct = cellCenterPct(parsed.x, parsed.y);
|
|
320
|
+
node.style.left = pct.leftPct + "%";
|
|
321
|
+
node.style.top = pct.topPct + "%";
|
|
322
|
+
if (a.goal) goalById[id] = a.goal;
|
|
323
|
+
}
|
|
324
|
+
lastAgents = agents;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function renderHud() {
|
|
328
|
+
const ids = Object.keys(lastAgents).sort();
|
|
329
|
+
if (!ids.length) { hudEl.innerHTML = '<div class="hud-empty">no agents on the board.</div>'; return; }
|
|
330
|
+
hudEl.innerHTML = ids.map((id) => {
|
|
331
|
+
const cls = classOfAgentId(id);
|
|
332
|
+
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></div>";
|
|
334
|
+
}).join("");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const threadGeometry = {
|
|
338
|
+
parseCellId: (id) => tmctSpiderFly.parseCellId(id),
|
|
339
|
+
cellId: (x, y) => tmctSpiderFly.cellId(x, y),
|
|
340
|
+
directionDelta: tmctSpiderFly.DIRECTION_DELTA,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
function drawBoard(agents) {
|
|
344
|
+
const w = SPIDERFLY.boardPx, h = SPIDERFLY.boardPx;
|
|
345
|
+
boardCtx.clearRect(0, 0, w, h);
|
|
346
|
+
boardCtx.fillStyle = cssVar("--taught-soft") || "rgba(46,125,79,.12)";
|
|
347
|
+
for (const wc of SPIDERFLY.webCells) {
|
|
348
|
+
const p = tmctSpiderFly.parseCellId(wc);
|
|
349
|
+
boardCtx.fillRect((p.x - 1) * cellSize, (p.y - 1) * cellSize, cellSize, cellSize);
|
|
350
|
+
}
|
|
351
|
+
boardCtx.strokeStyle = cssVar("--line") || "#DDD9D0";
|
|
352
|
+
boardCtx.lineWidth = 1;
|
|
353
|
+
for (let i = 0; i <= SPIDERFLY.gridSize; i += 1) {
|
|
354
|
+
boardCtx.beginPath(); boardCtx.moveTo(i * cellSize, 0); boardCtx.lineTo(i * cellSize, h); boardCtx.stroke();
|
|
355
|
+
boardCtx.beginPath(); boardCtx.moveTo(0, i * cellSize); boardCtx.lineTo(w, i * cellSize); boardCtx.stroke();
|
|
356
|
+
}
|
|
357
|
+
threadHits = [];
|
|
358
|
+
const thread = threadCellsForSpiderPlan(agents, threadGeometry);
|
|
359
|
+
if (thread) {
|
|
360
|
+
boardCtx.strokeStyle = cssVar("--taught") || "#2E7D4F";
|
|
361
|
+
boardCtx.lineWidth = 2;
|
|
362
|
+
boardCtx.beginPath();
|
|
363
|
+
let prev = null;
|
|
364
|
+
thread.forEach((c, i) => {
|
|
365
|
+
const p = tmctSpiderFly.parseCellId(c);
|
|
366
|
+
const px = (p.x - 0.5) * cellSize, py = (p.y - 0.5) * cellSize;
|
|
367
|
+
if (i === 0) boardCtx.moveTo(px, py); else boardCtx.lineTo(px, py);
|
|
368
|
+
if (prev) threadHits.push({ x: (prev.x + px) / 2, y: (prev.y + py) / 2, step: i });
|
|
369
|
+
prev = { x: px, y: py };
|
|
370
|
+
});
|
|
371
|
+
boardCtx.stroke();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function drawPov() {
|
|
376
|
+
povCtx.clearRect(0, 0, SPIDERFLY.boardPx, SPIDERFLY.boardPx);
|
|
377
|
+
if (!povAgentId) return;
|
|
378
|
+
const agent = lastAgents[povAgentId];
|
|
379
|
+
if (!agent) { povAgentId = null; return; }
|
|
380
|
+
const p = tmctSpiderFly.parseCellId(agent.cell);
|
|
381
|
+
const visible = new Set(tmctSpiderFly.visibleCells(p.x, p.y, tmctSpiderFly.DEFAULT_VISION_RADIUS));
|
|
382
|
+
povCtx.fillStyle = "rgba(0,0,0,.55)";
|
|
383
|
+
for (let gy = 1; gy <= SPIDERFLY.gridSize; gy += 1) {
|
|
384
|
+
for (let gx = 1; gx <= SPIDERFLY.gridSize; gx += 1) {
|
|
385
|
+
if (visible.has(tmctSpiderFly.cellId(gx, gy))) continue;
|
|
386
|
+
povCtx.fillRect((gx - 1) * cellSize, (gy - 1) * cellSize, cellSize, cellSize);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
for (const [id, node] of Object.entries(spriteEls)) {
|
|
390
|
+
const a = lastAgents[id];
|
|
391
|
+
node.classList.toggle("dimmed", id !== povAgentId && !!a && !visible.has(a.cell));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
boardFrame.addEventListener("mousemove", (e) => {
|
|
396
|
+
if (!threadHits.length) { threadTip.style.display = "none"; return; }
|
|
397
|
+
const rect = boardFrame.getBoundingClientRect();
|
|
398
|
+
const scale = SPIDERFLY.boardPx / rect.width;
|
|
399
|
+
const mx = (e.clientX - rect.left) * scale, my = (e.clientY - rect.top) * scale;
|
|
400
|
+
const hit = threadHits.find((h) => (h.x - mx) ** 2 + (h.y - my) ** 2 <= 196);
|
|
401
|
+
if (!hit) { threadTip.style.display = "none"; return; }
|
|
402
|
+
threadTip.textContent = "step " + hit.step;
|
|
403
|
+
threadTip.style.left = ((hit.x / SPIDERFLY.boardPx) * 100) + "%";
|
|
404
|
+
threadTip.style.top = ((hit.y / SPIDERFLY.boardPx) * 100) + "%";
|
|
405
|
+
threadTip.style.display = "block";
|
|
406
|
+
});
|
|
407
|
+
boardFrame.addEventListener("mouseleave", () => { threadTip.style.display = "none"; });
|
|
408
|
+
|
|
409
|
+
function redraw(agents, turn) {
|
|
410
|
+
applyAgents(agents);
|
|
411
|
+
renderHud();
|
|
412
|
+
lastTurn = turn;
|
|
413
|
+
turnLabelEl.textContent = "turn: " + turn;
|
|
414
|
+
drawBoard(agents);
|
|
415
|
+
drawPov();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ---- chat dock ---------------------------------------------------------
|
|
419
|
+
function addChatLine(cls, html) {
|
|
420
|
+
const d = document.createElement("div");
|
|
421
|
+
d.className = cls; d.innerHTML = html;
|
|
422
|
+
chatlogEl.appendChild(d); chatlogEl.scrollTop = chatlogEl.scrollHeight;
|
|
423
|
+
}
|
|
424
|
+
chatformEl.addEventListener("submit", (e) => {
|
|
425
|
+
e.preventDefault();
|
|
426
|
+
const q = chatqEl.value.trim();
|
|
427
|
+
if (!q || !session) return;
|
|
428
|
+
chatqEl.value = "";
|
|
429
|
+
addChatLine("u", esc(q));
|
|
430
|
+
withLock(async () => {
|
|
431
|
+
const result = await session.turn(q);
|
|
432
|
+
addChatLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
|
|
433
|
+
const snap = await session.snapshot();
|
|
434
|
+
redraw(snap.agents, snap.turn);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ---- serialize every engine-touching call: the ticker and the chat dock
|
|
439
|
+
// share one in-memory store, and an overlapping tick()/turn() pair could
|
|
440
|
+
// race against the same @turnN write.
|
|
441
|
+
let lock = Promise.resolve();
|
|
442
|
+
function withLock(fn) {
|
|
443
|
+
const run = lock.then(fn, fn);
|
|
444
|
+
lock = run.catch(() => {});
|
|
445
|
+
return run;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function boot() {
|
|
449
|
+
session = await tmctSpiderFly.createSpiderFlySession();
|
|
450
|
+
redraw(session.initial.agents, session.initial.turn);
|
|
451
|
+
statusEl.textContent = session.opening;
|
|
452
|
+
chatqEl.disabled = false;
|
|
453
|
+
resetBtn.disabled = false; playBtn.disabled = false; stepBtn.disabled = false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let loopScheduled = false;
|
|
457
|
+
const ticker = createTicker({
|
|
458
|
+
onTick: () => withLock(async () => {
|
|
459
|
+
const result = await session.tick();
|
|
460
|
+
redraw(result.agents, result.turn);
|
|
461
|
+
}),
|
|
462
|
+
onRender: (state) => {
|
|
463
|
+
playBtn.textContent = state.playing ? "\\u23f8 pause" : "\\u25b6 play";
|
|
464
|
+
playBtn.disabled = state.animating;
|
|
465
|
+
stepBtn.disabled = state.animating || state.playing;
|
|
466
|
+
resetBtn.disabled = state.animating;
|
|
467
|
+
if (preview && !state.playing && !state.animating && lastTurn >= SPIDERFLY.previewMaxTurns && !loopScheduled) {
|
|
468
|
+
loopScheduled = true;
|
|
469
|
+
setTimeout(() => { loopScheduled = false; ticker.reset().then(() => ticker.play()); }, 1200);
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
onReset: () => withLock(boot),
|
|
473
|
+
hasNext: () => !preview || lastTurn < SPIDERFLY.previewMaxTurns,
|
|
474
|
+
waitMs: SPIDERFLY.tickWaitMs,
|
|
475
|
+
});
|
|
476
|
+
playBtn.addEventListener("click", () => ticker.play());
|
|
477
|
+
stepBtn.addEventListener("click", () => ticker.stepOnce());
|
|
478
|
+
resetBtn.addEventListener("click", () => ticker.reset());
|
|
479
|
+
|
|
480
|
+
boot().then(() => { if (preview) ticker.play(); });
|
|
481
|
+
|
|
482
|
+
new MutationObserver(() => { drawBoard(lastAgents); drawPov(); })
|
|
483
|
+
.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
|
|
484
|
+
if (window.matchMedia) {
|
|
485
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { drawBoard(lastAgents); drawPov(); });
|
|
486
|
+
}
|
|
487
|
+
})();
|
|
488
|
+
</script>
|
|
489
|
+
</body>
|
|
490
|
+
</html>
|
|
491
|
+
`;
|
|
492
|
+
}
|