@polycode-projects/the-mechanical-code-talker 2.7.12 → 2.7.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/worlds/manifest.json +5 -5
- package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
- package/corpus/worlds/src/ashcombe-hall.jsonl +1 -0
- package/package.json +1 -1
- package/src/domain/spider-fly-world.mjs +13 -0
- package/src/domain/sprite-map.mjs +49 -1
- package/src/services/adventure-autoplay.mjs +192 -0
- package/src/services/adventure-viz.mjs +262 -0
- package/src/services/spider-fly-viz.mjs +39 -9
- package/src/services/spider-fly.mjs +236 -68
- package/src/surfaces/web/adventure-browser-entry.mjs +88 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +1 -0
- package/src/surfaces/web/spider-fly-browser-entry.mjs +10 -8
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
34
34
|
import { createTicker } from "./viz-ticker.mjs";
|
|
35
35
|
import { GRID_SIZE, WEB_HOME, WEB_RADIUS, isInWebBlock, cellId } from "../domain/spider-fly-world.mjs";
|
|
36
|
+
import { FLY_INITIAL_MASS, SPIDER_INITIAL_MASS } from "./spider-fly.mjs";
|
|
36
37
|
|
|
37
38
|
const CELL_PX = 44;
|
|
38
39
|
const BOARD_PX = CELL_PX * GRID_SIZE;
|
|
@@ -111,6 +112,8 @@ export function renderSpiderFlyHtml({ title = DEFAULT_TITLE } = {}) {
|
|
|
111
112
|
cellPx: CELL_PX,
|
|
112
113
|
previewMaxTurns: PREVIEW_MAX_TURNS,
|
|
113
114
|
tickWaitMs: TICK_WAIT_MS,
|
|
115
|
+
maxFlyMass: FLY_INITIAL_MASS,
|
|
116
|
+
maxSpiderMass: SPIDER_INITIAL_MASS,
|
|
114
117
|
});
|
|
115
118
|
|
|
116
119
|
return `<!doctype html>
|
|
@@ -154,6 +157,9 @@ ${THEME_TOKENS_CSS}
|
|
|
154
157
|
.hud-id { font-family: ${MONO_STACK}; font-size: .74rem; }
|
|
155
158
|
.hud-id.spider { color: var(--taught); } .hud-id.fly { color: var(--fly); } .hud-id.egg { color: var(--muted); }
|
|
156
159
|
.hud-goal { font-size: .85rem; }
|
|
160
|
+
.mass-track { height: 4px; margin-top: .3rem; background: var(--line); border-radius: 2px; overflow: hidden; }
|
|
161
|
+
.mass-fill { height: 100%; background: var(--taught); }
|
|
162
|
+
.mass-fill.fly { background: var(--fly); }
|
|
157
163
|
.hud-empty { color: var(--muted); font-size: .85rem; }
|
|
158
164
|
.chatlog { display: flex; flex-direction: column; gap: .4rem; max-height: 220px; overflow-y: auto; margin-bottom: .5rem; }
|
|
159
165
|
.chatlog:empty { display: none; margin-bottom: 0; }
|
|
@@ -264,6 +270,7 @@ const SPIDERFLY = ${gridData};
|
|
|
264
270
|
// ---- state shared across redraws --------------------------------------
|
|
265
271
|
let session = null;
|
|
266
272
|
let lastAgents = {};
|
|
273
|
+
let lastActiveWebs = [];
|
|
267
274
|
let lastTurn = 0;
|
|
268
275
|
const goalById = {};
|
|
269
276
|
const spriteEls = {};
|
|
@@ -324,13 +331,21 @@ const SPIDERFLY = ${gridData};
|
|
|
324
331
|
lastAgents = agents;
|
|
325
332
|
}
|
|
326
333
|
|
|
334
|
+
function massBarHtml(cls, mass) {
|
|
335
|
+
const maxMass = cls === "spider" ? SPIDERFLY.maxSpiderMass : cls === "fly" ? SPIDERFLY.maxFlyMass : null;
|
|
336
|
+
if (typeof mass !== "number" || !maxMass) return "";
|
|
337
|
+
const pct = Math.max(0, Math.min(100, (mass / maxMass) * 100));
|
|
338
|
+
return '<div class="mass-track"><div class="mass-fill ' + esc(cls) + '" style="width:' + pct + '%"></div></div>';
|
|
339
|
+
}
|
|
340
|
+
|
|
327
341
|
function renderHud() {
|
|
328
342
|
const ids = Object.keys(lastAgents).sort();
|
|
329
343
|
if (!ids.length) { hudEl.innerHTML = '<div class="hud-empty">no agents on the board.</div>'; return; }
|
|
330
344
|
hudEl.innerHTML = ids.map((id) => {
|
|
331
345
|
const cls = classOfAgentId(id);
|
|
332
346
|
return '<div class="hud-row"><span class="hud-id ' + esc(cls) + '">' + esc(id) + '</span>'
|
|
333
|
-
+ '<span class="hud-goal">' + esc(goalById[id] || "watching\\u2026") + "</span
|
|
347
|
+
+ '<span class="hud-goal">' + esc(goalById[id] || "watching\\u2026") + "</span>"
|
|
348
|
+
+ massBarHtml(cls, lastAgents[id].mass) + "</div>";
|
|
334
349
|
}).join("");
|
|
335
350
|
}
|
|
336
351
|
|
|
@@ -340,7 +355,7 @@ const SPIDERFLY = ${gridData};
|
|
|
340
355
|
directionDelta: tmctSpiderFly.DIRECTION_DELTA,
|
|
341
356
|
};
|
|
342
357
|
|
|
343
|
-
function drawBoard(agents) {
|
|
358
|
+
function drawBoard(agents, activeWebs) {
|
|
344
359
|
const w = SPIDERFLY.boardPx, h = SPIDERFLY.boardPx;
|
|
345
360
|
boardCtx.clearRect(0, 0, w, h);
|
|
346
361
|
boardCtx.fillStyle = cssVar("--taught-soft") || "rgba(46,125,79,.12)";
|
|
@@ -348,6 +363,20 @@ const SPIDERFLY = ${gridData};
|
|
|
348
363
|
const p = tmctSpiderFly.parseCellId(wc);
|
|
349
364
|
boardCtx.fillRect((p.x - 1) * cellSize, (p.y - 1) * cellSize, cellSize, cellSize);
|
|
350
365
|
}
|
|
366
|
+
// A spider-built dynamic web is a distinct color from the always-on
|
|
367
|
+
// static home zone above, plus a dashed outline — same concept
|
|
368
|
+
// (hasActiveWebAt), visually two different things on the board.
|
|
369
|
+
boardCtx.fillStyle = cssVar("--alert-soft") || "rgba(176,80,63,.12)";
|
|
370
|
+
boardCtx.strokeStyle = cssVar("--alert") || "#B0503F";
|
|
371
|
+
boardCtx.lineWidth = 1;
|
|
372
|
+
boardCtx.setLineDash([3, 2]);
|
|
373
|
+
for (const web of activeWebs || []) {
|
|
374
|
+
const p = tmctSpiderFly.parseCellId(web.cell);
|
|
375
|
+
if (!p) continue;
|
|
376
|
+
boardCtx.fillRect((p.x - 1) * cellSize, (p.y - 1) * cellSize, cellSize, cellSize);
|
|
377
|
+
boardCtx.strokeRect((p.x - 1) * cellSize + 0.5, (p.y - 1) * cellSize + 0.5, cellSize - 1, cellSize - 1);
|
|
378
|
+
}
|
|
379
|
+
boardCtx.setLineDash([]);
|
|
351
380
|
boardCtx.strokeStyle = cssVar("--line") || "#DDD9D0";
|
|
352
381
|
boardCtx.lineWidth = 1;
|
|
353
382
|
for (let i = 0; i <= SPIDERFLY.gridSize; i += 1) {
|
|
@@ -406,12 +435,13 @@ const SPIDERFLY = ${gridData};
|
|
|
406
435
|
});
|
|
407
436
|
boardFrame.addEventListener("mouseleave", () => { threadTip.style.display = "none"; });
|
|
408
437
|
|
|
409
|
-
function redraw(agents, turn) {
|
|
438
|
+
function redraw(agents, turn, activeWebs) {
|
|
410
439
|
applyAgents(agents);
|
|
411
440
|
renderHud();
|
|
412
441
|
lastTurn = turn;
|
|
442
|
+
lastActiveWebs = activeWebs || [];
|
|
413
443
|
turnLabelEl.textContent = "turn: " + turn;
|
|
414
|
-
drawBoard(agents);
|
|
444
|
+
drawBoard(agents, lastActiveWebs);
|
|
415
445
|
drawPov();
|
|
416
446
|
}
|
|
417
447
|
|
|
@@ -431,7 +461,7 @@ const SPIDERFLY = ${gridData};
|
|
|
431
461
|
const result = await session.turn(q);
|
|
432
462
|
addChatLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
|
|
433
463
|
const snap = await session.snapshot();
|
|
434
|
-
redraw(snap.agents, snap.turn);
|
|
464
|
+
redraw(snap.agents, snap.turn, snap.activeWebs);
|
|
435
465
|
});
|
|
436
466
|
});
|
|
437
467
|
|
|
@@ -447,7 +477,7 @@ const SPIDERFLY = ${gridData};
|
|
|
447
477
|
|
|
448
478
|
async function boot() {
|
|
449
479
|
session = await tmctSpiderFly.createSpiderFlySession();
|
|
450
|
-
redraw(session.initial.agents, session.initial.turn);
|
|
480
|
+
redraw(session.initial.agents, session.initial.turn, session.initial.activeWebs);
|
|
451
481
|
statusEl.textContent = session.opening;
|
|
452
482
|
chatqEl.disabled = false;
|
|
453
483
|
resetBtn.disabled = false; playBtn.disabled = false; stepBtn.disabled = false;
|
|
@@ -457,7 +487,7 @@ const SPIDERFLY = ${gridData};
|
|
|
457
487
|
const ticker = createTicker({
|
|
458
488
|
onTick: () => withLock(async () => {
|
|
459
489
|
const result = await session.tick();
|
|
460
|
-
redraw(result.agents, result.turn);
|
|
490
|
+
redraw(result.agents, result.turn, result.activeWebs);
|
|
461
491
|
}),
|
|
462
492
|
onRender: (state) => {
|
|
463
493
|
playBtn.textContent = state.playing ? "\\u23f8 pause" : "\\u25b6 play";
|
|
@@ -479,10 +509,10 @@ const SPIDERFLY = ${gridData};
|
|
|
479
509
|
|
|
480
510
|
boot().then(() => { if (preview) ticker.play(); });
|
|
481
511
|
|
|
482
|
-
new MutationObserver(() => { drawBoard(lastAgents); drawPov(); })
|
|
512
|
+
new MutationObserver(() => { drawBoard(lastAgents, lastActiveWebs); drawPov(); })
|
|
483
513
|
.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
|
|
484
514
|
if (window.matchMedia) {
|
|
485
|
-
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { drawBoard(lastAgents); drawPov(); });
|
|
515
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { drawBoard(lastAgents, lastActiveWebs); drawPov(); });
|
|
486
516
|
}
|
|
487
517
|
})();
|
|
488
518
|
</script>
|
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
// pack and this engine read from.
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
|
-
WORLD_NAME, WEB_HOME,
|
|
13
|
+
WORLD_NAME, WEB_HOME, WEB_DURATION_TURNS, SPIDER_INITIAL_MASS, SPIDER_MASS_DECREMENT_PER_TURN,
|
|
14
14
|
cellId, parseCellId, chebyshevDistance, visibleCells, isInWebBlock, perimeterCells,
|
|
15
15
|
DIRECTION_DELTA,
|
|
16
16
|
} from "../domain/spider-fly-world.mjs";
|
|
17
17
|
import { findActionPath, findReachableSet } from "../domain/planning.mjs";
|
|
18
18
|
import { appendFacts, loadMemory, readFactRows } from "../adapters/memory/core.mjs";
|
|
19
19
|
import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
|
|
20
|
+
import { mulberry32 } from "../domain/seeded-random.mjs";
|
|
21
|
+
import { fnv1a32 } from "../domain/hash.mjs";
|
|
20
22
|
|
|
21
23
|
// ---- tunable constants (starting values, not fixed — the vision radius and
|
|
22
24
|
// mass economy all want checking against a real playable board) -------------
|
|
@@ -27,6 +29,22 @@ export const FLY_MASS_DECREMENT_PER_TURN = 1;
|
|
|
27
29
|
export const EGG_HATCH_DELAY_TURNS = 3;
|
|
28
30
|
export const FLY_SPAWN_INTERVAL_TURNS = 3;
|
|
29
31
|
export const EGGS_EATEN_THRESHOLD = 2;
|
|
32
|
+
export { SPIDER_INITIAL_MASS, SPIDER_MASS_DECREMENT_PER_TURN, WEB_DURATION_TURNS };
|
|
33
|
+
|
|
34
|
+
// ---- seeded "randomness" (never Math.random) ---------------------------------
|
|
35
|
+
// Every "random" decision (fly wander, fly/spawn placement) is a mulberry32
|
|
36
|
+
// draw seeded by an fnv1a32 hash of a context string built from data already
|
|
37
|
+
// in the facts (world name, turn number, the subject's own id, a purpose
|
|
38
|
+
// tag) — the same hash-seeds-a-PRNG idiom answer-variants.mjs's phrase
|
|
39
|
+
// selection already uses. Two runs from the same starting facts produce the
|
|
40
|
+
// byte-identical sequence of "random" choices, never wall-clock driven.
|
|
41
|
+
|
|
42
|
+
/** Deterministically pick one of `options` (must be non-empty), keyed on
|
|
43
|
+
* `contextString`. */
|
|
44
|
+
function seededPick(options, contextString) {
|
|
45
|
+
const rng = mulberry32(fnv1a32(contextString));
|
|
46
|
+
return options[Math.floor(rng() * options.length)];
|
|
47
|
+
}
|
|
30
48
|
|
|
31
49
|
// ---- the state fold ----------------------------------------------------------
|
|
32
50
|
|
|
@@ -37,19 +55,24 @@ function splitSnapshot(subject) {
|
|
|
37
55
|
return m ? { base: m[1], turn: Number(m[2]) } : { base: subject, turn: 0 };
|
|
38
56
|
}
|
|
39
57
|
|
|
58
|
+
const WEB_ID_RE = /^web-\d+$/;
|
|
59
|
+
|
|
40
60
|
/** Fold fact rows into the current spider-fly world state: per-subject
|
|
41
61
|
* newest placement (mgx:currently-in), newest fly mass, spider's newest
|
|
42
|
-
* flies-eaten count, each egg's laid-at-turn,
|
|
43
|
-
* starved/hatched-into markers that
|
|
44
|
-
* counter is derived, never stored —
|
|
45
|
-
* exactly foldWorldState's own convention.
|
|
62
|
+
* flies-eaten count, each egg's laid-at-turn, each dynamic web's cell +
|
|
63
|
+
* built-at turn, and the terminal eaten-by/starved/hatched-into markers that
|
|
64
|
+
* make a subject no longer live. The turn counter is derived, never stored —
|
|
65
|
+
* the largest @turnN suffix seen, exactly foldWorldState's own convention.
|
|
66
|
+
* Pure. */
|
|
46
67
|
export function foldSpiderFlyState(factRows) {
|
|
47
68
|
const placements = new Map(); // subject -> { cell, turn }
|
|
48
|
-
const mass = new Map(); // fly subject -> { value, turn }
|
|
69
|
+
const mass = new Map(); // fly/spider subject -> { value, turn }
|
|
49
70
|
const fliesEaten = new Map(); // spider subject -> { value, turn }
|
|
50
71
|
const laidAtTurn = new Map(); // egg subject -> { value, turn }
|
|
72
|
+
const webCell = new Map(); // web subject -> { cell, turn }
|
|
73
|
+
const webBuiltAt = new Map(); // web subject -> { value, turn }
|
|
51
74
|
const eatenBy = new Map(); // fly subject -> { spider, turn }
|
|
52
|
-
const starved = new Set(); // fly subject
|
|
75
|
+
const starved = new Set(); // fly/spider subject
|
|
53
76
|
const hatchedInto = new Map(); // egg subject -> { spider, turn }
|
|
54
77
|
let turnCount = 0;
|
|
55
78
|
|
|
@@ -60,6 +83,10 @@ export function foldSpiderFlyState(factRows) {
|
|
|
60
83
|
if (row.predicate === "mgx:currently-in") {
|
|
61
84
|
const prior = placements.get(base);
|
|
62
85
|
if (!prior || turn >= prior.turn) placements.set(base, { cell: row.object, turn });
|
|
86
|
+
if (WEB_ID_RE.test(base)) {
|
|
87
|
+
const priorWeb = webCell.get(base);
|
|
88
|
+
if (!priorWeb || turn >= priorWeb.turn) webCell.set(base, { cell: row.object, turn });
|
|
89
|
+
}
|
|
63
90
|
continue;
|
|
64
91
|
}
|
|
65
92
|
if (row.predicate === "mgx:mass") {
|
|
@@ -77,6 +104,11 @@ export function foldSpiderFlyState(factRows) {
|
|
|
77
104
|
if (!prior || turn >= prior.turn) laidAtTurn.set(base, { value: Number(row.object), turn });
|
|
78
105
|
continue;
|
|
79
106
|
}
|
|
107
|
+
if (row.predicate === "mgx:web-built-at-turn") {
|
|
108
|
+
const prior = webBuiltAt.get(base);
|
|
109
|
+
if (!prior || turn >= prior.turn) webBuiltAt.set(base, { value: Number(row.object), turn });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
80
112
|
if (row.predicate === "mgx:eaten-by") {
|
|
81
113
|
const prior = eatenBy.get(base);
|
|
82
114
|
if (!prior || turn >= prior.turn) eatenBy.set(base, { spider: row.object, turn });
|
|
@@ -90,8 +122,14 @@ export function foldSpiderFlyState(factRows) {
|
|
|
90
122
|
}
|
|
91
123
|
}
|
|
92
124
|
|
|
125
|
+
const webs = new Map(); // web subject -> { cell, builtAtTurn }
|
|
126
|
+
for (const [id, { cell }] of webCell) {
|
|
127
|
+
const builtAtTurn = webBuiltAt.get(id)?.value;
|
|
128
|
+
if (builtAtTurn !== undefined) webs.set(id, { cell, builtAtTurn });
|
|
129
|
+
}
|
|
130
|
+
|
|
93
131
|
const removed = new Set([...eatenBy.keys(), ...starved, ...hatchedInto.keys()]);
|
|
94
|
-
return { placements, mass, fliesEaten, laidAtTurn, eatenBy, starved, hatchedInto, removed, turnCount };
|
|
132
|
+
return { placements, mass, fliesEaten, laidAtTurn, webs, eatenBy, starved, hatchedInto, removed, turnCount };
|
|
95
133
|
}
|
|
96
134
|
|
|
97
135
|
const sortedLiveSubjects = (state, re) =>
|
|
@@ -148,17 +186,35 @@ export function gridApplyActions(factRows) {
|
|
|
148
186
|
* elsewhere on the folded state, never on the path-search state itself). */
|
|
149
187
|
export const spiderPathStateKey = (state) => cellId(state.x, state.y);
|
|
150
188
|
|
|
189
|
+
/** Whether (x, y) is currently webbed — the static home zone (always active)
|
|
190
|
+
* OR a live spider-built web (mgx:web-built-at-turn + WEB_DURATION_TURNS >
|
|
191
|
+
* turn). The one predicate every eat precondition and the fly's movement
|
|
192
|
+
* gate consult, so the static zone and dynamic webs are ONE concept. `state`
|
|
193
|
+
* may be omitted (or carry no `webs` map) — the static-zone check alone
|
|
194
|
+
* still answers correctly, just blind to dynamic webs; every real caller
|
|
195
|
+
* threads the folded state through. */
|
|
196
|
+
export function hasActiveWebAt(x, y, state, turn) {
|
|
197
|
+
if (isInWebBlock(x, y)) return true;
|
|
198
|
+
if (!state?.webs?.size) return false;
|
|
199
|
+
const target = cellId(x, y);
|
|
200
|
+
for (const { cell, builtAtTurn } of state.webs.values()) {
|
|
201
|
+
if (cell === target && builtAtTurn + WEB_DURATION_TURNS > turn) return true;
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
151
206
|
/** The spider's multi-step path: findActionPath wired with isGoal =
|
|
152
|
-
* "co-located with the believed fly cell, and that cell
|
|
153
|
-
*
|
|
154
|
-
* never fire (it doesn't depend on the search state,
|
|
155
|
-
* target), so this returns null — an honest "no path to
|
|
156
|
-
* than a path toward a cell that would never satisfy the eat
|
|
157
|
-
* Null also covers "no believed target at all."
|
|
158
|
-
|
|
207
|
+
* "co-located with the believed fly cell, and that cell has an active web
|
|
208
|
+
* (static or dynamic)." When the fly's believed cell sits outside every
|
|
209
|
+
* active web, isGoal can never fire (it doesn't depend on the search state,
|
|
210
|
+
* only on the fixed target), so this returns null — an honest "no path to
|
|
211
|
+
* an eat" rather than a path toward a cell that would never satisfy the eat
|
|
212
|
+
* condition. Null also covers "no believed target at all." `state`/`turn`
|
|
213
|
+
* are optional, defaulting to "static web zone only" (see hasActiveWebAt). */
|
|
214
|
+
export function planSpiderPath(spiderCell, believedFlyCell, applyActions, state, turn) {
|
|
159
215
|
if (!believedFlyCell) return null;
|
|
160
|
-
const isGoal = (
|
|
161
|
-
|
|
216
|
+
const isGoal = (s) =>
|
|
217
|
+
s.x === believedFlyCell.x && s.y === believedFlyCell.y && hasActiveWebAt(s.x, s.y, state, turn);
|
|
162
218
|
return findActionPath(spiderCell, isGoal, applyActions, { stateKey: spiderPathStateKey });
|
|
163
219
|
}
|
|
164
220
|
|
|
@@ -178,12 +234,24 @@ function bestOneStepBy(fromCell, applyActions, scoreOf, isBetter) {
|
|
|
178
234
|
return best;
|
|
179
235
|
}
|
|
180
236
|
|
|
237
|
+
/** A fly with no believed spider position wanders instead of holding still:
|
|
238
|
+
* a seeded, uniform pick among staying put or any one-ply reachable cell,
|
|
239
|
+
* keyed on this turn + the fly's own id (purpose "wander") — deterministic
|
|
240
|
+
* and replayable, never Math.random. Looks random to a human watching. The
|
|
241
|
+
* caller is responsible for skipping this entirely when the fly sits in an
|
|
242
|
+
* active web this tick (a webbed fly can't move at all, wander or not). */
|
|
243
|
+
export function randomFlyWander(flyCell, applyActions, turn, flyId) {
|
|
244
|
+
const options = [flyCell, ...findReachableSet(flyCell, applyActions, { maxDepth: 1 }).map((r) => r.node)];
|
|
245
|
+
return seededPick(options, `${WORLD_NAME}:${turn}:${flyId}:wander`);
|
|
246
|
+
}
|
|
247
|
+
|
|
181
248
|
/** The fly's one move this turn: score every one-ply reachable cell (plus
|
|
182
249
|
* staying put) by Chebyshev distance from the fly's believed spider
|
|
183
250
|
* position, move to the highest-scoring cell. A fly with no believed
|
|
184
|
-
* spider position
|
|
185
|
-
|
|
186
|
-
|
|
251
|
+
* spider position wanders instead (randomFlyWander) — `turn`/`flyId` key
|
|
252
|
+
* that seeded draw. */
|
|
253
|
+
export function greedyFlyMove(flyCell, believedSpiderCell, applyActions, turn, flyId) {
|
|
254
|
+
if (!believedSpiderCell) return randomFlyWander(flyCell, applyActions, turn, flyId);
|
|
187
255
|
return bestOneStepBy(
|
|
188
256
|
flyCell, applyActions,
|
|
189
257
|
(cell) => chebyshevDistance(cell.x, cell.y, believedSpiderCell.x, believedSpiderCell.y),
|
|
@@ -204,6 +272,20 @@ export function greedySpiderApproach(spiderCell, believedFlyCell, applyActions)
|
|
|
204
272
|
);
|
|
205
273
|
}
|
|
206
274
|
|
|
275
|
+
/** A spider's move when another live spider is believed visible: the mirror
|
|
276
|
+
* image of greedyFlyMove's evasion — score every one-ply reachable cell
|
|
277
|
+
* (plus staying put) by Chebyshev distance from the other spider's believed
|
|
278
|
+
* position, move to the highest-scoring (furthest) cell. Priority branch 1
|
|
279
|
+
* of §5's avoid-spiders > chase-flies > hold-and-web ordering. */
|
|
280
|
+
export function greedySpiderAvoid(spiderCell, believedOtherSpiderCell, applyActions) {
|
|
281
|
+
if (!believedOtherSpiderCell) return spiderCell;
|
|
282
|
+
return bestOneStepBy(
|
|
283
|
+
spiderCell, applyActions,
|
|
284
|
+
(cell) => chebyshevDistance(cell.x, cell.y, believedOtherSpiderCell.x, believedOtherSpiderCell.y),
|
|
285
|
+
(score, bestScore) => score > bestScore,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
207
289
|
// ---- visibility and belief (§4): static grid topology is common knowledge
|
|
208
290
|
// to both agents; only dynamic entity positions are gated by vision. A told
|
|
209
291
|
// fact (a later chat-integration piece of work, not built here) is the one
|
|
@@ -274,14 +356,16 @@ function mostRecentEaterSpider(state, eatenDeltaBySpider) {
|
|
|
274
356
|
/**
|
|
275
357
|
* One ecology pass over the tick's post-movement state: `postMovePlacements`
|
|
276
358
|
* is a Map(subject -> {x,y}) for every currently-live spider and fly after
|
|
277
|
-
* this turn's movement writes; `postMoveMassByFly`
|
|
278
|
-
* number), the
|
|
279
|
-
* is
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
359
|
+
* this turn's movement writes; `postMoveMassByFly`/`postMoveMassBySpider` are
|
|
360
|
+
* Map(subject -> number), the mass after this turn's decrement, pre-removal
|
|
361
|
+
* (`postMoveMassBySpider` is optional — a spider absent from it is simply
|
|
362
|
+
* never starve-checked, so callers that don't track spider mass, e.g. older
|
|
363
|
+
* tests, see no behavior change). `state` is the PRE-move fold (for history:
|
|
364
|
+
* prior flies-eaten counts, prior eggs, prior eaten turns, live webs).
|
|
365
|
+
* Returns `{ writes, events }` — writes to append alongside the turn's
|
|
366
|
+
* movement facts, events for the tick's own return payload. Pure.
|
|
283
367
|
*/
|
|
284
|
-
export function runEcologyPass({ state, postMovePlacements, postMoveMassByFly, turn }) {
|
|
368
|
+
export function runEcologyPass({ state, postMovePlacements, postMoveMassByFly, postMoveMassBySpider = new Map(), turn }) {
|
|
285
369
|
const k = turn;
|
|
286
370
|
const writes = [];
|
|
287
371
|
const events = { eaten: [], starved: [], laid: null, hatched: [], spawned: null };
|
|
@@ -289,18 +373,22 @@ export function runEcologyPass({ state, postMovePlacements, postMoveMassByFly, t
|
|
|
289
373
|
const spiders = [...postMovePlacements.keys()].filter((id) => /^spider-\d+$/.test(id)).sort();
|
|
290
374
|
const flies = [...postMovePlacements.keys()].filter((id) => /^fly-\d+$/.test(id)).sort();
|
|
291
375
|
|
|
292
|
-
// 1. Eat — a spider and a fly sharing an
|
|
376
|
+
// 1. Eat — a spider and a fly sharing an actively-webbed cell (static home
|
|
377
|
+
// zone or a live dynamic web). The eating spider gains exactly the fly's
|
|
378
|
+
// post-decrement remaining mass, not a flat bonus.
|
|
293
379
|
const claimedFlies = new Set();
|
|
294
380
|
const eatenDeltaBySpider = new Map();
|
|
381
|
+
const eatenMassBySpider = new Map();
|
|
295
382
|
for (const spiderId of spiders) {
|
|
296
383
|
const sCell = postMovePlacements.get(spiderId);
|
|
297
|
-
if (!
|
|
384
|
+
if (!hasActiveWebAt(sCell.x, sCell.y, state, k)) continue;
|
|
298
385
|
for (const flyId of flies) {
|
|
299
386
|
if (claimedFlies.has(flyId)) continue;
|
|
300
387
|
const fCell = postMovePlacements.get(flyId);
|
|
301
388
|
if (sCell.x !== fCell.x || sCell.y !== fCell.y) continue;
|
|
302
389
|
claimedFlies.add(flyId);
|
|
303
390
|
eatenDeltaBySpider.set(spiderId, (eatenDeltaBySpider.get(spiderId) ?? 0) + 1);
|
|
391
|
+
eatenMassBySpider.set(spiderId, (eatenMassBySpider.get(spiderId) ?? 0) + (postMoveMassByFly.get(flyId) ?? 0));
|
|
304
392
|
writes.push({ subject: `${flyId}@turn${k}`, predicate: "mgx:eaten-by", object: spiderId });
|
|
305
393
|
events.eaten.push({ fly: flyId, spider: spiderId, cell: cellId(sCell.x, sCell.y) });
|
|
306
394
|
}
|
|
@@ -308,9 +396,14 @@ export function runEcologyPass({ state, postMovePlacements, postMoveMassByFly, t
|
|
|
308
396
|
for (const [spiderId, delta] of eatenDeltaBySpider) {
|
|
309
397
|
const newCount = (state.fliesEaten.get(spiderId)?.value ?? 0) + delta;
|
|
310
398
|
writes.push({ subject: `${spiderId}@turn${k}`, predicate: "mgx:flies-eaten", object: String(newCount) });
|
|
399
|
+
const priorSpiderMass = postMoveMassBySpider.get(spiderId) ?? (state.mass.get(spiderId)?.value ?? SPIDER_INITIAL_MASS);
|
|
400
|
+
const newSpiderMass = priorSpiderMass + (eatenMassBySpider.get(spiderId) ?? 0);
|
|
401
|
+
writes.push({ subject: `${spiderId}@turn${k}`, predicate: "mgx:mass", object: String(newSpiderMass) });
|
|
311
402
|
}
|
|
312
403
|
|
|
313
|
-
// 2. Starve — mass reached zero, and not already claimed by this turn's
|
|
404
|
+
// 2. Starve — mass reached zero, and not already claimed by this turn's
|
|
405
|
+
// eat. Spiders waste away the same as flies; a spider that just ate
|
|
406
|
+
// survives regardless (eat resolves first).
|
|
314
407
|
for (const flyId of flies) {
|
|
315
408
|
if (claimedFlies.has(flyId)) continue;
|
|
316
409
|
if ((postMoveMassByFly.get(flyId) ?? 0) <= 0) {
|
|
@@ -319,6 +412,14 @@ export function runEcologyPass({ state, postMovePlacements, postMoveMassByFly, t
|
|
|
319
412
|
}
|
|
320
413
|
}
|
|
321
414
|
const deadFliesThisTick = new Set([...claimedFlies, ...events.starved]);
|
|
415
|
+
for (const spiderId of spiders) {
|
|
416
|
+
if (eatenDeltaBySpider.has(spiderId)) continue;
|
|
417
|
+
if (!postMoveMassBySpider.has(spiderId)) continue;
|
|
418
|
+
if (postMoveMassBySpider.get(spiderId) <= 0) {
|
|
419
|
+
writes.push({ subject: `${spiderId}@turn${k}`, predicate: "mgx:starved", object: "true" });
|
|
420
|
+
events.starved.push(spiderId);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
322
423
|
|
|
323
424
|
// 3. Lay — the eat condition has fired enough times since the last egg (or
|
|
324
425
|
// once, at game start), with no live egg outstanding right now.
|
|
@@ -351,11 +452,14 @@ export function runEcologyPass({ state, postMovePlacements, postMoveMassByFly, t
|
|
|
351
452
|
const newSpiderId = `spider-${nextSpiderNum}`;
|
|
352
453
|
nextSpiderNum += 1;
|
|
353
454
|
writes.push({ subject: `${newSpiderId}@turn${k}`, predicate: "mgx:currently-in", object: eggCell });
|
|
455
|
+
writes.push({ subject: `${newSpiderId}@turn${k}`, predicate: "mgx:mass", object: String(SPIDER_INITIAL_MASS) });
|
|
354
456
|
writes.push({ subject: `${eggId}@turn${k}`, predicate: "mgx:hatched-into", object: newSpiderId });
|
|
355
457
|
events.hatched.push({ egg: eggId, spider: newSpiderId, cell: eggCell });
|
|
356
458
|
}
|
|
357
459
|
|
|
358
|
-
// 5. Spawn — every third turn, a new fly at
|
|
460
|
+
// 5. Spawn — every third turn, a new fly at a seeded pick among the
|
|
461
|
+
// currently-uncontested perimeter cells (never Math.random — see
|
|
462
|
+
// seededPick's own header comment).
|
|
359
463
|
if (k % FLY_SPAWN_INTERVAL_TURNS === 0) {
|
|
360
464
|
const occupied = new Set();
|
|
361
465
|
for (const spiderId of spiders) { const c = postMovePlacements.get(spiderId); occupied.add(cellId(c.x, c.y)); }
|
|
@@ -369,9 +473,10 @@ export function runEcologyPass({ state, postMovePlacements, postMoveMassByFly, t
|
|
|
369
473
|
occupied.add(state.placements.get(eggId)?.cell);
|
|
370
474
|
}
|
|
371
475
|
for (const h of events.hatched) occupied.add(h.cell);
|
|
372
|
-
const
|
|
373
|
-
if (
|
|
476
|
+
const uncontested = perimeterCells().filter((c) => !occupied.has(c));
|
|
477
|
+
if (uncontested.length) {
|
|
374
478
|
const newFlyId = `fly-${1 + maxIdSuffix(state.placements.keys(), /^fly-(\d+)$/)}`;
|
|
479
|
+
const cell = seededPick(uncontested, `${WORLD_NAME}:${k}:${newFlyId}:spawn`);
|
|
375
480
|
writes.push({ subject: `${newFlyId}@turn${k}`, predicate: "mgx:currently-in", object: cell });
|
|
376
481
|
writes.push({ subject: `${newFlyId}@turn${k}`, predicate: "mgx:mass", object: String(FLY_INITIAL_MASS) });
|
|
377
482
|
events.spawned = newFlyId;
|
|
@@ -393,18 +498,26 @@ export async function startSpiderFlyGame(memoryDir, { flyCount = 1 } = {}) {
|
|
|
393
498
|
if (state.placements.has("spider-1")) return { started: false, facts: [] };
|
|
394
499
|
|
|
395
500
|
const perimeter = perimeterCells();
|
|
396
|
-
const facts = [
|
|
501
|
+
const facts = [
|
|
502
|
+
{ subject: "spider-1", predicate: "mgx:currently-in", object: cellId(WEB_HOME.x, WEB_HOME.y) },
|
|
503
|
+
{ subject: "spider-1", predicate: "mgx:mass", object: String(SPIDER_INITIAL_MASS) },
|
|
504
|
+
];
|
|
505
|
+
const occupied = new Set([cellId(WEB_HOME.x, WEB_HOME.y)]);
|
|
397
506
|
for (let i = 0; i < flyCount; i += 1) {
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
507
|
+
const flyId = `fly-${i + 1}`;
|
|
508
|
+
const uncontested = perimeter.filter((c) => !occupied.has(c));
|
|
509
|
+
const cell = seededPick(uncontested.length ? uncontested : perimeter, `${WORLD_NAME}:0:${flyId}:spawn`);
|
|
510
|
+
occupied.add(cell);
|
|
511
|
+
facts.push({ subject: flyId, predicate: "mgx:currently-in", object: cell });
|
|
512
|
+
facts.push({ subject: flyId, predicate: "mgx:mass", object: String(FLY_INITIAL_MASS) });
|
|
401
513
|
}
|
|
402
514
|
await appendFacts(memoryDir, facts.map((f) => ({ ...f, provenance: worldProvenanceTag(WORLD_NAME) })));
|
|
403
515
|
return { started: true, facts };
|
|
404
516
|
}
|
|
405
517
|
|
|
406
518
|
function goalLineFor(subject, believed, arrived, kind) {
|
|
407
|
-
if (
|
|
519
|
+
if (kind === "spider-avoid") return `avoiding ${believed.subject}, last seen at ${cellId(believed.cell.x, believed.cell.y)}.`;
|
|
520
|
+
if (!believed) return kind === "spider" ? "no fly in sight — holding position in the web." : "no spider in sight — wandering.";
|
|
408
521
|
const seenAt = cellId(believed.cell.x, believed.cell.y);
|
|
409
522
|
if (kind === "spider") {
|
|
410
523
|
return arrived
|
|
@@ -414,19 +527,37 @@ function goalLineFor(subject, believed, arrived, kind) {
|
|
|
414
527
|
return `evading — last saw ${believed.subject} at ${seenAt}.`;
|
|
415
528
|
}
|
|
416
529
|
|
|
530
|
+
/** Live (unexpired, by `turn`) dynamic webs from a `Map(webId -> {cell,
|
|
531
|
+
* builtAtTurn})` (either a folded state's own `.webs`, or that widened with
|
|
532
|
+
* web(s) minted THIS tick before they've been written/read back), as a
|
|
533
|
+
* plain array of { id, cell, builtAtTurn, expiresAtTurn }. Excludes the
|
|
534
|
+
* always-on static home zone (that's WEB_HOME/WEB_RADIUS, drawn separately —
|
|
535
|
+
* this is only the spider-built kind), for a renderer to draw distinctly. */
|
|
536
|
+
export function liveWebs(websMap, turn) {
|
|
537
|
+
const out = [];
|
|
538
|
+
for (const [id, { cell, builtAtTurn }] of websMap) {
|
|
539
|
+
if (builtAtTurn + WEB_DURATION_TURNS > turn) out.push({ id, cell, builtAtTurn, expiresAtTurn: builtAtTurn + WEB_DURATION_TURNS });
|
|
540
|
+
}
|
|
541
|
+
return out;
|
|
542
|
+
}
|
|
543
|
+
|
|
417
544
|
/**
|
|
418
545
|
* One full tick: fold state, compute each live spider's and fly's belief,
|
|
419
|
-
* replan/re-score, execute one movement step per agent
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
546
|
+
* replan/re-score, execute one movement step per agent (spiders: avoid other
|
|
547
|
+
* spiders > chase flies > hold-and-web; flies: evade > wander, unless
|
|
548
|
+
* trapped in an active web), run the ecology pass, and append everything as
|
|
549
|
+
* this turn's @turnN facts in one write. `opts.toldFacts` is the belief
|
|
550
|
+
* layer's chat-integration extension point (§4) — an array of `{ subject,
|
|
551
|
+
* toAgent, cell, turn }` rows, empty until a later piece of work wires
|
|
552
|
+
* chat-told positions through it.
|
|
424
553
|
*
|
|
425
|
-
* Returns `{ turn, writes, agents, ecology }`: `agents` is keyed
|
|
426
|
-
* live spider/fly subject after this tick, each `{ cell, goal,
|
|
427
|
-
* spider's `plan` is its found path's remaining
|
|
428
|
-
* has none this tick); `ecology` is the tick's
|
|
429
|
-
* spawned event summary
|
|
554
|
+
* Returns `{ turn, writes, agents, ecology, activeWebs }`: `agents` is keyed
|
|
555
|
+
* by every live spider/fly subject after this tick, each `{ cell, goal,
|
|
556
|
+
* plan, mass }` (the spider's `plan` is its found path's remaining
|
|
557
|
+
* directions, or null when it has none this tick); `ecology` is the tick's
|
|
558
|
+
* eaten/starved/laid/hatched/spawned event summary; `activeWebs` is every
|
|
559
|
+
* currently-live dynamic web (static home zone excluded — that's fixed grid
|
|
560
|
+
* geometry, not runtime state), for a renderer to draw distinctly.
|
|
430
561
|
*/
|
|
431
562
|
export async function runSpiderFlyTick(memoryDir, opts = {}) {
|
|
432
563
|
const { visionRadius = DEFAULT_VISION_RADIUS, toldFacts = [] } = opts;
|
|
@@ -441,51 +572,88 @@ export async function runSpiderFlyTick(memoryDir, opts = {}) {
|
|
|
441
572
|
const movementWrites = [];
|
|
442
573
|
const postMovePlacements = new Map();
|
|
443
574
|
const postMoveMassByFly = new Map();
|
|
575
|
+
const postMoveMassBySpider = new Map();
|
|
444
576
|
const agents = {};
|
|
577
|
+
const tickWebs = new Map(state.webs); // widened in place as spiders build/refresh this tick
|
|
578
|
+
let nextWebNum = 1 + maxIdSuffix(state.webs.keys(), /^web-(\d+)$/);
|
|
445
579
|
|
|
446
580
|
for (const spiderId of spiders) {
|
|
447
581
|
const spiderCell = parseCellId(state.placements.get(spiderId).cell);
|
|
448
|
-
const
|
|
449
|
-
|
|
582
|
+
const priorMass = state.mass.get(spiderId)?.value ?? SPIDER_INITIAL_MASS;
|
|
583
|
+
const newMass = Math.max(0, priorMass - SPIDER_MASS_DECREMENT_PER_TURN);
|
|
584
|
+
postMoveMassBySpider.set(spiderId, newMass);
|
|
585
|
+
|
|
586
|
+
// Priority 1: avoid any OTHER live spider believed visible.
|
|
587
|
+
const otherSpiders = spiders.filter((id) => id !== spiderId);
|
|
588
|
+
const avoidTarget = nearestBelievedTarget(spiderId, spiderCell, otherSpiders, state, { visionRadius, toldFacts });
|
|
589
|
+
let nextCell;
|
|
450
590
|
let plan = null;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
591
|
+
let goal;
|
|
592
|
+
if (avoidTarget) {
|
|
593
|
+
nextCell = greedySpiderAvoid(spiderCell, avoidTarget.cell, applyActions);
|
|
594
|
+
goal = goalLineFor(spiderId, avoidTarget, false, "spider-avoid");
|
|
595
|
+
} else {
|
|
596
|
+
// Priority 2: chase a believed-visible fly, exactly as before.
|
|
597
|
+
const target = nearestBelievedTarget(spiderId, spiderCell, flies, state, { visionRadius, toldFacts });
|
|
598
|
+
if (target) {
|
|
599
|
+
nextCell = spiderCell;
|
|
600
|
+
const path = planSpiderPath(spiderCell, target.cell, applyActions, state, k);
|
|
601
|
+
if (path) {
|
|
602
|
+
if (path.actions.length) { nextCell = path.states[1]; plan = path.actions; }
|
|
603
|
+
} else {
|
|
604
|
+
nextCell = greedySpiderApproach(spiderCell, target.cell, applyActions);
|
|
605
|
+
}
|
|
606
|
+
// "Arrived" is the real eat precondition (co-located with the
|
|
607
|
+
// believed target, inside an active web) — NOT merely "didn't move
|
|
608
|
+
// this turn", which a greedy-approach spider also does whenever it's
|
|
609
|
+
// already at its closest reachable cell but still a step away
|
|
610
|
+
// (Chebyshev-adjacent isn't co-located; has-exit-* edges have no
|
|
611
|
+
// diagonal hop).
|
|
612
|
+
const arrived = nextCell.x === target.cell.x && nextCell.y === target.cell.y && hasActiveWebAt(nextCell.x, nextCell.y, state, k);
|
|
613
|
+
goal = goalLineFor(spiderId, target, arrived, "spider");
|
|
455
614
|
} else {
|
|
456
|
-
|
|
615
|
+
// Priority 3: hold position, and build/refresh a web there unless an
|
|
616
|
+
// unexpired web already covers this exact cell.
|
|
617
|
+
nextCell = spiderCell;
|
|
618
|
+
const heldCellId = cellId(spiderCell.x, spiderCell.y);
|
|
619
|
+
if (!hasActiveWebAt(spiderCell.x, spiderCell.y, state, k)) {
|
|
620
|
+
const webId = `web-${nextWebNum}`;
|
|
621
|
+
nextWebNum += 1;
|
|
622
|
+
tickWebs.set(webId, { cell: heldCellId, builtAtTurn: k });
|
|
623
|
+
movementWrites.push({ subject: `${webId}@turn${k}`, predicate: "mgx:currently-in", object: heldCellId });
|
|
624
|
+
movementWrites.push({ subject: `${webId}@turn${k}`, predicate: "mgx:web-built-at-turn", object: String(k) });
|
|
625
|
+
goal = "no fly in sight — building a web here.";
|
|
626
|
+
} else {
|
|
627
|
+
goal = goalLineFor(spiderId, null, false, "spider");
|
|
628
|
+
}
|
|
457
629
|
}
|
|
458
630
|
}
|
|
631
|
+
|
|
459
632
|
postMovePlacements.set(spiderId, nextCell);
|
|
460
633
|
movementWrites.push({ subject: `${spiderId}@turn${k}`, predicate: "mgx:currently-in", object: cellId(nextCell.x, nextCell.y) });
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
// greedy-approach spider also does whenever it's already at its closest
|
|
464
|
-
// reachable cell but still a step away (Chebyshev-adjacent isn't
|
|
465
|
-
// co-located; has-exit-* edges have no diagonal hop). Using "didn't move"
|
|
466
|
-
// as the proxy previously mislabeled that stuck-but-not-there case as
|
|
467
|
-
// "co-located ... in the web" even when nowhere near the web.
|
|
468
|
-
const arrived = !!target && nextCell.x === target.cell.x && nextCell.y === target.cell.y && isInWebBlock(nextCell.x, nextCell.y);
|
|
469
|
-
agents[spiderId] = { cell: cellId(nextCell.x, nextCell.y), goal: goalLineFor(spiderId, target, arrived, "spider"), plan };
|
|
634
|
+
movementWrites.push({ subject: `${spiderId}@turn${k}`, predicate: "mgx:mass", object: String(newMass) });
|
|
635
|
+
agents[spiderId] = { cell: cellId(nextCell.x, nextCell.y), goal, plan, mass: newMass };
|
|
470
636
|
}
|
|
471
637
|
|
|
472
638
|
for (const flyId of flies) {
|
|
473
639
|
const flyCell = parseCellId(state.placements.get(flyId).cell);
|
|
474
640
|
const believedSpider = nearestBelievedTarget(flyId, flyCell, spiders, state, { visionRadius, toldFacts });
|
|
475
|
-
const
|
|
641
|
+
const webbed = hasActiveWebAt(flyCell.x, flyCell.y, state, k);
|
|
642
|
+
const nextCell = webbed ? flyCell : greedyFlyMove(flyCell, believedSpider?.cell ?? null, applyActions, k, flyId);
|
|
476
643
|
postMovePlacements.set(flyId, nextCell);
|
|
477
644
|
movementWrites.push({ subject: `${flyId}@turn${k}`, predicate: "mgx:currently-in", object: cellId(nextCell.x, nextCell.y) });
|
|
478
645
|
const priorMass = state.mass.get(flyId)?.value ?? FLY_INITIAL_MASS;
|
|
479
646
|
const newMass = Math.max(0, priorMass - FLY_MASS_DECREMENT_PER_TURN);
|
|
480
647
|
postMoveMassByFly.set(flyId, newMass);
|
|
481
648
|
movementWrites.push({ subject: `${flyId}@turn${k}`, predicate: "mgx:mass", object: String(newMass) });
|
|
482
|
-
|
|
649
|
+
const goal = webbed ? "trapped in an active web — can't move." : goalLineFor(flyId, believedSpider, true, "fly");
|
|
650
|
+
agents[flyId] = { cell: cellId(nextCell.x, nextCell.y), goal, mass: newMass };
|
|
483
651
|
}
|
|
484
652
|
|
|
485
|
-
const ecology = runEcologyPass({ state, postMovePlacements, postMoveMassByFly, turn: k });
|
|
653
|
+
const ecology = runEcologyPass({ state, postMovePlacements, postMoveMassByFly, postMoveMassBySpider, turn: k });
|
|
486
654
|
const writes = [...movementWrites, ...ecology.writes];
|
|
487
655
|
const provenance = `${worldProvenanceTag(WORLD_NAME)}:turn${k}`;
|
|
488
656
|
await appendFacts(memoryDir, writes.map((f) => ({ ...f, provenance })));
|
|
489
657
|
|
|
490
|
-
return { turn: k, writes, agents, ecology: ecology.events };
|
|
658
|
+
return { turn: k, writes, agents, ecology: ecology.events, activeWebs: liveWebs(tickWebs, k) };
|
|
491
659
|
}
|