@polycode-projects/the-mechanical-code-talker 2.8.1 → 2.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/domain/game-config.mjs +10 -4
- package/src/domain/hanoi-lesson.mjs +53 -0
- package/src/domain/spider-fly-world.mjs +16 -0
- package/src/services/adventure-editor.mjs +361 -0
- package/src/services/adventure-viz.mjs +422 -51
- package/src/services/plan-pddl.mjs +245 -0
- package/src/services/plan-viz.mjs +324 -67
- package/src/services/spider-fly-turn.mjs +120 -3
- package/src/services/spider-fly-viz.mjs +341 -22
- package/src/services/spider-fly.mjs +337 -143
- package/src/surfaces/web/adventure-browser-entry.mjs +34 -3
- package/src/surfaces/web/memory-ask-browser.bundle.js +10 -4
- package/src/surfaces/web/plan-browser-entry.mjs +114 -0
- package/src/surfaces/web/spider-fly-browser-entry.mjs +33 -1
|
@@ -18,14 +18,16 @@
|
|
|
18
18
|
// input. scripts/build-demo-site.mjs calls it directly and writes the result
|
|
19
19
|
// to public/spider-fly.html, after building the sibling bundle.
|
|
20
20
|
//
|
|
21
|
-
//
|
|
21
|
+
// Runtime pieces are spliced into the page's own inlined script via
|
|
22
22
|
// `.toString()` — exactly ledger-viz.mjs's own `facetCounts`/
|
|
23
23
|
// `resolveAnsweredTerm` pattern — because they are genuinely UI-only, with no
|
|
24
24
|
// reason to live inside the engine bundle: `createTicker` (viz-ticker.mjs,
|
|
25
|
-
// the shared play/pause/step/reset primitive), `classOfAgentId
|
|
26
|
-
// `threadCellsForSpiderPlan` (
|
|
27
|
-
//
|
|
28
|
-
//
|
|
25
|
+
// the shared play/pause/step/reset primitive), `classOfAgentId`,
|
|
26
|
+
// `threadCellsForSpiderPlan` (the silk-thread reconstruction), `nextCorpses`
|
|
27
|
+
// (the dying-agent bookkeeping behind the dusty-corner corpse pile) and
|
|
28
|
+
// `facingDegreesFor` (plan-driven sprite orientation) — all kept as real,
|
|
29
|
+
// independently-tested exports rather than raw inline-script text, the same
|
|
30
|
+
// discipline ledger-viz.mjs holds its own spliced helpers to. Sprite
|
|
29
31
|
// resolution, grid geometry and the chat turn engine all come from the
|
|
30
32
|
// bundle's own real ES exports instead, since (unlike ledger-viz, which
|
|
31
33
|
// reuses a FIXED shared bundle it can't extend for one page's own needs) this
|
|
@@ -33,13 +35,22 @@
|
|
|
33
35
|
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
34
36
|
import { createTicker } from "./viz-ticker.mjs";
|
|
35
37
|
import { GRID_SIZE, WEB_HOME, WEB_RADIUS, isInWebBlock, cellId } from "../domain/spider-fly-world.mjs";
|
|
36
|
-
import { FLY_INITIAL_MASS,
|
|
38
|
+
import { FLY_INITIAL_MASS, EGG_LAY_MASS_THRESHOLD } from "./spider-fly.mjs";
|
|
39
|
+
import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
// A larger cell than this page's first cut (44px) — the board now fills
|
|
42
|
+
// noticeably more of the stage's own width, closing most of the dead gap a
|
|
43
|
+
// fixed-260px side column used to leave next to a small fixed-440px board.
|
|
44
|
+
const CELL_PX = 54;
|
|
39
45
|
const BOARD_PX = CELL_PX * GRID_SIZE;
|
|
40
46
|
const DEFAULT_TITLE = "tmct — the spider and the fly";
|
|
41
47
|
const PREVIEW_MAX_TURNS = 40;
|
|
42
48
|
const TICK_WAIT_MS = 700;
|
|
49
|
+
// How many turns a corpse lingers at the bottom of its own board column
|
|
50
|
+
// before the carcass "rots" (stops being drawn at all) — a page-local
|
|
51
|
+
// constant, not a game-config knob: purely cosmetic, never read by the
|
|
52
|
+
// engine, so a visitor's carrying/hatch mechanics never depend on it.
|
|
53
|
+
const CORPSE_LINGER_TURNS = 4;
|
|
43
54
|
|
|
44
55
|
function webCellIds() {
|
|
45
56
|
const out = [];
|
|
@@ -95,6 +106,57 @@ export function threadCellsForSpiderPlan(agents, geometry) {
|
|
|
95
106
|
return null;
|
|
96
107
|
}
|
|
97
108
|
|
|
109
|
+
/** The sprite-facing rotation (degrees) for one agent this tick, driven by
|
|
110
|
+
* its CURRENT plan's first step (spider-fly.mjs's own `agents[id].plan`),
|
|
111
|
+
* never its actual next move — the two usually coincide, but re-planning
|
|
112
|
+
* fresh every tick means they can visibly diverge as a plan gets clobbered
|
|
113
|
+
* and replaced, which is the intended, honest demonstration of "plans get
|
|
114
|
+
* clobbered under partial/unreliable knowledge," not a glitch to smooth
|
|
115
|
+
* over. A held agent (`plan` empty/absent — no direction to face) keeps
|
|
116
|
+
* `previousDegrees` unchanged rather than snapping back to a default, so
|
|
117
|
+
* holding still never spins the sprite; a brand-new agent with no prior
|
|
118
|
+
* facing at all defaults to 0 (the art's own default north/up pose).
|
|
119
|
+
* Self-contained (no outer refs), `.toString()`-splice safe — the
|
|
120
|
+
* direction -> degrees table lives INSIDE the function body on purpose:
|
|
121
|
+
* spider.toml and fly.toml both draw the head/thorax at the TOP of the
|
|
122
|
+
* viewBox (facing north by default), matching spider-fly-world.mjs's own
|
|
123
|
+
* DIRECTION_DELTA (north decreases y — up on screen). */
|
|
124
|
+
export function facingDegreesFor(plan, previousDegrees) {
|
|
125
|
+
const FACING_DEGREES = { north: 0, east: 90, south: 180, west: 270 };
|
|
126
|
+
const direction = plan && plan[0];
|
|
127
|
+
if (direction && FACING_DEGREES[direction] !== undefined) return FACING_DEGREES[direction];
|
|
128
|
+
return previousDegrees ?? 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The updated corpse set for one redraw (§A.2.5 — visual-only, entirely
|
|
133
|
+
* client-side; the actual starve/eat removal already happened in the
|
|
134
|
+
* engine). Every id present in `prevAgents` but absent from `agents` died
|
|
135
|
+
* THIS tick — eaten or starved are the only two ways an agent ever leaves
|
|
136
|
+
* the engine's own returned roster — and is added at its last-known cell
|
|
137
|
+
* and class; every corpse already older than `lingerTurns` past its own
|
|
138
|
+
* death turn is dropped first, so the set never grows without bound.
|
|
139
|
+
* Returns a plain `{ [id]: { cls, cell, diedAtTurn } }` map. Pure.
|
|
140
|
+
* `lingerTurns` defaults to a literal 4 (not the module-level
|
|
141
|
+
* CORPSE_LINGER_TURNS constant) so this function stays fully
|
|
142
|
+
* `.toString()`-splice safe — every real caller (both the inlined page and
|
|
143
|
+
* CORPSE_LINGER_TURNS's own callers elsewhere in this module) passes it
|
|
144
|
+
* explicitly anyway.
|
|
145
|
+
*/
|
|
146
|
+
export function nextCorpses(prevCorpses, prevAgents, agents, turn, lingerTurns = 4) {
|
|
147
|
+
const out = {};
|
|
148
|
+
for (const [id, corpse] of Object.entries(prevCorpses || {})) {
|
|
149
|
+
if (turn - corpse.diedAtTurn <= lingerTurns) out[id] = corpse;
|
|
150
|
+
}
|
|
151
|
+
for (const id of Object.keys(prevAgents || {})) {
|
|
152
|
+
if (agents[id] || out[id]) continue;
|
|
153
|
+
const cell = prevAgents[id]?.cell;
|
|
154
|
+
if (!cell) continue;
|
|
155
|
+
out[id] = { cls: classOfAgentId(id), cell, diedAtTurn: turn };
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
98
160
|
/** The self-contained spider-and-fly page. Pure — the same output for the
|
|
99
161
|
* same `title`/`spriteTemplates` every time; every other piece of state
|
|
100
162
|
* this page shows is computed live in the browser once the sibling bundle
|
|
@@ -117,8 +179,15 @@ export function renderSpiderFlyHtml({ title = DEFAULT_TITLE, spriteTemplates = [
|
|
|
117
179
|
cellPx: CELL_PX,
|
|
118
180
|
previewMaxTurns: PREVIEW_MAX_TURNS,
|
|
119
181
|
tickWaitMs: TICK_WAIT_MS,
|
|
182
|
+
corpseLingerTurns: CORPSE_LINGER_TURNS,
|
|
120
183
|
maxFlyMass: FLY_INITIAL_MASS,
|
|
121
|
-
|
|
184
|
+
// The spider's mass bar now scales against the EGG-LAY threshold, not
|
|
185
|
+
// its own starting mass — "how close to laying" is the meaningful cap
|
|
186
|
+
// to visualize under the new mass-gated lay mechanic (§A.2.2); the old
|
|
187
|
+
// denominator (a flat starting mass) said nothing about progress toward
|
|
188
|
+
// the spider's actual goal.
|
|
189
|
+
maxSpiderMass: EGG_LAY_MASS_THRESHOLD,
|
|
190
|
+
defaultConfig: DEFAULT_GAME_CONFIG.spiderFly,
|
|
122
191
|
spriteTemplates,
|
|
123
192
|
});
|
|
124
193
|
|
|
@@ -137,32 +206,58 @@ ${THEME_TOKENS_CSS}
|
|
|
137
206
|
html { background: var(--bg); }
|
|
138
207
|
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
|
|
139
208
|
.mono { font-family: ${MONO_STACK}; }
|
|
140
|
-
main { max-width:
|
|
209
|
+
main { max-width: 1120px; margin: 0 auto; padding: 1.4rem 1.2rem 2.2rem; }
|
|
141
210
|
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
142
211
|
h1 { font-size: 1.4rem; margin: .3rem 0 .9rem; text-wrap: balance; }
|
|
143
212
|
button { font: inherit; color: inherit; background: none; cursor: pointer; }
|
|
144
213
|
button:focus-visible, input:focus-visible, .sprite:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
145
|
-
.stage { display: grid; grid-template-columns: minmax(0, 1fr)
|
|
214
|
+
.stage { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 360px); gap: 1.2rem; align-items: start; }
|
|
146
215
|
@media (max-width: 760px) { .stage { grid-template-columns: 1fr; } }
|
|
147
|
-
|
|
216
|
+
/* A dusty window corner: a soft light glow near the top-left (WEB_HOME
|
|
217
|
+
already sits near that corner — spider-fly-world.mjs's own header
|
|
218
|
+
comment), and a faint diagonal weave standing in for dust/silk caught
|
|
219
|
+
in the light. Decoration only — the 10x10 game grid itself is drawn by
|
|
220
|
+
drawBoard() on the canvas beneath, unchanged. */
|
|
221
|
+
.board-frame {
|
|
222
|
+
position: relative; width: ${BOARD_PX}px; max-width: 100%; aspect-ratio: 1 / 1;
|
|
223
|
+
background:
|
|
224
|
+
radial-gradient(140% 140% at 6% 6%, rgba(255, 241, 199, .55), transparent 52%),
|
|
225
|
+
repeating-linear-gradient(115deg, rgba(120, 110, 90, .06) 0 1px, transparent 1px 30px),
|
|
226
|
+
var(--card);
|
|
227
|
+
border: 1px solid var(--line);
|
|
228
|
+
box-shadow: inset 0 0 0 6px var(--bg), inset 0 0 0 7px var(--line);
|
|
229
|
+
}
|
|
230
|
+
@media (prefers-color-scheme: dark) { .board-frame { background: radial-gradient(140% 140% at 6% 6%, rgba(130, 112, 60, .32), transparent 52%), repeating-linear-gradient(115deg, rgba(255, 255, 255, .045) 0 1px, transparent 1px 30px), var(--card); } }
|
|
231
|
+
:root[data-theme="dark"] .board-frame { background: radial-gradient(140% 140% at 6% 6%, rgba(130, 112, 60, .32), transparent 52%), repeating-linear-gradient(115deg, rgba(255, 255, 255, .045) 0 1px, transparent 1px 30px), var(--card); }
|
|
148
232
|
.board-frame canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
149
233
|
.sprite-layer { position: absolute; inset: 0; }
|
|
150
234
|
.sprite { position: absolute; width: 7.6%; height: 7.6%; transform: translate(-50%, -50%); transition: left .25s ease, top .25s ease; }
|
|
151
|
-
@media (prefers-reduced-motion: reduce) { .sprite { transition: none; } }
|
|
152
|
-
.sprite
|
|
235
|
+
@media (prefers-reduced-motion: reduce) { .sprite, .sprite-face { transition: none !important; } }
|
|
236
|
+
.sprite-face { width: 100%; height: 100%; transition: transform .25s ease; }
|
|
237
|
+
.sprite-face svg { width: 100%; height: 100%; display: block; }
|
|
153
238
|
.sprite[data-cls="spider"] { color: var(--taught); }
|
|
154
239
|
.sprite[data-cls="fly"] { color: var(--fly); }
|
|
155
240
|
.sprite[data-cls="egg"] { color: var(--muted); }
|
|
156
241
|
.sprite.dimmed { opacity: .28; }
|
|
242
|
+
/* A corpse never faces anywhere in particular (the rot has no plan) and
|
|
243
|
+
never rotates — grayscale-and-fade at the bottom of its own column
|
|
244
|
+
until CORPSE_LINGER_TURNS passes and it stops being drawn at all. */
|
|
245
|
+
.sprite.corpse { filter: grayscale(1); opacity: .38; pointer-events: none; }
|
|
246
|
+
.sprite.corpse .sprite-face { transform: none !important; }
|
|
157
247
|
.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; }
|
|
158
248
|
.side { display: flex; flex-direction: column; gap: .8rem; min-width: 0; }
|
|
159
|
-
.hud, .chat { background: var(--card); border: 1px solid var(--line); padding: .6rem .75rem; }
|
|
160
|
-
.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; }
|
|
161
|
-
|
|
249
|
+
.hud, .chat, .tuning { background: var(--card); border: 1px solid var(--line); padding: .6rem .75rem; }
|
|
250
|
+
.hud h2, .chat h2, .tuning h2 { font-family: ${MONO_STACK}; font-size: .66rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-weight: 400; margin: 0 0 .5rem; }
|
|
251
|
+
/* Fixed-height, internally-scrolling — a hatch can mint several spiders
|
|
252
|
+
at once now, so the agent count (and a naive card list's own height)
|
|
253
|
+
can jump sharply; the panel must never grow the page underneath it. */
|
|
254
|
+
.hud-list { max-height: 420px; overflow-y: auto; }
|
|
255
|
+
.hud-row { display: flex; flex-direction: column; gap: .1rem; padding: .4rem 0; border-top: 1px solid var(--line); }
|
|
162
256
|
.hud-row:first-of-type { border-top: none; }
|
|
163
257
|
.hud-id { font-family: ${MONO_STACK}; font-size: .74rem; }
|
|
164
258
|
.hud-id.spider { color: var(--taught); } .hud-id.fly { color: var(--fly); } .hud-id.egg { color: var(--muted); }
|
|
165
259
|
.hud-goal { font-size: .85rem; }
|
|
260
|
+
.hud-plan, .hud-belief { font-family: ${MONO_STACK}; font-size: .66rem; color: var(--muted); margin-top: .2rem; line-height: 1.4; }
|
|
166
261
|
.mass-track { height: 4px; margin-top: .3rem; background: var(--line); border-radius: 2px; overflow: hidden; }
|
|
167
262
|
.mass-fill { height: 100%; background: var(--taught); }
|
|
168
263
|
.mass-fill.fly { background: var(--fly); }
|
|
@@ -182,13 +277,33 @@ ${THEME_TOKENS_CSS}
|
|
|
182
277
|
.pill:hover:not(:disabled) { border-color: var(--taught); }
|
|
183
278
|
.pill:disabled { opacity: .45; cursor: default; }
|
|
184
279
|
.pill[data-role="addr"].active { border-color: var(--taught); color: var(--taught); }
|
|
280
|
+
/* The dynamic deception-pill rail (§A.2.4): a true/false tag shown ONLY
|
|
281
|
+
here, via border style/color and a small human-facing glyph — the
|
|
282
|
+
submitted sentence itself (data-sentence, filled into #chatq on click)
|
|
283
|
+
never carries the tag, so a clicked pill is indistinguishable from a
|
|
284
|
+
hand-typed claim once it's in the input. */
|
|
285
|
+
.dynpills { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .5rem; padding-top: .5rem; border-top: 1px solid var(--line); }
|
|
286
|
+
.dynpills:empty { display: none; padding-top: 0; border-top: none; }
|
|
287
|
+
.pill[data-role="dyn-addr"][data-active="1"] { border-color: var(--taught); color: var(--taught); }
|
|
288
|
+
.pill[data-role="dyn-claim"][data-truth="true"] { border-color: var(--taught-t2, var(--taught)); }
|
|
289
|
+
.pill[data-role="dyn-claim"][data-truth="true"]::before { content: "✓ "; opacity: .5; }
|
|
290
|
+
.pill[data-role="dyn-claim"][data-truth="false"] { border-style: dashed; border-color: var(--alert); }
|
|
291
|
+
.pill[data-role="dyn-claim"][data-truth="false"]::before { content: "✕ "; opacity: .6; }
|
|
292
|
+
.tuning-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1rem; }
|
|
293
|
+
.tuning-col h3 { font-size: .74rem; margin: 0 0 .4rem; font-weight: 600; }
|
|
294
|
+
.tuning-col.spider h3 { color: var(--taught); } .tuning-col.fly h3 { color: var(--fly); }
|
|
295
|
+
.tuning-col label { display: block; font-size: .68rem; color: var(--muted); margin-bottom: .6rem; }
|
|
296
|
+
.tuning-col .tuning-val { font-family: ${MONO_STACK}; color: var(--ink); float: right; }
|
|
297
|
+
.tuning-col input[type="range"] { display: block; width: 100%; margin-top: .2rem; accent-color: var(--taught); }
|
|
298
|
+
.tuning-col.fly input[type="range"] { accent-color: var(--fly); }
|
|
299
|
+
.tuning-col input:disabled { opacity: .45; }
|
|
185
300
|
.controls-row { display: flex; align-items: center; gap: .6rem; margin-top: 1rem; flex-wrap: wrap; }
|
|
186
301
|
.controls-row button { font-family: ${MONO_STACK}; font-size: .78rem; padding: .3rem .7rem; border: 1px solid var(--line); background: var(--card); color: var(--ink); }
|
|
187
302
|
.controls-row button:hover:not(:disabled) { border-color: var(--taught); }
|
|
188
303
|
.controls-row button:disabled { opacity: .4; cursor: default; }
|
|
189
304
|
.controls-row .turn { margin-left: auto; font-family: ${MONO_STACK}; font-size: .78rem; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
190
305
|
.status { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); margin-top: .5rem; }
|
|
191
|
-
body.preview .side, body.preview .controls-row, body.preview .status { display: none; }
|
|
306
|
+
body.preview .side, body.preview .controls-row, body.preview .status, body.preview .tuning { display: none; }
|
|
192
307
|
body.preview main { padding: 0; max-width: none; }
|
|
193
308
|
body.preview .stage { display: block; }
|
|
194
309
|
body.preview .eyebrow, body.preview h1 { display: none; }
|
|
@@ -208,7 +323,7 @@ ${THEME_TOKENS_CSS}
|
|
|
208
323
|
<aside class="side" aria-label="Agents and chat">
|
|
209
324
|
<div class="hud">
|
|
210
325
|
<h2>agents</h2>
|
|
211
|
-
<div id="hud"></div>
|
|
326
|
+
<div class="hud-list" id="hud"></div>
|
|
212
327
|
</div>
|
|
213
328
|
<div class="chat">
|
|
214
329
|
<h2>tell the spider or the fly something</h2>
|
|
@@ -225,9 +340,33 @@ ${THEME_TOKENS_CSS}
|
|
|
225
340
|
<button type="button" class="pill" data-role="dir" data-direction="east" disabled>the fly is east</button>
|
|
226
341
|
<button type="button" class="pill" data-role="dir" data-direction="west" disabled>the fly is west</button>
|
|
227
342
|
</div>
|
|
343
|
+
<div class="dynpills" id="dynamicPills" role="group" aria-label="address one individual and feed it a true or false position claim"></div>
|
|
228
344
|
</div>
|
|
229
345
|
</aside>
|
|
230
346
|
</div>
|
|
347
|
+
<div class="tuning" id="tuning">
|
|
348
|
+
<h2>live tuning — mass loss, spawn rate, vision, per class</h2>
|
|
349
|
+
<div class="tuning-grid">
|
|
350
|
+
<div class="tuning-col spider">
|
|
351
|
+
<h3>spider</h3>
|
|
352
|
+
<label>mass lost/turn <span class="tuning-val" id="tvSpiderMass"></span>
|
|
353
|
+
<input type="range" id="ctlSpiderMass" min="0.1" max="3" step="0.1" disabled></label>
|
|
354
|
+
<label>hatchlings per egg <span class="tuning-val" id="tvSpiderSpawn"></span>
|
|
355
|
+
<input type="range" id="ctlSpiderSpawn" min="1" max="5" step="1" disabled></label>
|
|
356
|
+
<label>vision radius <span class="tuning-val" id="tvSpiderVision"></span>
|
|
357
|
+
<input type="range" id="ctlSpiderVision" min="1" max="8" step="1" disabled></label>
|
|
358
|
+
</div>
|
|
359
|
+
<div class="tuning-col fly">
|
|
360
|
+
<h3>fly</h3>
|
|
361
|
+
<label>mass lost/turn <span class="tuning-val" id="tvFlyMass"></span>
|
|
362
|
+
<input type="range" id="ctlFlyMass" min="0.1" max="3" step="0.1" disabled></label>
|
|
363
|
+
<label>spawns every N turns <span class="tuning-val" id="tvFlySpawn"></span>
|
|
364
|
+
<input type="range" id="ctlFlySpawn" min="1" max="10" step="1" disabled></label>
|
|
365
|
+
<label>vision radius <span class="tuning-val" id="tvFlyVision"></span>
|
|
366
|
+
<input type="range" id="ctlFlyVision" min="1" max="8" step="1" disabled></label>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
231
370
|
<div class="controls-row">
|
|
232
371
|
<button id="resetBtn" type="button" disabled>reset</button>
|
|
233
372
|
<button id="playBtn" type="button" disabled>▶ play</button>
|
|
@@ -246,6 +385,8 @@ const SPIDERFLY = ${gridData};
|
|
|
246
385
|
const createTicker = ${createTicker.toString()};
|
|
247
386
|
const classOfAgentId = ${classOfAgentId.toString()};
|
|
248
387
|
const threadCellsForSpiderPlan = ${threadCellsForSpiderPlan.toString()};
|
|
388
|
+
const facingDegreesFor = ${facingDegreesFor.toString()};
|
|
389
|
+
const nextCorpses = ${nextCorpses.toString()};
|
|
249
390
|
const esc = ${escapeHtml.toString()};
|
|
250
391
|
const el = (id) => document.getElementById(id);
|
|
251
392
|
const boardFrame = el("boardFrame");
|
|
@@ -260,11 +401,20 @@ const SPIDERFLY = ${gridData};
|
|
|
260
401
|
const chatpillsEl = el("chatpills");
|
|
261
402
|
const addressPillEls = [...chatpillsEl.querySelectorAll('[data-role="addr"]')];
|
|
262
403
|
const directionPillEls = [...chatpillsEl.querySelectorAll('[data-role="dir"]')];
|
|
404
|
+
const dynamicPillsEl = el("dynamicPills");
|
|
263
405
|
const statusEl = el("status");
|
|
264
406
|
const turnLabelEl = el("turnLabel");
|
|
265
407
|
const resetBtn = el("resetBtn");
|
|
266
408
|
const playBtn = el("playBtn");
|
|
267
409
|
const stepBtn = el("stepBtn");
|
|
410
|
+
const TUNING_CONTROLS = [
|
|
411
|
+
{ input: "ctlSpiderMass", out: "tvSpiderMass", key: "spiderMassDecrementPerTurn" },
|
|
412
|
+
{ input: "ctlSpiderSpawn", out: "tvSpiderSpawn", key: "eggHatchCount" },
|
|
413
|
+
{ input: "ctlSpiderVision", out: "tvSpiderVision", key: "spiderVisionRadius" },
|
|
414
|
+
{ input: "ctlFlyMass", out: "tvFlyMass", key: "flyMassDecrementPerTurn" },
|
|
415
|
+
{ input: "ctlFlySpawn", out: "tvFlySpawn", key: "flySpawnIntervalTurns" },
|
|
416
|
+
{ input: "ctlFlyVision", out: "tvFlyVision", key: "flyVisionRadius" },
|
|
417
|
+
].map((c) => ({ ...c, inputEl: el(c.input), outEl: el(c.out) }));
|
|
268
418
|
|
|
269
419
|
const params = new URLSearchParams(location.search);
|
|
270
420
|
const preview = params.get("preview") === "1";
|
|
@@ -296,12 +446,23 @@ const SPIDERFLY = ${gridData};
|
|
|
296
446
|
let lastTurn = 0;
|
|
297
447
|
const goalById = {};
|
|
298
448
|
const spriteEls = {};
|
|
449
|
+
const facingByAgent = {};
|
|
450
|
+
let corpses = {};
|
|
451
|
+
const corpseEls = {};
|
|
452
|
+
let selectedAddresseeId = null;
|
|
299
453
|
let povAgentId = null;
|
|
300
454
|
let threadHits = [];
|
|
455
|
+
// Populated once the live session boots (session.getConfig()) and kept in
|
|
456
|
+
// sync with whatever the tuning sliders below are currently set to — the
|
|
457
|
+
// POV overlay's own per-class vision radius reads this instead of a fixed
|
|
458
|
+
// constant, so dragging a slider changes what a toggled POV shows too.
|
|
459
|
+
let liveConfig = {};
|
|
301
460
|
|
|
302
461
|
function removeStaleSprites(agents) {
|
|
303
462
|
for (const id of Object.keys(spriteEls)) {
|
|
304
|
-
if (!agents[id]) {
|
|
463
|
+
if (!agents[id]) {
|
|
464
|
+
spriteEls[id].remove(); delete spriteEls[id]; delete goalById[id]; delete facingByAgent[id];
|
|
465
|
+
}
|
|
305
466
|
}
|
|
306
467
|
}
|
|
307
468
|
|
|
@@ -329,7 +490,13 @@ const SPIDERFLY = ${gridData};
|
|
|
329
490
|
const sprite = window.tmctSpiderFly
|
|
330
491
|
? tmctSpiderFly.resolveSpriteAsset(cls, (session && session.taxonomyRows) || [], [], SPIDERFLY.spriteTemplates, tmctSpiderFly.SPRITE_REGISTRY)
|
|
331
492
|
: "";
|
|
332
|
-
|
|
493
|
+
// The sprite SVG lives in its own inner wrapper so plan-driven facing
|
|
494
|
+
// (a CSS rotate on THIS wrapper) never fights the outer .sprite node's
|
|
495
|
+
// own translate(-50%,-50%) positioning transform.
|
|
496
|
+
const face = document.createElement("div");
|
|
497
|
+
face.className = "sprite-face";
|
|
498
|
+
face.innerHTML = sprite;
|
|
499
|
+
node.appendChild(face);
|
|
333
500
|
if (!preview) {
|
|
334
501
|
node.tabIndex = 0;
|
|
335
502
|
node.setAttribute("role", "button");
|
|
@@ -354,11 +521,54 @@ const SPIDERFLY = ${gridData};
|
|
|
354
521
|
const pct = cellCenterPct(parsed.x, parsed.y);
|
|
355
522
|
node.style.left = pct.leftPct + "%";
|
|
356
523
|
node.style.top = pct.topPct + "%";
|
|
524
|
+
// Facing is driven by the agent's own CURRENT plan's first step, not
|
|
525
|
+
// its actual next move — the two usually agree, but a fresh plan is
|
|
526
|
+
// computed every tick, so facing can visibly flip as a plan gets
|
|
527
|
+
// clobbered and replaced under partial/unreliable knowledge (the
|
|
528
|
+
// whole point of this page, not a glitch).
|
|
529
|
+
facingByAgent[id] = facingDegreesFor(a.plan, facingByAgent[id]);
|
|
530
|
+
const face = node.querySelector(".sprite-face");
|
|
531
|
+
if (face) face.style.transform = "rotate(" + facingByAgent[id] + "deg)";
|
|
357
532
|
if (a.goal) goalById[id] = a.goal;
|
|
358
533
|
}
|
|
359
534
|
lastAgents = agents;
|
|
360
535
|
}
|
|
361
536
|
|
|
537
|
+
// ---- corpses (§A.2.5): visual-only — the actual starve/eat removal
|
|
538
|
+
// already happened in the engine before this redraw ever sees "agents".
|
|
539
|
+
// A corpse sinks to the bottom row of the SAME board column it died in
|
|
540
|
+
// ("drops to the bottom" — spider-fly-world.mjs's own x/y convention,
|
|
541
|
+
// y = GRID_SIZE is the bottom row) and fades out once CORPSE_LINGER_TURNS
|
|
542
|
+
// passes, drawn in the same sprite layer as every live sprite, just
|
|
543
|
+
// grayscaled and non-interactive (see the .sprite.corpse CSS rule).
|
|
544
|
+
function renderCorpses() {
|
|
545
|
+
for (const id of Object.keys(corpseEls)) {
|
|
546
|
+
if (!corpses[id]) { corpseEls[id].remove(); delete corpseEls[id]; }
|
|
547
|
+
}
|
|
548
|
+
for (const [id, corpse] of Object.entries(corpses)) {
|
|
549
|
+
let node = corpseEls[id];
|
|
550
|
+
if (!node) {
|
|
551
|
+
node = document.createElement("div");
|
|
552
|
+
node.className = "sprite corpse";
|
|
553
|
+
node.dataset.cls = corpse.cls;
|
|
554
|
+
node.setAttribute("aria-hidden", "true");
|
|
555
|
+
const face = document.createElement("div");
|
|
556
|
+
face.className = "sprite-face";
|
|
557
|
+
face.innerHTML = window.tmctSpiderFly
|
|
558
|
+
? tmctSpiderFly.resolveSpriteAsset(corpse.cls, (session && session.taxonomyRows) || [], [], SPIDERFLY.spriteTemplates, tmctSpiderFly.SPRITE_REGISTRY)
|
|
559
|
+
: "";
|
|
560
|
+
node.appendChild(face);
|
|
561
|
+
spriteLayer.appendChild(node);
|
|
562
|
+
corpseEls[id] = node;
|
|
563
|
+
}
|
|
564
|
+
const deathCell = tmctSpiderFly.parseCellId(corpse.cell);
|
|
565
|
+
if (!deathCell) continue;
|
|
566
|
+
const pct = cellCenterPct(deathCell.x, SPIDERFLY.gridSize);
|
|
567
|
+
node.style.left = pct.leftPct + "%";
|
|
568
|
+
node.style.top = pct.topPct + "%";
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
362
572
|
function massBarHtml(cls, mass) {
|
|
363
573
|
const maxMass = cls === "spider" ? SPIDERFLY.maxSpiderMass : cls === "fly" ? SPIDERFLY.maxFlyMass : null;
|
|
364
574
|
if (typeof mass !== "number" || !maxMass) return "";
|
|
@@ -366,14 +576,41 @@ const SPIDERFLY = ${gridData};
|
|
|
366
576
|
return '<div class="mass-track"><div class="mass-fill ' + esc(cls) + '" style="width:' + pct + '%"></div></div>';
|
|
367
577
|
}
|
|
368
578
|
|
|
579
|
+
// The last-created plan as a plain arrow chain ("west → west"), or
|
|
580
|
+
// "holding." when the agent's plan this tick is empty — mirrors exactly
|
|
581
|
+
// what drove this tick's sprite facing (facingDegreesFor reads the same
|
|
582
|
+
// plan[0]), so the HUD line and the sprite's own orientation never
|
|
583
|
+
// disagree about what the agent is "about to do".
|
|
584
|
+
function planLineHtml(plan) {
|
|
585
|
+
const text = plan && plan.length ? plan.map((d) => esc(d)).join(" \\u2192 ") : "holding";
|
|
586
|
+
return '<div class="hud-plan">plan: ' + text + ".</div>";
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// The agent's own current world-knowledge-graph snapshot (§A.2.7) — every
|
|
590
|
+
// OTHER live individual it believes it knows the position of, or
|
|
591
|
+
// "unseen" when it has no belief at all. Deliberately never ground truth:
|
|
592
|
+
// this is what the agent would actually ACT on, which a false pill or a
|
|
593
|
+
// told fact can make visibly wrong compared to where that individual
|
|
594
|
+
// really is — the gap IS the demonstration.
|
|
595
|
+
function beliefLineHtml(belief) {
|
|
596
|
+
const entries = Object.entries(belief || {});
|
|
597
|
+
if (!entries.length) return "";
|
|
598
|
+
const text = entries.map(([id, cell]) => esc(id) + (cell ? " @ " + esc(cell) : " unseen")).join(" \\u00b7 ");
|
|
599
|
+
return '<div class="hud-belief">believes: ' + text + "</div>";
|
|
600
|
+
}
|
|
601
|
+
|
|
369
602
|
function renderHud() {
|
|
370
603
|
const ids = Object.keys(lastAgents).sort();
|
|
371
604
|
if (!ids.length) { hudEl.innerHTML = '<div class="hud-empty">no agents on the board.</div>'; return; }
|
|
372
605
|
hudEl.innerHTML = ids.map((id) => {
|
|
373
606
|
const cls = classOfAgentId(id);
|
|
607
|
+
const a = lastAgents[id];
|
|
374
608
|
return '<div class="hud-row"><span class="hud-id ' + esc(cls) + '">' + esc(id) + '</span>'
|
|
375
609
|
+ '<span class="hud-goal">' + esc(goalById[id] || "watching\\u2026") + "</span>"
|
|
376
|
-
+ massBarHtml(cls,
|
|
610
|
+
+ massBarHtml(cls, a.mass)
|
|
611
|
+
+ planLineHtml(a.plan)
|
|
612
|
+
+ beliefLineHtml(a.belief)
|
|
613
|
+
+ "</div>";
|
|
377
614
|
}).join("");
|
|
378
615
|
}
|
|
379
616
|
|
|
@@ -435,7 +672,14 @@ const SPIDERFLY = ${gridData};
|
|
|
435
672
|
const agent = lastAgents[povAgentId];
|
|
436
673
|
if (!agent) { povAgentId = null; return; }
|
|
437
674
|
const p = tmctSpiderFly.parseCellId(agent.cell);
|
|
438
|
-
|
|
675
|
+
// The live, slider-adjustable per-class radius (falling back to the
|
|
676
|
+
// engine's own shipped default before the session has finished
|
|
677
|
+
// booting) — a POV toggle always reflects whatever vision range this
|
|
678
|
+
// class currently actually has, not a fixed constant.
|
|
679
|
+
const radius = classOfAgentId(povAgentId) === "spider"
|
|
680
|
+
? (liveConfig.spiderVisionRadius ?? tmctSpiderFly.DEFAULT_VISION_RADIUS)
|
|
681
|
+
: (liveConfig.flyVisionRadius ?? tmctSpiderFly.DEFAULT_VISION_RADIUS);
|
|
682
|
+
const visible = new Set(tmctSpiderFly.visibleCells(p.x, p.y, radius));
|
|
439
683
|
povCtx.fillStyle = "rgba(0,0,0,.55)";
|
|
440
684
|
for (let gy = 1; gy <= SPIDERFLY.gridSize; gy += 1) {
|
|
441
685
|
for (let gx = 1; gx <= SPIDERFLY.gridSize; gx += 1) {
|
|
@@ -464,8 +708,13 @@ const SPIDERFLY = ${gridData};
|
|
|
464
708
|
boardFrame.addEventListener("mouseleave", () => { threadTip.style.display = "none"; });
|
|
465
709
|
|
|
466
710
|
function redraw(agents, turn, activeWebs) {
|
|
711
|
+
// nextCorpses compares the OLD lastAgents against the NEW agents, so it
|
|
712
|
+
// must run before applyAgents overwrites lastAgents below.
|
|
713
|
+
corpses = nextCorpses(corpses, lastAgents, agents, turn, SPIDERFLY.corpseLingerTurns);
|
|
467
714
|
applyAgents(agents);
|
|
715
|
+
renderCorpses();
|
|
468
716
|
renderHud();
|
|
717
|
+
renderDynamicPills();
|
|
469
718
|
lastTurn = turn;
|
|
470
719
|
lastActiveWebs = activeWebs || [];
|
|
471
720
|
turnLabelEl.textContent = "turn: " + turn;
|
|
@@ -529,6 +778,67 @@ const SPIDERFLY = ${gridData};
|
|
|
529
778
|
chatqEl.addEventListener("input", refreshPills);
|
|
530
779
|
refreshPills();
|
|
531
780
|
|
|
781
|
+
// ---- deception pills (§A.2.4): a SEPARATE dynamic rail, alongside (never
|
|
782
|
+
// replacing) the static one above. tmctSpiderFly.pillsForSpiderFly is the
|
|
783
|
+
// exact same pure function spider-fly-turn.mjs exports — this page never
|
|
784
|
+
// reimplements the true/false claim logic, only renders its output and
|
|
785
|
+
// fills #chatq on click, same click-to-fill discipline as every other
|
|
786
|
+
// pill on this page (never auto-submits).
|
|
787
|
+
function renderDynamicPills() {
|
|
788
|
+
if (!session || !Object.keys(lastAgents).length) { dynamicPillsEl.innerHTML = ""; return; }
|
|
789
|
+
const result = tmctSpiderFly.pillsForSpiderFly(lastAgents, selectedAddresseeId, {});
|
|
790
|
+
selectedAddresseeId = result.addresseeId;
|
|
791
|
+
const addrHtml = result.addressPills.map((p) =>
|
|
792
|
+
'<button type="button" class="pill" data-role="dyn-addr" data-id="' + esc(p.id) + '"'
|
|
793
|
+
+ (p.id === selectedAddresseeId ? ' data-active="1"' : "") + ">" + esc(p.label) + "</button>"
|
|
794
|
+
).join("");
|
|
795
|
+
const claimHtml = result.claimPills.map((p) =>
|
|
796
|
+
'<button type="button" class="pill" data-role="dyn-claim" data-truth="' + (p.truth ? "true" : "false")
|
|
797
|
+
+ '" data-sentence="' + esc(p.sentence) + '">' + esc(p.text) + "</button>"
|
|
798
|
+
).join("");
|
|
799
|
+
dynamicPillsEl.innerHTML = addrHtml + claimHtml;
|
|
800
|
+
}
|
|
801
|
+
dynamicPillsEl.addEventListener("click", (e) => {
|
|
802
|
+
const btn = e.target.closest(".pill");
|
|
803
|
+
if (!btn) return;
|
|
804
|
+
if (btn.dataset.role === "dyn-addr") {
|
|
805
|
+
selectedAddresseeId = btn.dataset.id;
|
|
806
|
+
renderDynamicPills();
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (btn.dataset.role === "dyn-claim") {
|
|
810
|
+
chatqEl.value = btn.dataset.sentence;
|
|
811
|
+
refreshPills();
|
|
812
|
+
chatqEl.focus();
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// ---- live tuning (mass-loss-rate / spawn-rate / vision-radius, per
|
|
817
|
+
// class): each slider writes straight through session.setConfig, which
|
|
818
|
+
// every future tick()/turn() call reads — a change never rewinds a value
|
|
819
|
+
// already written to a past turn's facts, same posture as editing
|
|
820
|
+
// tmct.toml between sessions, just live and per-class.
|
|
821
|
+
function applyTuningValue(control, value) {
|
|
822
|
+
liveConfig[control.key] = value;
|
|
823
|
+
control.outEl.textContent = String(value);
|
|
824
|
+
control.inputEl.value = String(value);
|
|
825
|
+
if (session) session.setConfig({ [control.key]: value });
|
|
826
|
+
}
|
|
827
|
+
function initTuning(config) {
|
|
828
|
+
liveConfig = { ...config };
|
|
829
|
+
for (const control of TUNING_CONTROLS) {
|
|
830
|
+
applyTuningValue(control, liveConfig[control.key]);
|
|
831
|
+
control.inputEl.disabled = false;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
for (const control of TUNING_CONTROLS) {
|
|
835
|
+
control.inputEl.addEventListener("input", () => {
|
|
836
|
+
const value = Number(control.inputEl.value);
|
|
837
|
+
applyTuningValue(control, value);
|
|
838
|
+
drawPov(); // a vision-radius change should reflect immediately in an open POV overlay
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
532
842
|
// ---- serialize every engine-touching call: the ticker and the chat dock
|
|
533
843
|
// share one in-memory store, and an overlapping tick()/turn() pair could
|
|
534
844
|
// race against the same @turnN write.
|
|
@@ -539,8 +849,17 @@ const SPIDERFLY = ${gridData};
|
|
|
539
849
|
return run;
|
|
540
850
|
}
|
|
541
851
|
|
|
852
|
+
let tuningInitialized = false;
|
|
542
853
|
async function boot() {
|
|
543
854
|
session = await tmctSpiderFly.createSpiderFlySession();
|
|
855
|
+
// A reset mints a brand-new session (fresh board, fresh config) — the
|
|
856
|
+
// FIRST boot seeds the sliders from the engine's own shipped defaults;
|
|
857
|
+
// every boot after that re-applies whatever the visitor already had the
|
|
858
|
+
// sliders set to, so tuning survives a reset instead of silently
|
|
859
|
+
// reverting.
|
|
860
|
+
if (!tuningInitialized) { initTuning(session.getConfig()); tuningInitialized = true; }
|
|
861
|
+
else session.setConfig(liveConfig);
|
|
862
|
+
selectedAddresseeId = null;
|
|
544
863
|
redraw(session.initial.agents, session.initial.turn, session.initial.activeWebs);
|
|
545
864
|
statusEl.textContent = session.opening;
|
|
546
865
|
chatqEl.disabled = false;
|