@polycode-projects/the-mechanical-code-talker 5.0.4 → 5.0.6

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.
@@ -6,14 +6,6 @@
6
6
  // on `{ as: character }` — the addressee is in the sentence, "@fox-1 look"),
7
7
  // and the simulation itself advances as ONE whole-world tick
8
8
  // (`runTownSquareTick`) rather than mud's per-character `autoplayTick`.
9
- //
10
- // `src/services/predator-prey.mjs` — the engine this file drives — does not
11
- // exist in every worktree yet; a concurrent track owns it. The import below
12
- // is guarded (dynamic, try/catch) so this module still loads, and every
13
- // test that imports mudiii-viz.mjs (which never imports this file) is
14
- // unaffected either way. `createMudiiiSession` throws a clear "the engine
15
- // isn't built yet" error if actually called before that track lands, rather
16
- // than a bare ERR_MODULE_NOT_FOUND with no context.
17
9
  import {
18
10
  createInMemoryStore, appendFacts, appendRule, loadMemory, readFactRows, removeFacts,
19
11
  } from "../../adapters/memory/core.mjs";
@@ -21,25 +13,22 @@ import { parseEntities } from "../../domain/codegraph.mjs";
21
13
  import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
22
14
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
23
15
  import { worldProvenanceTag } from "../../domain/worlds-pack.mjs";
24
- import { layoutNamed } from "../../domain/town-square-world.mjs";
16
+ import {
17
+ COMPASS_POINTS, DEFAULT_FACING, cellId, layoutNamed, parseCellId, reverseFacing, stepCellFrom, turnedFacing,
18
+ } from "../../domain/town-square-world.mjs";
19
+ import { findActionPath } from "../../domain/planning.mjs";
20
+ import { DEFAULT_GAME_CONFIG } from "../../domain/game-config.mjs";
25
21
  import { parseMudEditorText, planMudEditorSync, gridWorldEditorState } from "../../services/mud-editor.mjs";
22
+ import { pillsForMudiii } from "../../services/mudiii-turn.mjs";
23
+ import { relatedForTerm } from "../../domain/skos-view.mjs";
24
+ import { classAncestorChain } from "../../domain/sprite-map.mjs";
26
25
  import { createTurnSession } from "./turn-session.mjs";
27
26
  import { publishTmctSurface } from "./tmct-surface.mjs";
28
27
  import { graphAsk, enginePlan } from "./engine-surface.mjs";
29
-
30
- let engine = null;
31
- async function loadEngine() {
32
- if (engine) return engine;
33
- try {
34
- engine = await import("../../services/predator-prey.mjs");
35
- } catch (err) {
36
- throw new Error(
37
- "mudiii's engine (src/services/predator-prey.mjs) is not built in this worktree yet "
38
- + `(${err && err.message ? err.message : err})`,
39
- );
40
- }
41
- return engine;
42
- }
28
+ import {
29
+ foldTownSquareState, gridApplyActions, pathStateKey, placeFood as engPlaceFood, recastTownSquare,
30
+ roleOfId, runTownSquareTick, startTownSquareGame, townSquareBoard,
31
+ } from "../../services/predator-prey.mjs";
43
32
 
44
33
  /** `count` entries drawn at random from `roster`, in random order, without
45
34
  * repeats — mirrors mud-browser-entry.mjs's own `pickMudRoster`. `random` is
@@ -78,6 +67,76 @@ export function townSquareRosterArgs(agents, roleOf) {
78
67
  return { predatorCount, preyCount };
79
68
  }
80
69
 
70
+ // Where each press on the page's ring of controls sits relative to the agent's
71
+ // own facing. Up and down are steps — straight ahead and straight back — and
72
+ // the other six turn on the spot to the angle they sit at, which is what makes
73
+ // the ring one rule rather than eight cases.
74
+ const RING_ANGLES = Object.freeze({
75
+ up: 0, forward: 0,
76
+ "up-right": 45, right: 90, "down-right": 135,
77
+ down: 180, back: 180,
78
+ "down-left": -135, left: -90, "up-left": -45,
79
+ });
80
+ const RING_STEPS = new Set(["up", "forward", "down", "back"]);
81
+
82
+ /** The `manualMoves` entry one press asks of an agent standing at `cell` and
83
+ * facing `facing`: `{ cell, facing }` for a step, `{ facing }` alone for a
84
+ * turn on the spot. Null when `direction` names neither a ring press nor a
85
+ * compass point.
86
+ *
87
+ * `direction` is either a ring press — "up"/"forward" and "down"/"back" step
88
+ * straight ahead and straight back with the facing left alone, "left" and
89
+ * "right" turn ninety degrees, and the four diagonals turn to the angle they
90
+ * sit at (forty-five for the top pair, a hundred and thirty-five for the
91
+ * bottom) — or an absolute compass point, where a cardinal steps that way and
92
+ * faces that way and an intercardinal turns on the spot to face it.
93
+ *
94
+ * Whether the cell it names can be entered is the engine's answer rather than
95
+ * this function's: there is one legality table, and it is the world's own
96
+ * exit facts. Pure. */
97
+ export function driveRequest(direction, { cell, facing = DEFAULT_FACING } = {}) {
98
+ const press = String(direction ?? "").trim().toLowerCase();
99
+ if (!press) return null;
100
+ if (press in RING_ANGLES) {
101
+ const angle = RING_ANGLES[press];
102
+ if (!RING_STEPS.has(press)) {
103
+ const turned = turnedFacing(facing, angle);
104
+ return turned ? { facing: turned } : null;
105
+ }
106
+ const along = angle === 0 ? facing : reverseFacing(facing);
107
+ // An agent facing an intercardinal has no cell straight ahead of it — the
108
+ // grid carries no diagonal exit — so the press holds it where it stands.
109
+ const target = stepCellFrom(cell, along);
110
+ return target ? { cell: target, facing } : { facing };
111
+ }
112
+ const target = stepCellFrom(cell, press);
113
+ if (target) return { cell: target, facing: press };
114
+ return COMPASS_POINTS.includes(press) ? { facing: press } : null;
115
+ }
116
+
117
+ /** The shortest route from `fromCell` to `toCell` over `factRows`' own exit
118
+ * facts, as `{ cells, directions }` — `cells` runs from one end to the other
119
+ * inclusive, `directions` names one hop each. Null when nothing connects
120
+ * them, so a caller declines visibly rather than drawing a line through a
121
+ * building the board would never let anyone walk through.
122
+ *
123
+ * The exit table is the one legality answer, the same rows the engine's own
124
+ * chase and forage searches read, so a route drawn here is a route the world
125
+ * agrees with. */
126
+ export function routeBetweenCells(factRows, fromCell, toCell) {
127
+ const from = parseCellId(fromCell);
128
+ const to = parseCellId(toCell);
129
+ if (!from || !to) return null;
130
+ const found = findActionPath(
131
+ from,
132
+ (searchState) => searchState.x === to.x && searchState.y === to.y,
133
+ gridApplyActions(factRows),
134
+ { stateKey: pathStateKey },
135
+ );
136
+ if (!found) return null;
137
+ return { cells: found.states.map((s) => cellId(s.x, s.y)), directions: found.actions };
138
+ }
139
+
81
140
  /** A live, shared town-square world one visitor watches and talks over.
82
141
  * `worldPayload` is `{ name, facts, rules, opening }`, read once at build
83
142
  * time the same way every other viz page's world payload is. `agents` is
@@ -88,15 +147,14 @@ export function townSquareRosterArgs(agents, roleOf) {
88
147
  * (`runTownSquareTick`) and its conversation is a single shared dock.
89
148
  *
90
149
  * Returns `{ memoryDir, codeGraph, graph, refreshGraph, turn, tick, board,
91
- * snapshot, applyEdit, placeFood }`. A reset is not a method here: the
92
- * page re-opens a whole session for it, which is what a reset means when the
93
- * store is in memory and belongs to one visitor. */
94
- export async function createMudiiiSession(worldPayload, { agents = [], epoch = 0 } = {}) {
95
- const {
96
- startTownSquareGame, runTownSquareTick, townSquareBoard, foldTownSquareState,
97
- placeFood: engPlaceFood, roleOfId,
98
- } = await loadEngine();
99
-
150
+ * snapshot, applyEdit, placeFood, driveAgent, recast }`.
151
+ *
152
+ * `getTeachEnabled` is read fresh on every turn, so a visitor can tick the
153
+ * page's teach box mid-session and have the very next line read as a fact to
154
+ * store rather than a command to run. */
155
+ export async function createMudiiiSession(
156
+ worldPayload, { agents = [], epoch = 0, getTeachEnabled = () => false } = {},
157
+ ) {
100
158
  // Every engine call is layout-scoped: the world pack ships the BOARD, and
101
159
  // the layout carries the geometry plus the cast counts the engine mints the
102
160
  // animals from. Resolved once here so no call site has to remember it.
@@ -135,8 +193,11 @@ export async function createMudiiiSession(worldPayload, { agents = [], epoch = 0
135
193
 
136
194
  const turnSession = createTurnSession({
137
195
  memoryDir, graph: codeGraph, lexicon, sessionId: "town-square",
138
- vocabHint: 'Try "@fox-1 look", or "what does fox-1 believe".',
139
- buildExtraOptions: () => ({ planState: planHolder.state }),
196
+ vocabHint: 'Try "@fox the goblin is east", or "what does the fox see".',
197
+ buildExtraOptions: () => ({
198
+ planState: planHolder.state,
199
+ gameConfig: { ...DEFAULT_GAME_CONFIG, mudiii: { ...DEFAULT_GAME_CONFIG.mudiii, teach: getTeachEnabled() } },
200
+ }),
140
201
  captureExtraState: async (result, state) => {
141
202
  if ("planState" in result) planHolder.state = state.planState;
142
203
  },
@@ -150,6 +211,56 @@ export async function createMudiiiSession(worldPayload, { agents = [], epoch = 0
150
211
  return runTownSquareTick(memoryDir, { layout });
151
212
  }
152
213
 
214
+ /** Drive one agent by hand for a turn — `driveAgent("fox-1", "up")`, where
215
+ * `direction` is a ring press or a compass point and `driveRequest` above
216
+ * says what each one means.
217
+ *
218
+ * A press goes through the SAME whole-world tick every other turn goes
219
+ * through: it spends a turn, the ecology pass runs, and every other agent
220
+ * decides and moves. So the return IS a tick payload, with one field added:
221
+ * `driven` says whether the world took the press and where the agent ended
222
+ * up. A press the board refuses — a wall, a prop, an agent that is not on
223
+ * the board — still advances the world, and that agent decides for itself
224
+ * this turn rather than freezing. */
225
+ async function driveAgent(agentId, direction) {
226
+ const state = foldTownSquareState(readFactRows(await loadMemory(memoryDir)));
227
+ const standing = state.removed.has(agentId) ? null : state.placements.get(agentId);
228
+ const facing = state.facing.get(agentId)?.value ?? DEFAULT_FACING;
229
+ const request = standing ? driveRequest(direction, { cell: standing.cell, facing }) : null;
230
+ const result = await runTownSquareTick(memoryDir, {
231
+ layout, manualMoves: request ? { [agentId]: request } : {},
232
+ });
233
+ const accepted = result.rungs[agentId] === "driven";
234
+ const after = result.agents[agentId] ?? null;
235
+ return {
236
+ ...result,
237
+ driven: {
238
+ agent: agentId,
239
+ direction: String(direction ?? ""),
240
+ accepted,
241
+ from: standing?.cell ?? null,
242
+ cell: accepted && after ? after.cell : null,
243
+ facing: accepted && after ? after.facing : null,
244
+ },
245
+ };
246
+ }
247
+
248
+ /** Re-cast this same store onto a fresh epoch and hand back the opening
249
+ * board. This is what a Reset means once a store is live: the world's own
250
+ * facts, everything taught into it and everything the editor changed all
251
+ * stand, and only the animals are minted again. Re-opening a whole session
252
+ * instead would throw away the taught facts along with the cast, which is
253
+ * not what "reset the board" says.
254
+ *
255
+ * `agents` sizes the new cast the same way `createMudiiiSession`'s own
256
+ * roster does; `epoch` defaults to one past whatever the store is on. */
257
+ async function recast({ agents: nextAgents = [], epoch: nextEpoch = null } = {}) {
258
+ await recastTownSquare(memoryDir, {
259
+ layout, epoch: nextEpoch, ...townSquareRosterArgs(nextAgents, (id) => roleOfId(id)),
260
+ });
261
+ return townSquareBoard(memoryDir, { layout });
262
+ }
263
+
153
264
  /** The board as it stands, in the same payload shape `tick` returns, with no
154
265
  * turn spent. A page opens a session and then draws THIS — otherwise its
155
266
  * first sight of where anything stands is the first tick, and every mesh
@@ -201,6 +312,8 @@ export async function createMudiiiSession(worldPayload, { agents = [], epoch = 0
201
312
  refreshGraph,
202
313
  turn: turnSession.turn,
203
314
  tick,
315
+ driveAgent,
316
+ recast,
204
317
  board,
205
318
  snapshot,
206
319
  applyEdit,
@@ -220,5 +333,12 @@ publishTmctSurface({
220
333
  plan: enginePlan,
221
334
  page: {
222
335
  pickMudiiiRoster,
336
+ // pillsForMudiii closes over two other modules' bindings, so the page
337
+ // reaches it through this bag rather than a `.toString()` splice.
338
+ pillsForMudiii,
339
+ // What the editor's own suggestion rail reads for the word under the
340
+ // cursor: the lateral SKOS neighbourhood and the vertical is-a chain.
341
+ relatedForTerm, classAncestorChain,
342
+ routeBetweenCells,
223
343
  },
224
344
  });