@polycode-projects/the-mechanical-code-talker 5.0.4 → 5.0.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.
@@ -21,7 +21,9 @@ import { parseEntities } from "../../domain/codegraph.mjs";
21
21
  import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
22
22
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
23
23
  import { worldProvenanceTag } from "../../domain/worlds-pack.mjs";
24
- import { layoutNamed } from "../../domain/town-square-world.mjs";
24
+ import {
25
+ COMPASS_POINTS, DEFAULT_FACING, layoutNamed, reverseFacing, stepCellFrom, turnedFacing,
26
+ } from "../../domain/town-square-world.mjs";
25
27
  import { parseMudEditorText, planMudEditorSync, gridWorldEditorState } from "../../services/mud-editor.mjs";
26
28
  import { createTurnSession } from "./turn-session.mjs";
27
29
  import { publishTmctSurface } from "./tmct-surface.mjs";
@@ -78,6 +80,53 @@ export function townSquareRosterArgs(agents, roleOf) {
78
80
  return { predatorCount, preyCount };
79
81
  }
80
82
 
83
+ // Where each press on the page's ring of controls sits relative to the agent's
84
+ // own facing. Up and down are steps — straight ahead and straight back — and
85
+ // the other six turn on the spot to the angle they sit at, which is what makes
86
+ // the ring one rule rather than eight cases.
87
+ const RING_ANGLES = Object.freeze({
88
+ up: 0, forward: 0,
89
+ "up-right": 45, right: 90, "down-right": 135,
90
+ down: 180, back: 180,
91
+ "down-left": -135, left: -90, "up-left": -45,
92
+ });
93
+ const RING_STEPS = new Set(["up", "forward", "down", "back"]);
94
+
95
+ /** The `manualMoves` entry one press asks of an agent standing at `cell` and
96
+ * facing `facing`: `{ cell, facing }` for a step, `{ facing }` alone for a
97
+ * turn on the spot. Null when `direction` names neither a ring press nor a
98
+ * compass point.
99
+ *
100
+ * `direction` is either a ring press — "up"/"forward" and "down"/"back" step
101
+ * straight ahead and straight back with the facing left alone, "left" and
102
+ * "right" turn ninety degrees, and the four diagonals turn to the angle they
103
+ * sit at (forty-five for the top pair, a hundred and thirty-five for the
104
+ * bottom) — or an absolute compass point, where a cardinal steps that way and
105
+ * faces that way and an intercardinal turns on the spot to face it.
106
+ *
107
+ * Whether the cell it names can be entered is the engine's answer rather than
108
+ * this function's: there is one legality table, and it is the world's own
109
+ * exit facts. Pure. */
110
+ export function driveRequest(direction, { cell, facing = DEFAULT_FACING } = {}) {
111
+ const press = String(direction ?? "").trim().toLowerCase();
112
+ if (!press) return null;
113
+ if (press in RING_ANGLES) {
114
+ const angle = RING_ANGLES[press];
115
+ if (!RING_STEPS.has(press)) {
116
+ const turned = turnedFacing(facing, angle);
117
+ return turned ? { facing: turned } : null;
118
+ }
119
+ const along = angle === 0 ? facing : reverseFacing(facing);
120
+ // An agent facing an intercardinal has no cell straight ahead of it — the
121
+ // grid carries no diagonal exit — so the press holds it where it stands.
122
+ const target = stepCellFrom(cell, along);
123
+ return target ? { cell: target, facing } : { facing };
124
+ }
125
+ const target = stepCellFrom(cell, press);
126
+ if (target) return { cell: target, facing: press };
127
+ return COMPASS_POINTS.includes(press) ? { facing: press } : null;
128
+ }
129
+
81
130
  /** A live, shared town-square world one visitor watches and talks over.
82
131
  * `worldPayload` is `{ name, facts, rules, opening }`, read once at build
83
132
  * time the same way every other viz page's world payload is. `agents` is
@@ -88,7 +137,7 @@ export function townSquareRosterArgs(agents, roleOf) {
88
137
  * (`runTownSquareTick`) and its conversation is a single shared dock.
89
138
  *
90
139
  * Returns `{ memoryDir, codeGraph, graph, refreshGraph, turn, tick, board,
91
- * snapshot, applyEdit, placeFood }`. A reset is not a method here: the
140
+ * snapshot, applyEdit, placeFood, driveAgent }`. A reset is not a method here: the
92
141
  * page re-opens a whole session for it, which is what a reset means when the
93
142
  * store is in memory and belongs to one visitor. */
94
143
  export async function createMudiiiSession(worldPayload, { agents = [], epoch = 0 } = {}) {
@@ -150,6 +199,40 @@ export async function createMudiiiSession(worldPayload, { agents = [], epoch = 0
150
199
  return runTownSquareTick(memoryDir, { layout });
151
200
  }
152
201
 
202
+ /** Drive one agent by hand for a turn — `driveAgent("fox-1", "up")`, where
203
+ * `direction` is a ring press or a compass point and `driveRequest` above
204
+ * says what each one means.
205
+ *
206
+ * A press goes through the SAME whole-world tick every other turn goes
207
+ * through: it spends a turn, the ecology pass runs, and every other agent
208
+ * decides and moves. So the return IS a tick payload, with one field added:
209
+ * `driven` says whether the world took the press and where the agent ended
210
+ * up. A press the board refuses — a wall, a prop, an agent that is not on
211
+ * the board — still advances the world, and that agent decides for itself
212
+ * this turn rather than freezing. */
213
+ async function driveAgent(agentId, direction) {
214
+ const state = foldTownSquareState(readFactRows(await loadMemory(memoryDir)));
215
+ const standing = state.removed.has(agentId) ? null : state.placements.get(agentId);
216
+ const facing = state.facing.get(agentId)?.value ?? DEFAULT_FACING;
217
+ const request = standing ? driveRequest(direction, { cell: standing.cell, facing }) : null;
218
+ const result = await runTownSquareTick(memoryDir, {
219
+ layout, manualMoves: request ? { [agentId]: request } : {},
220
+ });
221
+ const accepted = result.rungs[agentId] === "driven";
222
+ const after = result.agents[agentId] ?? null;
223
+ return {
224
+ ...result,
225
+ driven: {
226
+ agent: agentId,
227
+ direction: String(direction ?? ""),
228
+ accepted,
229
+ from: standing?.cell ?? null,
230
+ cell: accepted && after ? after.cell : null,
231
+ facing: accepted && after ? after.facing : null,
232
+ },
233
+ };
234
+ }
235
+
153
236
  /** The board as it stands, in the same payload shape `tick` returns, with no
154
237
  * turn spent. A page opens a session and then draws THIS — otherwise its
155
238
  * first sight of where anything stands is the first tick, and every mesh
@@ -201,6 +284,7 @@ export async function createMudiiiSession(worldPayload, { agents = [], epoch = 0
201
284
  refreshGraph,
202
285
  turn: turnSession.turn,
203
286
  tick,
287
+ driveAgent,
204
288
  board,
205
289
  snapshot,
206
290
  applyEdit,