@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,352 @@
|
|
|
1
|
+
// spider-fly-turn.mjs — the chat lane for the headless spider-and-fly game
|
|
2
|
+
// (PLAN_SPIDER_FLY.md §6): loading the shipped board into the session's
|
|
3
|
+
// memory store, the stop command, the addressed spatial teach-frame that
|
|
4
|
+
// feeds a told-fact into the next tick, and the bare "tick" command this
|
|
5
|
+
// game's no-player-controlled-entity posture (§1 — both agents move on
|
|
6
|
+
// their own, every turn) needs for CLI use. Mirrors adventure.mjs's own
|
|
7
|
+
// shape exactly: closed-regex openers/stop, a slot-tagged one-at-a-time
|
|
8
|
+
// coexistence check against the other two lanes, a lane function returning
|
|
9
|
+
// { text, goal?, lane, note, miss? } or null when the turn is not this
|
|
10
|
+
// lane's to answer. The fourth of the four lanes sharing planState's slot.
|
|
11
|
+
//
|
|
12
|
+
// This module never plans a path or scores a move itself — every bit of
|
|
13
|
+
// game logic (fold, pathfinding, belief, ecology) lives in spider-fly.mjs;
|
|
14
|
+
// this file only recognizes chat shapes, resolves them to the shapes
|
|
15
|
+
// runSpiderFlyTick's own interface accepts, and renders its return value as
|
|
16
|
+
// chat text.
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
DIRECTION_DELTA, WORLD_NAME, cellId, parseCellId, inBounds, chebyshevDistance,
|
|
20
|
+
} from "../domain/spider-fly-world.mjs";
|
|
21
|
+
import { foldSpiderFlyState, runSpiderFlyTick, startSpiderFlyGame } from "./spider-fly.mjs";
|
|
22
|
+
import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
|
|
23
|
+
import { getWorldsPackProvider } from "../adapters/corpus/worlds-pack.mjs";
|
|
24
|
+
import { appendFacts, appendRule, loadMemory, readFactRows } from "../adapters/memory/core.mjs";
|
|
25
|
+
|
|
26
|
+
// ---- recognizers: the closed opening/stop/tick/address set -------------------
|
|
27
|
+
|
|
28
|
+
// The opener names the game without requiring a specific phrasing order —
|
|
29
|
+
// "watch the spider and the fly" / "play spider and fly" / "start the
|
|
30
|
+
// spider game" all match. Closed vocabulary only (watch/play/start/begin,
|
|
31
|
+
// spider, fly, game) — no general "start X" grammar, matching this
|
|
32
|
+
// project's standing preference and adventure.mjs's own opener style.
|
|
33
|
+
const SPIDER_FLY_OPEN_RE =
|
|
34
|
+
/^(?:let'?s\s+)?(?:watch|play|start|begin)\s+(?:the\s+)?spider(?:\s+and\s+(?:the\s+)?fly)?(?:\s+game)?[.!?\s]*$/i;
|
|
35
|
+
// "stop watching" is this game's own stop word (there's nothing to "play" in
|
|
36
|
+
// the sense of typing moves — you watch, or address an agent), kept
|
|
37
|
+
// alongside "stop playing" so either reads naturally depending on how the
|
|
38
|
+
// player thinks of the session.
|
|
39
|
+
const SPIDER_FLY_STOP_RE =
|
|
40
|
+
/^(?:stop\s+(?:watching|playing)|quit\s+(?:the\s+)?(?:spider\s+and\s+fly\s+)?game|end\s+the\s+spider(?:\s+and\s+fly)?\s+game|leave\s+the\s+game)[.!?\s]*$/i;
|
|
41
|
+
// The bare tick command — NOT specified by PLAN_SPIDER_FLY.md itself (§11's
|
|
42
|
+
// Play/step button is the page's own answer to "nothing requires the human
|
|
43
|
+
// to act"; this is that same need's CLI/chat equivalent). Styled after the
|
|
44
|
+
// plan lane's own PLAN_NEXT_RE ("next"/"next move"/"go on"/"continue").
|
|
45
|
+
const SPIDER_FLY_TICK_RE = /^(?:tick|next\s+turn|advance(?:\s+the\s+turn)?)[.!?\s]*$/i;
|
|
46
|
+
|
|
47
|
+
// The spatial teach-frame (§6.1): "@spider the fly is east" / "@spider the
|
|
48
|
+
// fly is at cell-7-3". Its own closed regex, not a route through
|
|
49
|
+
// parseRelation/parseCopula/parseOfForm — §6.1 found both hit real grammar
|
|
50
|
+
// gaps for this exact shape (the bare copula reading mints a nonsense
|
|
51
|
+
// subclass axiom; the "of" form hits parseAce's own hard-null guard before
|
|
52
|
+
// ever reaching parseRelation/parseCopula). A fixed compass set — north,
|
|
53
|
+
// south, east, west only, since the grid has no vertical axis — rather than
|
|
54
|
+
// ace.mjs's own IMPERATIVE_DIRECTIONS (private to that module, and carries
|
|
55
|
+
// up/down which never apply here). "@" is required (never optional) per the
|
|
56
|
+
// design brief's own worked examples. An optional numeric suffix on either
|
|
57
|
+
// noun ("spider-2", "fly-3") supports a board that has grown past one of
|
|
58
|
+
// each through the egg/hatch/spawn ecology.
|
|
59
|
+
const SPIDER_FLY_ADDRESS_LEAD_RE = /^@(spider|fly)(?:-(\d+))?\b/i;
|
|
60
|
+
const SPIDER_FLY_TOLD_RE = new RegExp(
|
|
61
|
+
"^@(spider|fly)(?:-(\\d+))?[,:]?\\s+the\\s+(spider|fly)(?:-(\\d+))?\\s+is\\s+"
|
|
62
|
+
+ "(?:(north|south|east|west)|at\\s+(cell-\\d+-\\d+))[.!?\\s]*$",
|
|
63
|
+
"i",
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const WORLD_OPENING_FALLBACK =
|
|
67
|
+
"a spider waits in its web; a fly drifts in from the edge of the board. Neither is yours to move — watch, or address one by name in chat.";
|
|
68
|
+
|
|
69
|
+
// ---- the opening turn: load the shipped board through the worlds pack -------
|
|
70
|
+
|
|
71
|
+
async function openSpiderFlyGame({ planHolder, memoryDir, env, cache }) {
|
|
72
|
+
if (!memoryDir) {
|
|
73
|
+
return {
|
|
74
|
+
text: "the spider-and-fly game needs a session with a memory store to hold the board — start tmct inside a repo first.",
|
|
75
|
+
lane: "game-inform",
|
|
76
|
+
note: "SPIDER-FLY — opening declined: no memory store to load the board into",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const provider = getWorldsPackProvider(env);
|
|
80
|
+
let payload = null;
|
|
81
|
+
try { payload = await provider.load(WORLD_NAME); } catch { payload = null; }
|
|
82
|
+
if (!payload) {
|
|
83
|
+
return {
|
|
84
|
+
text: 'no worlds pack here — the spider-and-fly board ships in corpus/worlds/ (or the directory TMCT_WORLDS_PACK_DIR names), and it is missing or unreadable, so there is no board to load.',
|
|
85
|
+
lane: "game-inform",
|
|
86
|
+
note: "SPIDER-FLY — opening declined: the worlds pack is absent/unreadable",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const tag = worldProvenanceTag(WORLD_NAME);
|
|
91
|
+
await appendFacts(memoryDir, payload.facts.map((f) => ({
|
|
92
|
+
subject: f.subject, predicate: f.predicate, object: f.object, provenance: tag,
|
|
93
|
+
})));
|
|
94
|
+
for (const rule of payload.rules) {
|
|
95
|
+
try { await appendRule(memoryDir, { name: rule.name, kind: rule.ruleKind, slots: rule.slots, provenance: tag }); }
|
|
96
|
+
catch { /* one malformed rule row loses that rule, not the board */ }
|
|
97
|
+
}
|
|
98
|
+
if (cache) cache.rows = null; // the fact-rows cache predates these writes
|
|
99
|
+
|
|
100
|
+
const { started } = await startSpiderFlyGame(memoryDir, { flyCount: 1 });
|
|
101
|
+
planHolder.state = { spiderFly: { turn: 0 } };
|
|
102
|
+
const opener = started
|
|
103
|
+
? (payload.meta?.opening || WORLD_OPENING_FALLBACK)
|
|
104
|
+
: 'back to the spider-and-fly board — the spider and fly are already in play. Say "tick" to advance, or address one, e.g. "@spider the fly is east".';
|
|
105
|
+
return {
|
|
106
|
+
text: opener,
|
|
107
|
+
goal: 'watch the spider and fly, or address one (e.g. "@spider the fly is east")',
|
|
108
|
+
lane: "game-inform",
|
|
109
|
+
note: `SPIDER-FLY — loaded the board from the worlds pack into this session's memory (facts + rule rows, provenance ${tag}) and ${started ? "minted" : "found"} the starting agents`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---- resolving an addressed agent / belief target ----------------------------
|
|
114
|
+
|
|
115
|
+
const liveIdsOfKind = (kind, state) => {
|
|
116
|
+
const re = new RegExp(`^${kind}-\\d+$`);
|
|
117
|
+
return [...state.placements.keys()].filter((id) => re.test(id) && !state.removed.has(id)).sort();
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** An exact "kind-num" reference, or (no number given) the first live
|
|
121
|
+
* individual of that kind — null when nothing live matches. */
|
|
122
|
+
function resolveAgentId(kind, num, state) {
|
|
123
|
+
if (num) {
|
|
124
|
+
const id = `${kind}-${num}`;
|
|
125
|
+
return state.placements.has(id) && !state.removed.has(id) ? id : null;
|
|
126
|
+
}
|
|
127
|
+
const live = liveIdsOfKind(kind, state);
|
|
128
|
+
return live[0] ?? null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Same as resolveAgentId, but with no number given it picks the live
|
|
132
|
+
* individual NEAREST `nearCell` — the natural reading of "the fly" from a
|
|
133
|
+
* particular addressee's own position once the ecology has minted more
|
|
134
|
+
* than one spider or fly. */
|
|
135
|
+
function resolveNearestAgentId(kind, num, state, nearCell) {
|
|
136
|
+
if (num) return resolveAgentId(kind, num, state);
|
|
137
|
+
const live = liveIdsOfKind(kind, state);
|
|
138
|
+
if (!live.length || !nearCell) return live[0] ?? null;
|
|
139
|
+
let best = live[0];
|
|
140
|
+
let bestDist = Infinity;
|
|
141
|
+
for (const id of live) {
|
|
142
|
+
const c = parseCellId(state.placements.get(id).cell);
|
|
143
|
+
const d = chebyshevDistance(nearCell.x, nearCell.y, c.x, c.y);
|
|
144
|
+
if (d < bestDist) { bestDist = d; best = id; }
|
|
145
|
+
}
|
|
146
|
+
return best;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function noSuchAgentAnswer(kind, role) {
|
|
150
|
+
const text = role === "addressee"
|
|
151
|
+
? `there's no live ${kind} on the board to address.`
|
|
152
|
+
: `there's no live ${kind} on the board for that to be about.`;
|
|
153
|
+
return { text, lane: "game-inform", note: `SPIDER-FLY — told-fact declined: no live ${kind} resolves as the ${role}`, miss: true };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The believed target cell, either the literal cell-<x>-<y> or the
|
|
157
|
+
* addressee's own current cell shifted one step in the stated compass
|
|
158
|
+
* direction — null when the result would fall off the 10x10 board. */
|
|
159
|
+
function resolveTargetCell({ direction, cellLiteral, fromCell }) {
|
|
160
|
+
if (cellLiteral) {
|
|
161
|
+
const parsed = parseCellId(cellLiteral);
|
|
162
|
+
return parsed && inBounds(parsed.x, parsed.y) ? parsed : null;
|
|
163
|
+
}
|
|
164
|
+
const delta = DIRECTION_DELTA[direction.toLowerCase()];
|
|
165
|
+
const nx = fromCell.x + delta.dx;
|
|
166
|
+
const ny = fromCell.y + delta.dy;
|
|
167
|
+
return inBounds(nx, ny) ? { x: nx, y: ny } : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---- rendering one tick's return value as plain chat text --------------------
|
|
171
|
+
|
|
172
|
+
function renderTickText(tick, addressedNote) {
|
|
173
|
+
const parts = [];
|
|
174
|
+
if (addressedNote) parts.push(`${addressedNote}.`);
|
|
175
|
+
const ids = Object.keys(tick.agents).sort();
|
|
176
|
+
parts.push(ids.length
|
|
177
|
+
? `Turn ${tick.turn} — ${ids.map((id) => `${id} is now at ${tick.agents[id].cell}`).join("; ")}.`
|
|
178
|
+
: `Turn ${tick.turn} — no agents remain on the board.`);
|
|
179
|
+
const eco = tick.ecology;
|
|
180
|
+
const events = [];
|
|
181
|
+
for (const e of eco.eaten) events.push(`${e.fly} was eaten by ${e.spider} at ${e.cell}`);
|
|
182
|
+
for (const f of eco.starved) events.push(`${f} starved`);
|
|
183
|
+
if (eco.laid) events.push(`${eco.laid} was laid`);
|
|
184
|
+
for (const h of eco.hatched) events.push(`${h.egg} hatched into ${h.spider} at ${h.cell}`);
|
|
185
|
+
if (eco.spawned) events.push(`${eco.spawned} arrived at the board edge`);
|
|
186
|
+
if (events.length) parts.push(`${events.join("; ")}.`);
|
|
187
|
+
return parts.join(" ");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Both agents' own goal lines folded into ONE string — withGoalLine only
|
|
191
|
+
* renders a single "Goal (inferred): …" suffix per turn, and this game
|
|
192
|
+
* always has two live agents (at minimum) with independent goals, so
|
|
193
|
+
* calling it once per agent isn't an option without restructuring the
|
|
194
|
+
* shared pipeline. Each fragment's own trailing period is stripped so the
|
|
195
|
+
* combined string still reads as ONE sentence once withGoalLine appends its
|
|
196
|
+
* own final period. */
|
|
197
|
+
function combinedGoalLine(agents) {
|
|
198
|
+
const ids = Object.keys(agents).sort();
|
|
199
|
+
if (!ids.length) return null;
|
|
200
|
+
return ids.map((id) => `${id} — ${agents[id].goal.replace(/\.\s*$/, "")}`).join("; ");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function describeEcologyNote(eco) {
|
|
204
|
+
const bits = [];
|
|
205
|
+
if (eco.eaten.length) bits.push(`${eco.eaten.length} eaten`);
|
|
206
|
+
if (eco.starved.length) bits.push(`${eco.starved.length} starved`);
|
|
207
|
+
if (eco.laid) bits.push("1 laid");
|
|
208
|
+
if (eco.hatched.length) bits.push(`${eco.hatched.length} hatched`);
|
|
209
|
+
if (eco.spawned) bits.push("1 spawned");
|
|
210
|
+
return bits.length ? `; ${bits.join(", ")}` : "";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function runTickAndRender({ planHolder, memoryDir, cache, toldFacts = [], addressedNote = null }) {
|
|
214
|
+
const tick = await runSpiderFlyTick(memoryDir, { toldFacts });
|
|
215
|
+
if (cache) cache.rows = null;
|
|
216
|
+
planHolder.state = { spiderFly: { turn: tick.turn } };
|
|
217
|
+
return {
|
|
218
|
+
text: renderTickText(tick, addressedNote),
|
|
219
|
+
goal: combinedGoalLine(tick.agents),
|
|
220
|
+
lane: "game-answer",
|
|
221
|
+
note: `SPIDER-FLY — turn ${tick.turn}: ran runSpiderFlyTick (${toldFacts.length ? "with an addressed told-fact" : "no addressed target"})${describeEcologyNote(tick.ecology)}`,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The addressed teach-frame turn: resolve the addressee and the belief
|
|
226
|
+
* subject, resolve the told cell, and run ONE tick with that told-fact fed
|
|
227
|
+
* in. Told-facts are NOT persisted on the session slot across turns — each
|
|
228
|
+
* addressed line supplies belief for the NEXT tick only, then is gone
|
|
229
|
+
* (the simplest of the plan doc's own named options, §4's "carry only the
|
|
230
|
+
* current turn's told-facts"). This also matches how runSpiderFlyTick
|
|
231
|
+
* itself already works: it holds no standing plan or belief between calls,
|
|
232
|
+
* recomputing everything fresh from the folded fact rows every tick. */
|
|
233
|
+
async function runToldFactTurn(match, { planHolder, memoryDir, cache }) {
|
|
234
|
+
const [, addrKindRaw, addrNum, subjKindRaw, subjNum, direction, cellLiteral] = match;
|
|
235
|
+
const addrKind = addrKindRaw.toLowerCase();
|
|
236
|
+
const subjKind = subjKindRaw.toLowerCase();
|
|
237
|
+
const rows = readFactRows(await loadMemory(memoryDir));
|
|
238
|
+
const state = foldSpiderFlyState(rows);
|
|
239
|
+
|
|
240
|
+
const addresseeId = resolveAgentId(addrKind, addrNum, state);
|
|
241
|
+
if (!addresseeId) return noSuchAgentAnswer(addrKind, "addressee");
|
|
242
|
+
const addresseeCell = parseCellId(state.placements.get(addresseeId).cell);
|
|
243
|
+
const subjectId = resolveNearestAgentId(subjKind, subjNum, state, addresseeCell);
|
|
244
|
+
if (!subjectId) return noSuchAgentAnswer(subjKind, "subject");
|
|
245
|
+
|
|
246
|
+
const targetCell = resolveTargetCell({ direction, cellLiteral, fromCell: addresseeCell });
|
|
247
|
+
if (!targetCell) {
|
|
248
|
+
return {
|
|
249
|
+
text: `that falls off the edge of the 10x10 board from where the ${addrKind} is — try a direction or cell that stays on the board.`,
|
|
250
|
+
lane: "game-inform",
|
|
251
|
+
note: "SPIDER-FLY — told-fact declined: the resolved cell falls outside the board",
|
|
252
|
+
miss: true,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const targetCellId = cellId(targetCell.x, targetCell.y);
|
|
257
|
+
const toldFacts = [{ subject: subjectId, toAgent: addresseeId, cell: targetCellId, turn: state.turnCount + 1 }];
|
|
258
|
+
return runTickAndRender({
|
|
259
|
+
planHolder, memoryDir, cache, toldFacts,
|
|
260
|
+
addressedNote: `told the ${addresseeId} the ${subjectId} is at ${targetCellId}`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---- the lane ------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The whole spider-and-fly lane for one turn: the opening moves, the stop
|
|
268
|
+
* command, the addressed spatial teach-frame (§6.1), the bare tick command,
|
|
269
|
+
* and the one-at-a-time declines across the shared plan slot both other
|
|
270
|
+
* lanes already implement pairwise. Returns { text, lane, note, goal?,
|
|
271
|
+
* miss? } or null when the turn is not this lane's to answer — an
|
|
272
|
+
* unaddressed aside (e.g. "where is the spider") falls through to the
|
|
273
|
+
* ordinary lanes unchanged, board untouched (§6.2 — no special-cased
|
|
274
|
+
* spider-fly code path for plain questions).
|
|
275
|
+
*/
|
|
276
|
+
export async function spiderFlyTurn(line, { planHolder, memoryDir, env, cache = null, isPlanFrameLine = () => false }) {
|
|
277
|
+
const slot = planHolder?.state ?? null;
|
|
278
|
+
const spiderFly = slot?.spiderFly ?? null;
|
|
279
|
+
const opening = SPIDER_FLY_OPEN_RE.test(line);
|
|
280
|
+
|
|
281
|
+
if (!spiderFly) {
|
|
282
|
+
if (!opening) return null;
|
|
283
|
+
if (slot?.game) {
|
|
284
|
+
return {
|
|
285
|
+
text: 'a guess-the-number game is active — say "I give up" to end it, then start the spider-and-fly game.',
|
|
286
|
+
lane: "game-inform",
|
|
287
|
+
note: "SPIDER-FLY — an opening arrived mid-number-game; the slot holds one thing at a time",
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (slot?.adventure) {
|
|
291
|
+
return {
|
|
292
|
+
text: 'an adventure is running — say "stop playing" to end it, then start the spider-and-fly game.',
|
|
293
|
+
lane: "game-inform",
|
|
294
|
+
note: "SPIDER-FLY — an opening arrived mid-adventure; the slot holds one thing at a time",
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const planActive = slot && !slot.done
|
|
298
|
+
&& ((Array.isArray(slot.goals) && slot.goals.length) || (Array.isArray(slot.actions) && slot.actions.length));
|
|
299
|
+
if (planActive) {
|
|
300
|
+
return {
|
|
301
|
+
text: 'a plan is in progress — finish it or say "forget the goal" before we start the spider-and-fly game.',
|
|
302
|
+
lane: "game-inform",
|
|
303
|
+
note: "SPIDER-FLY — an opening arrived while a plan frame is active; the slot holds one thing at a time",
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return openSpiderFlyGame({ planHolder, memoryDir, env, cache });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// A game is live.
|
|
310
|
+
if (opening) {
|
|
311
|
+
return {
|
|
312
|
+
text: 'the spider-and-fly game is already running — say "stop watching" to end it first.',
|
|
313
|
+
lane: "game-inform",
|
|
314
|
+
note: "SPIDER-FLY — an opening arrived mid-game; declined, the running game stands",
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (SPIDER_FLY_STOP_RE.test(line)) {
|
|
318
|
+
planHolder.state = null;
|
|
319
|
+
return {
|
|
320
|
+
text: 'OK — the spider-and-fly game ends here. Everything the board wrote stays remembered; say "watch the spider and the fly" to pick it back up.',
|
|
321
|
+
lane: "game-inform",
|
|
322
|
+
note: "SPIDER-FLY — the game ended on request; the board's facts stay in the store",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (isPlanFrameLine(line)) {
|
|
326
|
+
return {
|
|
327
|
+
text: 'the spider-and-fly game is running — say "stop watching" to end it, then set your goal.',
|
|
328
|
+
lane: "game-inform",
|
|
329
|
+
note: "SPIDER-FLY — a plan frame arrived mid-game; the slot holds one thing at a time",
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (SPIDER_FLY_ADDRESS_LEAD_RE.test(line)) {
|
|
334
|
+
const told = String(line).trim().match(SPIDER_FLY_TOLD_RE);
|
|
335
|
+
if (!told) {
|
|
336
|
+
const addrKind = line.match(SPIDER_FLY_ADDRESS_LEAD_RE)[1].toLowerCase();
|
|
337
|
+
return {
|
|
338
|
+
text: `I heard you address the ${addrKind} but couldn't read a position from that — try "@${addrKind} the fly is east" or "@${addrKind} the fly is at cell-7-3".`,
|
|
339
|
+
lane: "game-inform",
|
|
340
|
+
note: "SPIDER-FLY — an addressed line didn't match the spatial teach-frame; honest decline, never a guess",
|
|
341
|
+
miss: true,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return runToldFactTurn(told, { planHolder, memoryDir, cache });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (SPIDER_FLY_TICK_RE.test(line)) {
|
|
348
|
+
return runTickAndRender({ planHolder, memoryDir, cache, toldFacts: [] });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return null; // an unaddressed aside — the ordinary lanes answer, board untouched
|
|
352
|
+
}
|