@polycode-projects/the-mechanical-code-talker 4.1.9 → 5.0.1
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/README.md +1 -1
- package/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +35 -5
- package/corpus/worlds/shards/town-square-chapel.jsonl.gz +0 -0
- package/corpus/worlds/shards/town-square-market.jsonl.gz +0 -0
- package/corpus/worlds/shards/town-square.jsonl.gz +0 -0
- package/corpus/worlds/src/town-square-chapel.jsonl +929 -0
- package/corpus/worlds/src/town-square-market.jsonl +455 -0
- package/corpus/worlds/src/town-square.jsonl +687 -0
- package/data/mudiii-assets.json +201 -0
- package/package.json +8 -2
- package/src/domain/agent-belief.mjs +96 -0
- package/src/domain/answer-variants.json +1 -1
- package/src/domain/game-config.mjs +65 -0
- package/src/domain/memory/compaction.mjs +2 -0
- package/src/domain/town-square-world.mjs +416 -0
- package/src/services/adventure-editor.mjs +36 -0
- package/src/services/adventure.mjs +45 -15
- package/src/services/chat.mjs +25 -1
- package/src/services/mud-editor.mjs +80 -0
- package/src/services/mudiii-scene.mjs +766 -0
- package/src/services/mudiii-turn.mjs +670 -0
- package/src/services/mudiii-viz.mjs +1247 -0
- package/src/services/pill-complete.mjs +495 -0
- package/src/services/predator-prey.mjs +916 -0
- package/src/services/spider-fly.mjs +14 -64
- package/src/services/world-teach.mjs +214 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +18 -3
- package/src/surfaces/web/memory-ask-browser.bundle.js +126 -121
- package/src/surfaces/web/mudiii-browser-entry.mjs +224 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
// mudiii-turn.mjs — the chat lane for the headless town-square game: loading
|
|
2
|
+
// one of the three shipped layouts into the session's memory store, the stop
|
|
3
|
+
// command, the addressed teach-frame (extended to the food channel spider-fly
|
|
4
|
+
// has no equivalent of), the bare "tick" command, the player's own food-
|
|
5
|
+
// placement verb, and the belief and orientation asides. The fifth lane on
|
|
6
|
+
// the shared plan slot, shaped exactly like spider-fly-turn.mjs (PLAN_MUD_
|
|
7
|
+
// MUDIII.md, "The chat lane") with vocabulary from predator-prey.mjs's own
|
|
8
|
+
// MUDIII_ROLES: fox hunts goblin, goblins forage crumbs and morsels.
|
|
9
|
+
//
|
|
10
|
+
// This module never plans a move or runs the ecology pass itself — every bit
|
|
11
|
+
// of game logic (fold, pathfinding, belief, ecology, food placement) lives in
|
|
12
|
+
// predator-prey.mjs; this file only recognizes chat shapes, resolves them to
|
|
13
|
+
// that engine's own interface, and renders the result as chat text.
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
TOWN_SQUARE_LAYOUTS, DEFAULT_GRID_SIZE, DIRECTION_DELTA,
|
|
17
|
+
cellId, parseCellId, inBounds, chebyshevDistance, oneStepDirectionBetween,
|
|
18
|
+
agentKindOf, liveIdsOfKind, layoutNamed,
|
|
19
|
+
} from "../domain/town-square-world.mjs";
|
|
20
|
+
import {
|
|
21
|
+
MUDIII_ROLES, foldTownSquareState, startTownSquareGame, runTownSquareTick,
|
|
22
|
+
placeFood, roleOfId, beliefSnapshotFor,
|
|
23
|
+
} from "./predator-prey.mjs";
|
|
24
|
+
import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
|
|
25
|
+
import { getWorldsPackProvider } from "../adapters/corpus/worlds-pack.mjs";
|
|
26
|
+
import { appendFacts, appendRule, loadMemory, readFactRows } from "../adapters/memory/core.mjs";
|
|
27
|
+
import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
28
|
+
|
|
29
|
+
const PREDATOR_KIND = MUDIII_ROLES.predator.kind; // "fox"
|
|
30
|
+
const PREY_KIND = MUDIII_ROLES.prey.kind; // "goblin"
|
|
31
|
+
const SPAWNED_FOOD_KIND = MUDIII_ROLES.food.spawnedKind; // "crumb"
|
|
32
|
+
const PLACED_FOOD_KIND = MUDIII_ROLES.food.placedKind; // "morsel"
|
|
33
|
+
|
|
34
|
+
// ---- recognizers: the closed opening/stop/tick/address set -------------------
|
|
35
|
+
|
|
36
|
+
// One opener per shipped layout, plus a role-named alias for the headline
|
|
37
|
+
// square. Closed vocabulary (visit/watch/enter/start/begin — never "play",
|
|
38
|
+
// which is adventure's own named-opener verb and would otherwise race it for
|
|
39
|
+
// "play town square"). None of these ever collides with adventure's own
|
|
40
|
+
// generic opener (requires the literal word "adventure") or spider-fly's
|
|
41
|
+
// (requires the literal word "spider").
|
|
42
|
+
const MUDIII_HEADLINE_OPEN_RE =
|
|
43
|
+
/^(?:let'?s\s+)?(?:visit|watch|enter|start|begin)\s+(?:the\s+)?town\s+square(?:\s+game)?[.!?\s]*$/i;
|
|
44
|
+
const MUDIII_FOX_GOBLIN_OPEN_RE =
|
|
45
|
+
/^(?:let'?s\s+)?watch\s+the\s+foxe?s?\s+and\s+(?:the\s+)?goblins?[.!?\s]*$/i;
|
|
46
|
+
const MUDIII_MARKET_OPEN_RE =
|
|
47
|
+
/^(?:let'?s\s+)?(?:visit|watch|enter|start|begin)\s+(?:the\s+)?market\s+(?:square|day)(?:\s+game)?[.!?\s]*$/i;
|
|
48
|
+
const MUDIII_CHAPEL_OPEN_RE =
|
|
49
|
+
/^(?:let'?s\s+)?(?:visit|watch|enter|start|begin)\s+(?:the\s+)?chapel\s+corner(?:\s+game)?[.!?\s]*$/i;
|
|
50
|
+
|
|
51
|
+
/** The layout name an opening line names, or null when the line opens
|
|
52
|
+
* nothing this lane recognizes. Shape-only, closed vocabulary — never a
|
|
53
|
+
* generic "play X" grammar, so a name this pack does not ship (e.g. "play
|
|
54
|
+
* mudiii") stays unclaimed and falls to chat's own last-resort honest
|
|
55
|
+
* decline rather than this lane guessing at it. */
|
|
56
|
+
function matchMudiiiOpening(line) {
|
|
57
|
+
const l = String(line).trim();
|
|
58
|
+
if (MUDIII_HEADLINE_OPEN_RE.test(l) || MUDIII_FOX_GOBLIN_OPEN_RE.test(l)) return "town-square";
|
|
59
|
+
if (MUDIII_MARKET_OPEN_RE.test(l)) return "town-square-market";
|
|
60
|
+
if (MUDIII_CHAPEL_OPEN_RE.test(l)) return "town-square-chapel";
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// "stop watching" is this game's own stop word, same reasoning as
|
|
65
|
+
// spider-fly's: there is nothing to "play" in the sense of typing moves.
|
|
66
|
+
const MUDIII_STOP_RE =
|
|
67
|
+
/^(?:stop\s+(?:watching|playing)|quit\s+(?:the\s+)?(?:town\s+square\s+)?game|end\s+the\s+town\s+square\s+game|leave\s+the\s+game)[.!?\s]*$/i;
|
|
68
|
+
// The bare tick command, styled after spider-fly-turn.mjs's own (itself
|
|
69
|
+
// styled after the plan lane's PLAN_NEXT_RE).
|
|
70
|
+
const MUDIII_TICK_RE = /^(?:tick|next\s+turn|advance(?:\s+the\s+turn)?)[.!?\s]*$/i;
|
|
71
|
+
|
|
72
|
+
// The addressed teach-frame: "@fox the goblin is west" / "@fox-1 the goblin
|
|
73
|
+
// is at cell-7-3". Only a fox or a goblin can be ADDRESSED (only an agent has
|
|
74
|
+
// belief to plant), but the SUBJECT of the claim can also be a crumb or a
|
|
75
|
+
// morsel — the one extension spider-fly's teach-frame has no equivalent of,
|
|
76
|
+
// since food is a forage target a goblin can be lied to about. A closed
|
|
77
|
+
// regex, not a route through the general grammar, mirroring spider-fly-
|
|
78
|
+
// turn.mjs's own reasoning (the general placement/copula grammar hits real
|
|
79
|
+
// gaps for this exact shape).
|
|
80
|
+
const MUDIII_ADDRESS_LEAD_RE = /^@(fox|goblin)(?:-(\d+))?\b/i;
|
|
81
|
+
const MUDIII_TOLD_RE = new RegExp(
|
|
82
|
+
"^@(fox|goblin)(?:-(\\d+))?[,:]?\\s+the\\s+(fox|goblin|crumb|morsel)(?:-(\\d+))?\\s+is\\s+"
|
|
83
|
+
+ "(?:(north|south|east|west)|at\\s+(cell-\\d+-\\d+))[.!?\\s]*$",
|
|
84
|
+
"i",
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// The observable-facts read: "what does the goblin see?" — belief only,
|
|
88
|
+
// never ground truth.
|
|
89
|
+
const MUDIII_SEE_RE = /^what (?:does|can) the (fox|goblin)(?:-(\d+))?\s+see[.!?\s]*$/i;
|
|
90
|
+
|
|
91
|
+
// The player's own verb: "put food at cell-3-4" (primary, never shadowed —
|
|
92
|
+
// the adventure imperative grammar's own "put" arm hard-requires a literal
|
|
93
|
+
// "in", so "at" never parses there) and "drop a morsel at cell-3-4"
|
|
94
|
+
// (secondary alias: free while a mudiii game is the only thing live on the
|
|
95
|
+
// shared slot, but shadowed by the adventure/mud imperative grammar's bare-
|
|
96
|
+
// object "drop" arm should one of those be live in the same session instead —
|
|
97
|
+
// which can't happen while THIS game holds the slot, since the slot holds
|
|
98
|
+
// one thing at a time).
|
|
99
|
+
const MUDIII_PUT_FOOD_RE = /^(?:put|place)\s+(?:a\s+|some\s+)?(?:food|morsel)\s+(?:at|on)\s+(cell-\d+-\d+)[.!?\s]*$/i;
|
|
100
|
+
const MUDIII_DROP_FOOD_RE = /^drop\s+(?:a\s+|some\s+)?(?:morsel|food)\s+(?:at|on)\s+(cell-\d+-\d+)[.!?\s]*$/i;
|
|
101
|
+
|
|
102
|
+
const WORLD_OPENING_FALLBACK =
|
|
103
|
+
"a fox prowls the town square; goblins pick over the stalls for scraps. Neither is yours to move. Watch, or address one by name in chat.";
|
|
104
|
+
|
|
105
|
+
// ---- the opening turn: load the shipped board through the worlds pack -------
|
|
106
|
+
|
|
107
|
+
async function openMudiiiGame(world, { planHolder, memoryDir, env, cache, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
108
|
+
if (!memoryDir) {
|
|
109
|
+
return {
|
|
110
|
+
text: "the town square needs a session with a memory store to hold the board — start tmct inside a repo first.",
|
|
111
|
+
lane: "game-inform",
|
|
112
|
+
note: "MUDIII — opening declined: no memory store to load the board into",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const provider = getWorldsPackProvider(env);
|
|
116
|
+
let payload = null;
|
|
117
|
+
try { payload = await provider.load(world); } catch { payload = null; }
|
|
118
|
+
if (!payload) {
|
|
119
|
+
return {
|
|
120
|
+
text: 'no worlds pack here — the town square 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.',
|
|
121
|
+
lane: "game-inform",
|
|
122
|
+
note: "MUDIII — opening declined: the worlds pack is absent/unreadable",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const tag = worldProvenanceTag(world);
|
|
127
|
+
await appendFacts(memoryDir, payload.facts.map((f) => ({
|
|
128
|
+
subject: f.subject, predicate: f.predicate, object: f.object, provenance: tag,
|
|
129
|
+
})));
|
|
130
|
+
for (const rule of payload.rules) {
|
|
131
|
+
try { await appendRule(memoryDir, { name: rule.name, kind: rule.ruleKind, slots: rule.slots, provenance: tag }); }
|
|
132
|
+
catch { /* one malformed rule row loses that rule, not the board */ }
|
|
133
|
+
}
|
|
134
|
+
if (cache) cache.rows = null; // the fact-rows cache predates these writes
|
|
135
|
+
|
|
136
|
+
const layout = layoutNamed(world) ?? TOWN_SQUARE_LAYOUTS[world];
|
|
137
|
+
const { started } = await startTownSquareGame(memoryDir, { layout, config: gameConfig?.mudiii, roles: MUDIII_ROLES });
|
|
138
|
+
planHolder.state = { mudiii: { world, turn: 0 } };
|
|
139
|
+
const opener = started
|
|
140
|
+
? (payload.meta?.opening || layout?.opening || WORLD_OPENING_FALLBACK)
|
|
141
|
+
: 'back to the town square — the fox and the goblins are already in play. Say "tick" to advance, or address one, e.g. "@fox the goblin is east".';
|
|
142
|
+
return {
|
|
143
|
+
text: opener,
|
|
144
|
+
goal: 'watch the fox and the goblins, or address one (e.g. "@fox the goblin is east")',
|
|
145
|
+
lane: "game-inform",
|
|
146
|
+
note: `MUDIII — loaded the "${world}" board from the worlds pack into this session's memory (facts + rule rows, provenance ${tag}) and ${started ? "minted" : "found"} the starting cast`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---- resolving an addressed agent / belief target / food subject -------------
|
|
151
|
+
|
|
152
|
+
/** An exact "kind-num" reference, or (no number given) the first live
|
|
153
|
+
* individual of that kind — null when nothing live matches. `kind` names a
|
|
154
|
+
* fox, a goblin, a crumb or a morsel: the resolution is identical for an
|
|
155
|
+
* agent and an inert item, since both are placed subjects on the same
|
|
156
|
+
* folded board. */
|
|
157
|
+
function resolveAgentId(kind, num, state) {
|
|
158
|
+
if (num) {
|
|
159
|
+
const id = `${kind}-${num}`;
|
|
160
|
+
return state.placements.has(id) && !state.removed.has(id) ? id : null;
|
|
161
|
+
}
|
|
162
|
+
const live = liveIdsOfKind(state.placements, kind, state.removed);
|
|
163
|
+
return live[0] ?? null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Same as resolveAgentId, but with no number given it picks the live
|
|
167
|
+
* individual NEAREST `nearCell` — the natural reading of "the goblin" from a
|
|
168
|
+
* particular addressee's own position once the ecology has minted more than
|
|
169
|
+
* one fox or goblin, or more than one crumb. */
|
|
170
|
+
function resolveNearestAgentId(kind, num, state, nearCell) {
|
|
171
|
+
if (num) return resolveAgentId(kind, num, state);
|
|
172
|
+
const live = liveIdsOfKind(state.placements, kind, state.removed);
|
|
173
|
+
if (!live.length || !nearCell) return live[0] ?? null;
|
|
174
|
+
let best = live[0];
|
|
175
|
+
let bestDist = Infinity;
|
|
176
|
+
for (const id of live) {
|
|
177
|
+
const c = parseCellId(state.placements.get(id).cell);
|
|
178
|
+
const d = chebyshevDistance(nearCell.x, nearCell.y, c.x, c.y);
|
|
179
|
+
if (d < bestDist) { bestDist = d; best = id; }
|
|
180
|
+
}
|
|
181
|
+
return best;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function noSuchAgentAnswer(kind, role) {
|
|
185
|
+
const text = role === "addressee"
|
|
186
|
+
? `there's no live ${kind} on the board to address.`
|
|
187
|
+
: `there's no live ${kind} on the board for that to be about.`;
|
|
188
|
+
return { text, lane: "game-inform", note: `MUDIII — told-fact declined: no live ${kind} resolves as the ${role}`, miss: true };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The believed target cell, either the literal cell-<x>-<y> or the
|
|
192
|
+
* addressee's own current cell shifted one step in the stated compass
|
|
193
|
+
* direction — null when the result would fall off `gridSize`'s board (a
|
|
194
|
+
* layout property, not a module constant — the three shipped layouts run
|
|
195
|
+
* 10, 12 and 14 to a side). */
|
|
196
|
+
function resolveTargetCell({ direction, cellLiteral, fromCell, gridSize }) {
|
|
197
|
+
if (cellLiteral) {
|
|
198
|
+
const parsed = parseCellId(cellLiteral);
|
|
199
|
+
return parsed && inBounds(gridSize, parsed.x, parsed.y) ? parsed : null;
|
|
200
|
+
}
|
|
201
|
+
const delta = DIRECTION_DELTA[direction.toLowerCase()];
|
|
202
|
+
const nx = fromCell.x + delta.dx;
|
|
203
|
+
const ny = fromCell.y + delta.dy;
|
|
204
|
+
return inBounds(gridSize, nx, ny) ? { x: nx, y: ny } : null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// oneStepDirectionBetween lives in town-square-world.mjs (the shared grid
|
|
208
|
+
// geometry both this chat-turn layer and the engine need); re-exported here
|
|
209
|
+
// so a caller building a deception pill's direction wording never has to
|
|
210
|
+
// reach into the domain layer by hand.
|
|
211
|
+
export { oneStepDirectionBetween };
|
|
212
|
+
|
|
213
|
+
// ---- deception pills, built on the addressed teach-frame above: dynamic,
|
|
214
|
+
// per-tick chat-dock suggestions alongside (never replacing) any static
|
|
215
|
+
// address/direction rail. No new grammar at all — every pill's sentence is
|
|
216
|
+
// exactly the SAME MUDIII_TOLD_RE line above already accepts, filled in with
|
|
217
|
+
// either the subject's real position or a deliberately false one, so a human
|
|
218
|
+
// clicking one submits a plain "@fox the goblin is east"-shaped line
|
|
219
|
+
// indistinguishable from a hand-typed claim, true or false alike. A pill's
|
|
220
|
+
// `truth` tag is for the human eye only — it never rides along in the
|
|
221
|
+
// submitted text itself.
|
|
222
|
+
|
|
223
|
+
/** "fox"/"goblin" bare when exactly one individual of that kind is live
|
|
224
|
+
* (nothing to disambiguate), else the individual's own numbered id. */
|
|
225
|
+
function agentPillLabel(kind, id, liveIdsOfKindArr) {
|
|
226
|
+
return liveIdsOfKindArr.length > 1 ? id : kind;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The point reflection of `cell` through the `gridSize`x`gridSize` board's
|
|
230
|
+
* center — cell-<gridSize+1-x>-<gridSize+1-y> — the canonical false-claim
|
|
231
|
+
* cell: deterministic, always in-bounds (1..gridSize reflects onto
|
|
232
|
+
* 1..gridSize), and never accidentally true for any of the three shipped
|
|
233
|
+
* layouts, all of which run an EVEN gridSize (10, 12, 14): x =
|
|
234
|
+
* (gridSize+1)-x has no integer solution when gridSize+1 is odd. */
|
|
235
|
+
function reflectedCell(cell, gridSize) {
|
|
236
|
+
return { x: gridSize + 1 - cell.x, y: gridSize + 1 - cell.y };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The town-square chat dock's dynamic pill set for one tick's live `agents`
|
|
241
|
+
* and `items` (runTownSquareTick's own `{ id: { cell, ... } }` shapes — only
|
|
242
|
+
* `.cell` is read from either): one address pill per live fox/goblin (bare
|
|
243
|
+
* "@fox" while exactly one of that kind is alive, numbered "@fox-2" once more
|
|
244
|
+
* than one is), plus — for whichever individual is currently addressed
|
|
245
|
+
* (`explicitAddresseeId`, falling back to `opts.defaultKind`'s first live
|
|
246
|
+
* individual, the predator kind by default) — one true-claim and one
|
|
247
|
+
* canonical false-claim pill per live individual of the OPPOSITE role (you
|
|
248
|
+
* address a fox about a goblin's position, or a goblin about a fox's), plus,
|
|
249
|
+
* when the addressee is prey (the one role that forages), one true/false
|
|
250
|
+
* claim pill per live food item — the channel spider-fly has no equivalent
|
|
251
|
+
* of, since only a forager has any use for a claim about where food sits.
|
|
252
|
+
*
|
|
253
|
+
* Returns `{ addressPills, claimPills, addresseeId }` — `addressPills` is
|
|
254
|
+
* `[{ id, kind, label }]`; `claimPills` is `[{ subjectId, truth, text,
|
|
255
|
+
* sentence }]` (`sentence` is the complete, ready-to-submit chat line); both
|
|
256
|
+
* empty when nothing is live. `opts.gridSize` must name the live layout's own
|
|
257
|
+
* board size (falls back to the headline layout's 12 otherwise) — the false-
|
|
258
|
+
* claim reflection needs it. Pure.
|
|
259
|
+
*/
|
|
260
|
+
export function pillsForMudiii(agents, items, explicitAddresseeId, opts = {}) {
|
|
261
|
+
const { defaultKind = PREDATOR_KIND, gridSize = DEFAULT_GRID_SIZE, roles = MUDIII_ROLES } = opts;
|
|
262
|
+
const predatorKind = roles.predator.kind;
|
|
263
|
+
const preyKind = roles.prey.kind;
|
|
264
|
+
const livePredators = liveIdsOfKind(agents, predatorKind);
|
|
265
|
+
const livePrey = liveIdsOfKind(agents, preyKind);
|
|
266
|
+
|
|
267
|
+
const addressPills = [
|
|
268
|
+
...livePredators.map((id) => ({ id, kind: predatorKind, label: `@${agentPillLabel(predatorKind, id, livePredators)}` })),
|
|
269
|
+
...livePrey.map((id) => ({ id, kind: preyKind, label: `@${agentPillLabel(preyKind, id, livePrey)}` })),
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
const fallbackAddresseeId = (defaultKind === preyKind ? livePrey[0] : livePredators[0]) ?? livePrey[0] ?? livePredators[0] ?? null;
|
|
273
|
+
const addresseeId = (explicitAddresseeId && agents[explicitAddresseeId]) ? explicitAddresseeId : fallbackAddresseeId;
|
|
274
|
+
if (!addresseeId) return { addressPills, claimPills: [], addresseeId: null };
|
|
275
|
+
|
|
276
|
+
const addresseeKind = agentKindOf(addresseeId);
|
|
277
|
+
const addresseeLabel = agentPillLabel(addresseeKind, addresseeId, addresseeKind === predatorKind ? livePredators : livePrey);
|
|
278
|
+
const addresseeCell = parseCellId(agents[addresseeId].cell);
|
|
279
|
+
|
|
280
|
+
const candidateKind = addresseeKind === predatorKind ? preyKind : predatorKind;
|
|
281
|
+
const candidateIds = candidateKind === predatorKind ? livePredators : livePrey;
|
|
282
|
+
|
|
283
|
+
const claimPills = [];
|
|
284
|
+
const addClaim = (subjectId, subjectLabel, trueCell) => {
|
|
285
|
+
const direction = oneStepDirectionBetween(addresseeCell, trueCell);
|
|
286
|
+
const trueValue = direction ? direction : `at ${cellId(trueCell.x, trueCell.y)}`;
|
|
287
|
+
const falseCell = reflectedCell(trueCell, gridSize);
|
|
288
|
+
const falseValue = `at ${cellId(falseCell.x, falseCell.y)}`;
|
|
289
|
+
claimPills.push({
|
|
290
|
+
subjectId, truth: true,
|
|
291
|
+
text: `the ${subjectLabel} is ${trueValue}`,
|
|
292
|
+
sentence: `@${addresseeLabel} the ${subjectLabel} is ${trueValue}`,
|
|
293
|
+
});
|
|
294
|
+
claimPills.push({
|
|
295
|
+
subjectId, truth: false,
|
|
296
|
+
text: `the ${subjectLabel} is ${falseValue}`,
|
|
297
|
+
sentence: `@${addresseeLabel} the ${subjectLabel} is ${falseValue}`,
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
for (const subjectId of candidateIds) {
|
|
302
|
+
const subjectLabel = agentPillLabel(candidateKind, subjectId, candidateIds);
|
|
303
|
+
addClaim(subjectId, subjectLabel, parseCellId(agents[subjectId].cell));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// The food channel: only prey forages, so only a goblin addressee gets
|
|
307
|
+
// claim pills about a crumb or a morsel — a fox never plans toward food at
|
|
308
|
+
// all (predator-prey.mjs's decide() never reads the item roster for a
|
|
309
|
+
// predator), so offering it there would be a pill with no effect.
|
|
310
|
+
if (addresseeKind === roles.prey.kind) {
|
|
311
|
+
const liveItemIds = Object.keys(items || {}).sort();
|
|
312
|
+
for (const itemId of liveItemIds) {
|
|
313
|
+
addClaim(itemId, itemId, parseCellId(items[itemId].cell));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { addressPills, claimPills, addresseeId };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---- rendering one tick's return value as plain chat text --------------------
|
|
321
|
+
|
|
322
|
+
const ECOLOGY_EVENT_TEXT = {
|
|
323
|
+
"eat-agent": (e) => `${e.predator} caught ${e.prey} at ${e.cell}`,
|
|
324
|
+
"eat-item": (e) => `${e.agent} ate ${e.item} at ${e.cell}`,
|
|
325
|
+
starve: (e) => `${e.agent} starved at ${e.cell}`,
|
|
326
|
+
"spawn-prey": (e) => `${e.agent} arrived at ${e.cell}`,
|
|
327
|
+
"spawn-food": (e) => `${e.item} appeared at ${e.cell}`,
|
|
328
|
+
"place-food": (e) => `${e.placedBy} put ${e.item} at ${e.cell}`,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
function renderTickText(tick, addressedNote) {
|
|
332
|
+
const parts = [];
|
|
333
|
+
if (addressedNote) parts.push(`${addressedNote}.`);
|
|
334
|
+
const agentIds = Object.keys(tick.agents).sort();
|
|
335
|
+
parts.push(agentIds.length
|
|
336
|
+
? `Turn ${tick.turn} — ${agentIds.map((id) => `${id} is now at ${tick.agents[id].cell}`).join("; ")}.`
|
|
337
|
+
: `Turn ${tick.turn} — no agents remain on the board.`);
|
|
338
|
+
const itemIds = Object.keys(tick.items).sort();
|
|
339
|
+
if (itemIds.length) parts.push(`Food: ${itemIds.map((id) => `${id} at ${tick.items[id].cell}`).join("; ")}.`);
|
|
340
|
+
const events = tick.ecology.map((e) => ECOLOGY_EVENT_TEXT[e.type]?.(e)).filter(Boolean);
|
|
341
|
+
if (events.length) parts.push(`${events.join("; ")}.`);
|
|
342
|
+
return parts.join(" ");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Every live agent's own goal line folded into ONE string — withGoalLine
|
|
346
|
+
* only renders a single "Goal (inferred): …" suffix per turn, and this game
|
|
347
|
+
* always has several live agents with independent goals. Null once nothing
|
|
348
|
+
* survives the tick. */
|
|
349
|
+
function combinedGoalLine(agents) {
|
|
350
|
+
const ids = Object.keys(agents).sort();
|
|
351
|
+
if (!ids.length) return null;
|
|
352
|
+
return ids.map((id) => `${id} — ${agents[id].goal.replace(/\.\s*$/, "")}`).join("; ");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function describeEcologyNote(events) {
|
|
356
|
+
const counts = {};
|
|
357
|
+
for (const e of events) counts[e.type] = (counts[e.type] ?? 0) + 1;
|
|
358
|
+
const label = { "eat-agent": "caught", "eat-item": "foraged", starve: "starved", "spawn-prey": "arrived", "spawn-food": "spawned", "place-food": "placed" };
|
|
359
|
+
const bits = Object.entries(counts).map(([type, n]) => `${n} ${label[type] ?? type}`);
|
|
360
|
+
return bits.length ? `; ${bits.join(", ")}` : "";
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function runTickAndRender({ planHolder, memoryDir, cache, world, toldFacts = [], addressedNote = null, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
364
|
+
const tick = await runTownSquareTick(memoryDir, { layout: world, toldFacts, config: gameConfig?.mudiii, roles: MUDIII_ROLES });
|
|
365
|
+
if (cache) cache.rows = null;
|
|
366
|
+
planHolder.state = { mudiii: { world, turn: tick.turn } };
|
|
367
|
+
return {
|
|
368
|
+
text: renderTickText(tick, addressedNote),
|
|
369
|
+
goal: combinedGoalLine(tick.agents),
|
|
370
|
+
lane: "game-answer",
|
|
371
|
+
note: `MUDIII — turn ${tick.turn} (${world}): ran runTownSquareTick (${toldFacts.length ? "with an addressed told-fact" : "no addressed target"})${describeEcologyNote(tick.ecology)}`,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ---- the player's own verb: placing food ------------------------------------
|
|
376
|
+
|
|
377
|
+
async function runPlaceFoodTurn(cellLiteral, { memoryDir, gameConfig = DEFAULT_GAME_CONFIG, world, layout }) {
|
|
378
|
+
const result = await placeFood(memoryDir, {
|
|
379
|
+
layout, cell: cellLiteral, config: gameConfig?.mudiii, roles: MUDIII_ROLES, placedBy: "player",
|
|
380
|
+
});
|
|
381
|
+
if (!result.placed) {
|
|
382
|
+
return {
|
|
383
|
+
text: result.reason,
|
|
384
|
+
lane: "game-inform",
|
|
385
|
+
note: `MUDIII — food placement declined: ${result.reason}`,
|
|
386
|
+
miss: true,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
text: `noted — a ${result.kind} now sits at ${result.cell}. Say "tick" to see what happens.`,
|
|
391
|
+
goal: "bait or feed the goblins",
|
|
392
|
+
lane: "game-answer",
|
|
393
|
+
note: `MUDIII — placed ${result.item} (${result.kind}) at ${result.cell} for turn ${result.turn} (${world}), provenance carries mgx:placed-by ${result.placedBy}`,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ---- the observable-facts read: "what does the goblin see?" -----------------
|
|
398
|
+
|
|
399
|
+
/** One `[id, cellId | null]` belief entry as a sentence: `"fox-1 is at
|
|
400
|
+
* cell-3-4."` when observed/told, `"crumb-2 has not been observed."`
|
|
401
|
+
* otherwise. Pure, self-contained, `.toString()`-splice safe. */
|
|
402
|
+
export function believedFactSentence(id, believedCell) {
|
|
403
|
+
return believedCell ? `${id} is at ${believedCell}.` : `${id} has not been observed.`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** "what does the goblin see?" / "what does the fox see?" rendered as plain
|
|
407
|
+
* text: the same beliefSnapshotFor read predator-prey.mjs's own tick loop
|
|
408
|
+
* uses, over the CURRENT board state — read-only, no tick runs. Candidates
|
|
409
|
+
* are every other live agent of either role AND every live food item (a
|
|
410
|
+
* goblin's own forage target), exactly the beliefCandidates set
|
|
411
|
+
* runTownSquareTick itself builds. toldFacts is empty — a told position only
|
|
412
|
+
* ever arrives fresh alongside a tick, so there is none standing between
|
|
413
|
+
* ticks to read back here. */
|
|
414
|
+
async function mudiiiBeliefAnswer(match, { memoryDir, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
415
|
+
const kind = match[1].toLowerCase();
|
|
416
|
+
const num = match[2];
|
|
417
|
+
const rows = readFactRows(await loadMemory(memoryDir));
|
|
418
|
+
const state = foldTownSquareState(rows);
|
|
419
|
+
const observerId = resolveAgentId(kind, num, state);
|
|
420
|
+
if (!observerId) return noSuchAgentAnswer(kind, "addressee");
|
|
421
|
+
const observerCell = parseCellId(state.placements.get(observerId).cell);
|
|
422
|
+
const role = roleOfId(observerId, MUDIII_ROLES);
|
|
423
|
+
const candidateIds = [
|
|
424
|
+
...liveIdsOfKind(state.placements, PREDATOR_KIND, state.removed),
|
|
425
|
+
...liveIdsOfKind(state.placements, PREY_KIND, state.removed),
|
|
426
|
+
...liveIdsOfKind(state.placements, SPAWNED_FOOD_KIND, state.removed),
|
|
427
|
+
...liveIdsOfKind(state.placements, PLACED_FOOD_KIND, state.removed),
|
|
428
|
+
];
|
|
429
|
+
const visionRadius = role === "predator"
|
|
430
|
+
? gameConfig?.mudiii?.predatorVisionRadius
|
|
431
|
+
: gameConfig?.mudiii?.preyVisionRadius;
|
|
432
|
+
const belief = beliefSnapshotFor(observerId, observerCell, candidateIds, state, { visionRadius });
|
|
433
|
+
const entries = Object.entries(belief);
|
|
434
|
+
const text = entries.length
|
|
435
|
+
? `${observerId} sees: ${entries.map(([id, cell]) => believedFactSentence(id, cell)).join(" ")}`
|
|
436
|
+
: `${observerId} is alone on the board — nothing else to see.`;
|
|
437
|
+
return {
|
|
438
|
+
text,
|
|
439
|
+
lane: "game-inform",
|
|
440
|
+
note: `MUDIII — belief snapshot rendered for ${observerId} via beliefSnapshotFor (read-only, no tick run)`,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** The addressed teach-frame turn: resolve the addressee and the belief
|
|
445
|
+
* subject (an agent OR a food item), resolve the told cell, and run ONE tick
|
|
446
|
+
* with that told-fact fed in. Told-facts are not persisted across turns —
|
|
447
|
+
* each addressed line supplies belief for the NEXT tick only, mirroring
|
|
448
|
+
* spider-fly-turn.mjs's runToldFactTurn exactly. */
|
|
449
|
+
async function runToldFactTurn(match, { planHolder, memoryDir, cache, gameConfig = DEFAULT_GAME_CONFIG, world, layout }) {
|
|
450
|
+
const [, addrKindRaw, addrNum, subjKindRaw, subjNum, direction, cellLiteral] = match;
|
|
451
|
+
const addrKind = addrKindRaw.toLowerCase();
|
|
452
|
+
const subjKind = subjKindRaw.toLowerCase();
|
|
453
|
+
const rows = readFactRows(await loadMemory(memoryDir));
|
|
454
|
+
const state = foldTownSquareState(rows);
|
|
455
|
+
|
|
456
|
+
const addresseeId = resolveAgentId(addrKind, addrNum, state);
|
|
457
|
+
if (!addresseeId) return noSuchAgentAnswer(addrKind, "addressee");
|
|
458
|
+
const addresseeCell = parseCellId(state.placements.get(addresseeId).cell);
|
|
459
|
+
const subjectId = resolveNearestAgentId(subjKind, subjNum, state, addresseeCell);
|
|
460
|
+
if (!subjectId) return noSuchAgentAnswer(subjKind, "subject");
|
|
461
|
+
|
|
462
|
+
const targetCell = resolveTargetCell({ direction, cellLiteral, fromCell: addresseeCell, gridSize: layout.gridSize });
|
|
463
|
+
if (!targetCell) {
|
|
464
|
+
return {
|
|
465
|
+
text: `that falls off the edge of the ${layout.gridSize}x${layout.gridSize} board from where the ${addrKind} is — try a direction or cell that stays on the board.`,
|
|
466
|
+
lane: "game-inform",
|
|
467
|
+
note: "MUDIII — told-fact declined: the resolved cell falls outside the board",
|
|
468
|
+
miss: true,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const targetCellId = cellId(targetCell.x, targetCell.y);
|
|
473
|
+
const toldFacts = [{ subject: subjectId, toAgent: addresseeId, cell: targetCellId, turn: state.tickCount + 1 }];
|
|
474
|
+
return runTickAndRender({
|
|
475
|
+
planHolder, memoryDir, cache, world, toldFacts, gameConfig,
|
|
476
|
+
addressedNote: `told the ${addresseeId} the ${subjectId} is at ${targetCellId}`,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ---- in-game orientation asides ---------------------------------------------
|
|
481
|
+
//
|
|
482
|
+
// "where is the fox", "where am I", "what can I do", "what is the fox's
|
|
483
|
+
// goal" — while the board is live these must answer from the board, not fall
|
|
484
|
+
// through to the code-graph lanes, where "where is the fox" would read "fox"
|
|
485
|
+
// as a module name. There is no player piece here (every fox and goblin
|
|
486
|
+
// moves on its own), so "where am I" reports the watcher stance.
|
|
487
|
+
|
|
488
|
+
const MUDIII_WHERE_AGENT_RE = /^where(?:'s|\s+is|\s+are)\s+(?:the\s+)?(fox|goblin)(?:-\d+)?(?:\s+now)?[?.!\s]*$/i;
|
|
489
|
+
const MUDIII_WHERE_AM_I_RE = /^where\s+am\s+i(?:\s+now)?[?.!\s]*$/i;
|
|
490
|
+
const MUDIII_OPTIONS_RE = /^(?:what\s+can\s+i\s+do(?:\s+(?:here|now))?|what\s+are\s+my\s+options|what\s+(?:should|do)\s+i\s+do(?:\s+(?:here|now))?|what\s+now)[?.!\s]*$/i;
|
|
491
|
+
const MUDIII_GOAL_AGENT_RE = /^what(?:'s|\s+is)\s+(?:the\s+)?(fox|goblin)(?:-\d+)?'s\s+goal[?.!\s]*$/i;
|
|
492
|
+
const MUDIII_GOAL_OF_RE = /^what(?:'s|\s+is)\s+(?:the\s+)?goal\s+of\s+the\s+(fox|goblin)(?:-\d+)?[?.!\s]*$/i;
|
|
493
|
+
const MUDIII_GOAL_GENERIC_RE = /^(?:what(?:'s|\s+is)\s+(?:the\s+)?(?:goal|objective|point|aim)|what\s+are\s+they\s+(?:doing|trying\s+to\s+do))[?.!\s]*$/i;
|
|
494
|
+
|
|
495
|
+
const WATCHER_STANCE =
|
|
496
|
+
'you have no piece here — every fox and goblin moves on its own. Watch, say "tick" to advance, or address one, e.g. "@fox the goblin is east".';
|
|
497
|
+
const OPTIONS_TEXT =
|
|
498
|
+
'say "tick" to advance a turn, address an agent — e.g. "@fox the goblin is at cell-7-3" — or place food, e.g. "put food at cell-3-4". Say "stop watching" to end.';
|
|
499
|
+
const PREDATOR_GOAL_TEXT = "the fox hunts goblins for their mass, and keeps clear of any other fox it can see.";
|
|
500
|
+
const PREY_GOAL_TEXT = "goblins forage for crumbs and morsels, and evade any fox they can see — hunger loses to survival every time.";
|
|
501
|
+
const GENERIC_GOAL_TEXT =
|
|
502
|
+
'the fox hunts the goblins; the goblins forage for food and evade the fox. You watch it play out — plant a belief to nudge one, place food to bait one, or say "tick" to advance.';
|
|
503
|
+
|
|
504
|
+
const positionsOfKind = (kind, state) =>
|
|
505
|
+
liveIdsOfKind(state.placements, kind, state.removed).map((id) => `${id} at ${state.placements.get(id).cell}`);
|
|
506
|
+
|
|
507
|
+
async function mudiiiContextAnswer(line, { memoryDir }) {
|
|
508
|
+
const l = String(line).trim();
|
|
509
|
+
const whereAgent = l.match(MUDIII_WHERE_AGENT_RE);
|
|
510
|
+
const asksWhereMe = MUDIII_WHERE_AM_I_RE.test(l);
|
|
511
|
+
const asksOptions = MUDIII_OPTIONS_RE.test(l);
|
|
512
|
+
const goalAgentMatch = l.match(MUDIII_GOAL_AGENT_RE) || l.match(MUDIII_GOAL_OF_RE);
|
|
513
|
+
const asksGoalGeneric = MUDIII_GOAL_GENERIC_RE.test(l);
|
|
514
|
+
if (!whereAgent && !asksWhereMe && !asksOptions && !goalAgentMatch && !asksGoalGeneric) return null;
|
|
515
|
+
let state;
|
|
516
|
+
try { state = foldTownSquareState(readFactRows(await loadMemory(memoryDir))); } catch { return null; }
|
|
517
|
+
|
|
518
|
+
if (whereAgent) {
|
|
519
|
+
const kind = whereAgent[1].toLowerCase();
|
|
520
|
+
const positions = positionsOfKind(kind, state);
|
|
521
|
+
return {
|
|
522
|
+
text: positions.length ? `${positions.join("; ")}.` : `there's no live ${kind} on the board right now.`,
|
|
523
|
+
lane: "game-answer",
|
|
524
|
+
note: `MUDIII — where-aside: ${kind} positions from the current board fold`,
|
|
525
|
+
goal: `find the ${kind}`,
|
|
526
|
+
miss: !positions.length,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (asksWhereMe) {
|
|
531
|
+
return { text: WATCHER_STANCE, lane: "game-inform", note: "MUDIII — where-am-I aside: the watcher stance (no player piece)", goal: "understand your role" };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (asksOptions) {
|
|
535
|
+
return { text: OPTIONS_TEXT, lane: "game-inform", note: "MUDIII — options aside: the live game's own commands", goal: "see what you can do" };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (goalAgentMatch) {
|
|
539
|
+
const kind = goalAgentMatch[1].toLowerCase();
|
|
540
|
+
const text = kind === PREDATOR_KIND ? PREDATOR_GOAL_TEXT : PREY_GOAL_TEXT;
|
|
541
|
+
return { text, lane: "game-inform", note: `MUDIII — goal aside: the ${kind}'s own role objective`, goal: `understand the ${kind}` };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return { text: GENERIC_GOAL_TEXT, lane: "game-inform", note: "MUDIII — goal aside: the game's predator/prey/forage objective", goal: "understand the game" };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ---- the lane ------------------------------------------------------------
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* The whole town-square lane for one turn: the opening moves (one per
|
|
551
|
+
* layout), the stop command, the addressed teach-frame (agents and food
|
|
552
|
+
* alike), the bare tick command, the food-placement verb, the belief and
|
|
553
|
+
* orientation asides, and the one-at-a-time declines against the other
|
|
554
|
+
* lanes sharing planState's slot. Returns { text, lane, note, goal?, miss? }
|
|
555
|
+
* or null when the turn is not this lane's to answer.
|
|
556
|
+
*/
|
|
557
|
+
export async function mudiiiTurn(line, { planHolder, memoryDir, env, cache = null, isPlanFrameLine = () => false, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
558
|
+
const slot = planHolder?.state ?? null;
|
|
559
|
+
const mudiii = slot?.mudiii ?? null;
|
|
560
|
+
const openingWorld = matchMudiiiOpening(line);
|
|
561
|
+
|
|
562
|
+
if (!mudiii) {
|
|
563
|
+
if (!openingWorld) return null;
|
|
564
|
+
if (slot?.game) {
|
|
565
|
+
return {
|
|
566
|
+
text: 'a guess-the-number game is active — say "I give up" to end it, then start the town square.',
|
|
567
|
+
lane: "game-inform",
|
|
568
|
+
note: "MUDIII — an opening arrived mid-number-game; the slot holds one thing at a time",
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
if (slot?.adventure) {
|
|
572
|
+
return {
|
|
573
|
+
text: 'an adventure is running — say "stop playing" to end it, then start the town square.',
|
|
574
|
+
lane: "game-inform",
|
|
575
|
+
note: "MUDIII — an opening arrived mid-adventure; the slot holds one thing at a time",
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
if (slot?.spiderFly) {
|
|
579
|
+
return {
|
|
580
|
+
text: 'the spider-and-fly game is running — say "stop watching" to end it, then start the town square.',
|
|
581
|
+
lane: "game-inform",
|
|
582
|
+
note: "MUDIII — an opening arrived mid-spider-fly-game; the slot holds one thing at a time",
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const planActive = slot && !slot.done
|
|
586
|
+
&& ((Array.isArray(slot.goals) && slot.goals.length) || (Array.isArray(slot.actions) && slot.actions.length));
|
|
587
|
+
if (planActive) {
|
|
588
|
+
return {
|
|
589
|
+
text: 'a plan is in progress — finish it or say "forget the goal" before we visit the town square.',
|
|
590
|
+
lane: "game-inform",
|
|
591
|
+
note: "MUDIII — an opening arrived while a plan frame is active; the slot holds one thing at a time",
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
return openMudiiiGame(openingWorld, { planHolder, memoryDir, env, cache, gameConfig });
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// A game is live.
|
|
598
|
+
const layout = layoutNamed(mudiii.world);
|
|
599
|
+
if (!layout) {
|
|
600
|
+
planHolder.state = null;
|
|
601
|
+
return {
|
|
602
|
+
text: 'the town square\'s own board name did not resolve — the game ends here; say "visit the town square" to start fresh.',
|
|
603
|
+
lane: "game-inform",
|
|
604
|
+
note: `MUDIII — the plan slot named an unknown layout ("${mudiii.world}"); the game was ended rather than run against nothing`,
|
|
605
|
+
miss: true,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (openingWorld) {
|
|
610
|
+
return {
|
|
611
|
+
text: 'the town square is already running — say "stop watching" to end it first.',
|
|
612
|
+
lane: "game-inform",
|
|
613
|
+
note: "MUDIII — an opening arrived mid-game; declined, the running game stands",
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
if (MUDIII_STOP_RE.test(line)) {
|
|
617
|
+
planHolder.state = null;
|
|
618
|
+
return {
|
|
619
|
+
text: 'OK — the town square game ends here. Everything the board wrote stays remembered; say "visit the town square" to pick it back up.',
|
|
620
|
+
lane: "game-inform",
|
|
621
|
+
note: "MUDIII — the game ended on request; the board's facts stay in the store",
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
// The food verb is checked BEFORE the plan-frame guard below, because
|
|
625
|
+
// "put food at cell-3-4" reads as a planning frame on its leading verb and
|
|
626
|
+
// would otherwise be answered with "stop watching, then set your goal" — a
|
|
627
|
+
// refusal to do the one thing this lane's own grammar most explicitly
|
|
628
|
+
// offers. A line the lane owns outright is not a plan frame.
|
|
629
|
+
const trimmed = String(line).trim();
|
|
630
|
+
const putMatch = trimmed.match(MUDIII_PUT_FOOD_RE) || trimmed.match(MUDIII_DROP_FOOD_RE);
|
|
631
|
+
if (putMatch) {
|
|
632
|
+
return runPlaceFoodTurn(putMatch[1], { memoryDir, gameConfig, world: mudiii.world, layout });
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (isPlanFrameLine(line)) {
|
|
636
|
+
return {
|
|
637
|
+
text: 'the town square game is running — say "stop watching" to end it, then set your goal.',
|
|
638
|
+
lane: "game-inform",
|
|
639
|
+
note: "MUDIII — a plan frame arrived mid-game; the slot holds one thing at a time",
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (MUDIII_ADDRESS_LEAD_RE.test(line)) {
|
|
644
|
+
const told = trimmed.match(MUDIII_TOLD_RE);
|
|
645
|
+
if (!told) {
|
|
646
|
+
const addrKind = line.match(MUDIII_ADDRESS_LEAD_RE)[1].toLowerCase();
|
|
647
|
+
return {
|
|
648
|
+
text: `I heard you address the ${addrKind} but couldn't read a position from that — try "@${addrKind} the goblin is east" or "@${addrKind} the goblin is at cell-7-3".`,
|
|
649
|
+
lane: "game-inform",
|
|
650
|
+
note: "MUDIII — an addressed line didn't match the spatial teach-frame; honest decline, never a guess",
|
|
651
|
+
miss: true,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
return runToldFactTurn(told, { planHolder, memoryDir, cache, gameConfig, world: mudiii.world, layout });
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const seeMatch = trimmed.match(MUDIII_SEE_RE);
|
|
658
|
+
if (seeMatch) {
|
|
659
|
+
return mudiiiBeliefAnswer(seeMatch, { memoryDir, gameConfig });
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (MUDIII_TICK_RE.test(line)) {
|
|
663
|
+
return runTickAndRender({ planHolder, memoryDir, cache, world: mudiii.world, toldFacts: [], gameConfig });
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const contextAside = await mudiiiContextAnswer(line, { memoryDir });
|
|
667
|
+
if (contextAside) return contextAside;
|
|
668
|
+
|
|
669
|
+
return null; // an unaddressed aside — the ordinary lanes answer, board untouched
|
|
670
|
+
}
|