@polycode-projects/the-mechanical-code-talker 2.8.0 → 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.
@@ -16,7 +16,7 @@
16
16
  // chat text.
17
17
 
18
18
  import {
19
- DIRECTION_DELTA, WORLD_NAME, cellId, parseCellId, inBounds, chebyshevDistance,
19
+ DIRECTION_DELTA, WORLD_NAME, cellId, parseCellId, inBounds, chebyshevDistance, oneStepDirectionBetween,
20
20
  } from "../domain/spider-fly-world.mjs";
21
21
  import { foldSpiderFlyState, runSpiderFlyTick, startSpiderFlyGame } from "./spider-fly.mjs";
22
22
  import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
@@ -170,6 +170,121 @@ function resolveTargetCell({ direction, cellLiteral, fromCell }) {
170
170
  return inBounds(nx, ny) ? { x: nx, y: ny } : null;
171
171
  }
172
172
 
173
+ // oneStepDirectionBetween lives in spider-fly-world.mjs (the shared grid
174
+ // geometry both this chat-turn layer and the engine need — see that
175
+ // module's own header comment); re-exported here so a caller of this file
176
+ // never has to reach into the domain layer just to build a deception pill's
177
+ // direction wording by hand.
178
+ export { oneStepDirectionBetween };
179
+
180
+ // ---- deception pills, built on the addressed teach-frame above: dynamic,
181
+ // per-tick chat-dock suggestions alongside (never replacing) the existing
182
+ // static 6-button address/direction rail. No new grammar at all — every
183
+ // pill's sentence is exactly the SAME SPIDER_FLY_TOLD_RE line above already
184
+ // accepts, filled in with either the subject's real position or a
185
+ // deliberately false one, so a human clicking one submits a plain
186
+ // "@spider the fly is east"-shaped line indistinguishable from a hand-typed
187
+ // claim, true or false alike. A pill's `truth` tag is for the human eye
188
+ // only — it never rides along in the submitted text itself.
189
+
190
+ const liveIdsOfKindFromAgents = (kind, agents) => {
191
+ const re = new RegExp(`^${kind}-\\d+$`);
192
+ return Object.keys(agents || {}).filter((id) => re.test(id)).sort();
193
+ };
194
+
195
+ /** "spider"/"fly" bare when exactly one individual of that kind is live
196
+ * (nothing to disambiguate), else the individual's own numbered id — a
197
+ * pill-set legibility choice, not a grammar restriction (the addressed
198
+ * teach-frame's own bare form always resolves to "the first live
199
+ * individual" regardless of count; this just stops offering a bare pill
200
+ * once it would read as ambiguous to a human watching two-plus). */
201
+ function agentPillLabel(kind, id, liveIdsOfKind) {
202
+ return liveIdsOfKind.length > 1 ? id : kind;
203
+ }
204
+
205
+ /** The point reflection of `cell` through the 10x10 board's center —
206
+ * cell-<11-x>-<11-y> — the canonical false-claim cell: deterministic (no
207
+ * seeded RNG needed, since pills are never persisted state), always
208
+ * in-bounds (1..10 reflects onto 1..10), and never accidentally true (x =
209
+ * 11-x has no integer solution for an integer x in 1..10, so the reflected
210
+ * cell can never coincide with the real one). */
211
+ function reflectedCell(cell) {
212
+ return { x: 11 - cell.x, y: 11 - cell.y };
213
+ }
214
+
215
+ /**
216
+ * The spider-fly chat dock's dynamic pill set for one tick's live `agents`
217
+ * (runSpiderFlyTick's/foldSpiderFlyState's own `{ id: { cell } }` shape —
218
+ * only `.cell` is read): one address pill per live spider/fly (bare
219
+ * "@spider" while exactly one of that kind is alive, numbered "@spider-2"
220
+ * once more than one is), plus — for whichever individual is currently
221
+ * addressed (`explicitAddresseeId`, falling back to `opts.defaultKind`'s
222
+ * first live individual, "spider" by default) — one true-claim and one
223
+ * canonical false-claim pill per live individual of the OPPOSITE kind (you
224
+ * address a spider about a fly's position, or a fly about a spider's — the
225
+ * predator/prey belief channel the addressed teach-frame already carries).
226
+ * A true-claim pill reads the nearest compass direction when the
227
+ * candidate's real cell sits exactly one cardinal step from the addressee's
228
+ * own cell (oneStepDirectionBetween), else the exact cell (always
229
+ * expressible). A false-claim pill always reads the exact cell form,
230
+ * holding the candidate's point-reflected cell (reflectedCell) — never a
231
+ * direction, since a fabricated direction has no single canonical,
232
+ * deterministic form the way a fabricated cell does.
233
+ *
234
+ * Returns `{ addressPills, claimPills, addresseeId }` — `addressPills` is
235
+ * `[{ id, kind, label }]`; `claimPills` is `[{ subjectId, truth, text,
236
+ * sentence }]` (`sentence` is the complete, ready-to-submit chat line);
237
+ * both empty when nothing is live. `addresseeId` is whichever individual
238
+ * the claim pills were actually built for (or null), so a caller can track
239
+ * "currently addressing" across ticks without re-deriving it. Pure.
240
+ */
241
+ export function pillsForSpiderFly(agents, explicitAddresseeId, opts = {}) {
242
+ const { defaultKind = "spider" } = opts;
243
+ const liveSpiders = liveIdsOfKindFromAgents("spider", agents);
244
+ const liveFlies = liveIdsOfKindFromAgents("fly", agents);
245
+
246
+ const addressPills = [
247
+ ...liveSpiders.map((id) => ({ id, kind: "spider", label: `@${agentPillLabel("spider", id, liveSpiders)}` })),
248
+ ...liveFlies.map((id) => ({ id, kind: "fly", label: `@${agentPillLabel("fly", id, liveFlies)}` })),
249
+ ];
250
+
251
+ const fallbackAddresseeId = (defaultKind === "fly" ? liveFlies[0] : liveSpiders[0]) ?? liveFlies[0] ?? liveSpiders[0] ?? null;
252
+ const addresseeId = (explicitAddresseeId && agents[explicitAddresseeId]) ? explicitAddresseeId : fallbackAddresseeId;
253
+ if (!addresseeId) return { addressPills, claimPills: [], addresseeId: null };
254
+
255
+ const addresseeKind = /^spider-\d+$/.test(addresseeId) ? "spider" : "fly";
256
+ const addresseeLabel = agentPillLabel(addresseeKind, addresseeId, addresseeKind === "spider" ? liveSpiders : liveFlies);
257
+ const addresseeCell = parseCellId(agents[addresseeId].cell);
258
+
259
+ const candidateKind = addresseeKind === "spider" ? "fly" : "spider";
260
+ const candidateIds = candidateKind === "spider" ? liveSpiders : liveFlies;
261
+
262
+ const claimPills = [];
263
+ for (const subjectId of candidateIds) {
264
+ const subjectLabel = agentPillLabel(candidateKind, subjectId, candidateIds);
265
+ const trueCell = parseCellId(agents[subjectId].cell);
266
+ const direction = oneStepDirectionBetween(addresseeCell, trueCell);
267
+ // A direction reads "is east"; the exact-cell fallback (used whenever
268
+ // there's no genuine one-step adjacency, and ALWAYS for the false claim
269
+ // below) reads "is at cell-x-y" — the two forms SPIDER_FLY_TOLD_RE
270
+ // itself accepts.
271
+ const trueValue = direction ? direction : `at ${cellId(trueCell.x, trueCell.y)}`;
272
+ const falseCell = reflectedCell(trueCell);
273
+ const falseValue = `at ${cellId(falseCell.x, falseCell.y)}`;
274
+ claimPills.push({
275
+ subjectId, truth: true,
276
+ text: `the ${subjectLabel} is ${trueValue}`,
277
+ sentence: `@${addresseeLabel} the ${subjectLabel} is ${trueValue}`,
278
+ });
279
+ claimPills.push({
280
+ subjectId, truth: false,
281
+ text: `the ${subjectLabel} is ${falseValue}`,
282
+ sentence: `@${addresseeLabel} the ${subjectLabel} is ${falseValue}`,
283
+ });
284
+ }
285
+ return { addressPills, claimPills, addresseeId };
286
+ }
287
+
173
288
  // ---- rendering one tick's return value as plain chat text --------------------
174
289
 
175
290
  function renderTickText(tick, addressedNote) {
@@ -181,10 +296,11 @@ function renderTickText(tick, addressedNote) {
181
296
  : `Turn ${tick.turn} — no agents remain on the board.`);
182
297
  const eco = tick.ecology;
183
298
  const events = [];
299
+ for (const c of eco.caught) events.push(`${c.spider} caught ${c.fly} at ${c.cell}`);
184
300
  for (const e of eco.eaten) events.push(`${e.fly} was eaten by ${e.spider} at ${e.cell}`);
185
301
  for (const f of eco.starved) events.push(`${f} starved`);
186
302
  if (eco.laid) events.push(`${eco.laid} was laid`);
187
- for (const h of eco.hatched) events.push(`${h.egg} hatched into ${h.spider} at ${h.cell}`);
303
+ for (const h of eco.hatched) events.push(`${h.egg} hatched into ${h.spiders.map((s) => s.spider).join(" and ")} at ${h.cell}`);
188
304
  if (eco.spawned) events.push(`${eco.spawned} arrived at the board edge`);
189
305
  if (events.length) parts.push(`${events.join("; ")}.`);
190
306
  return parts.join(" ");
@@ -205,10 +321,11 @@ function combinedGoalLine(agents) {
205
321
 
206
322
  function describeEcologyNote(eco) {
207
323
  const bits = [];
324
+ if (eco.caught.length) bits.push(`${eco.caught.length} caught`);
208
325
  if (eco.eaten.length) bits.push(`${eco.eaten.length} eaten`);
209
326
  if (eco.starved.length) bits.push(`${eco.starved.length} starved`);
210
327
  if (eco.laid) bits.push("1 laid");
211
- if (eco.hatched.length) bits.push(`${eco.hatched.length} hatched`);
328
+ if (eco.hatched.length) bits.push(`${eco.hatched.reduce((n, h) => n + h.spiders.length, 0)} hatched`);
212
329
  if (eco.spawned) bits.push("1 spawned");
213
330
  return bits.length ? `; ${bits.join(", ")}` : "";
214
331
  }