@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,416 @@
|
|
|
1
|
+
// town-square-world.mjs — the pure definition of every town-square board: cell
|
|
2
|
+
// naming, Chebyshev geometry, the prop tables that make buildings solid, the
|
|
3
|
+
// seed taxonomy, and the static fact/rule/meta rows each shipped layout
|
|
4
|
+
// carries. Pure — the only import is the sibling pure-data game-config.mjs, so
|
|
5
|
+
// the structural self-description facts below quote the same shipped defaults
|
|
6
|
+
// the engine actually runs.
|
|
7
|
+
//
|
|
8
|
+
// The one structural difference from spider-fly-world.mjs: grid size is a
|
|
9
|
+
// property of the layout, never a module constant. Three layouts ship at three
|
|
10
|
+
// sizes, so every geometry function takes the size (or the layout) as its first
|
|
11
|
+
// argument. A module-level GRID_SIZE would silently clip the 14x14 chapel board
|
|
12
|
+
// to whichever number was written here.
|
|
13
|
+
//
|
|
14
|
+
// Both scripts/gen-town-square-worlds.mjs (which writes
|
|
15
|
+
// corpus/worlds/src/<layout>.jsonl) and src/services/predator-prey.mjs (the
|
|
16
|
+
// runtime) read the SAME layouts from here, so a shipped world and the engine
|
|
17
|
+
// that plays it cannot drift apart.
|
|
18
|
+
|
|
19
|
+
import { DEFAULT_GAME_CONFIG } from "./game-config.mjs";
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_GRID_SIZE = 12;
|
|
22
|
+
export const DEFAULT_FACING = "south";
|
|
23
|
+
|
|
24
|
+
/** The species each role is cast as. The engine's own MUDIII_ROLES object
|
|
25
|
+
* (src/services/predator-prey.mjs) names the same four words; they are
|
|
26
|
+
* repeated rather than imported because a domain module may not read a
|
|
27
|
+
* service, and a test asserts the two agree. */
|
|
28
|
+
export const CAST_KINDS = Object.freeze({
|
|
29
|
+
predator: "fox",
|
|
30
|
+
prey: "goblin",
|
|
31
|
+
spawnedFood: "crumb",
|
|
32
|
+
placedFood: "morsel",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const cellId = (x, y) => `cell-${x}-${y}`;
|
|
36
|
+
|
|
37
|
+
const CELL_ID_RE = /^cell-(\d+)-(\d+)$/;
|
|
38
|
+
|
|
39
|
+
/** {x,y} from a "cell-<x>-<y>" id, or null when the string isn't one. */
|
|
40
|
+
export function parseCellId(id) {
|
|
41
|
+
const m = CELL_ID_RE.exec(String(id ?? ""));
|
|
42
|
+
return m ? { x: Number(m[1]), y: Number(m[2]) } : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const chebyshevDistance = (ax, ay, bx, by) => Math.max(Math.abs(ax - bx), Math.abs(ay - by));
|
|
46
|
+
|
|
47
|
+
export const inBounds = (gridSize, x, y) => x >= 1 && x <= gridSize && y >= 1 && y <= gridSize;
|
|
48
|
+
|
|
49
|
+
/** Every cell within Chebyshev `radius` of (cx, cy), clipped to a `gridSize`
|
|
50
|
+
* board, in raster order. Vision only — a prop's cell is visible, it just
|
|
51
|
+
* can't be walked into (see agent-belief.mjs: buildings block movement, not
|
|
52
|
+
* sight). */
|
|
53
|
+
export function visibleCells(gridSize, cx, cy, radius) {
|
|
54
|
+
const out = [];
|
|
55
|
+
for (let y = Math.max(1, cy - radius); y <= Math.min(gridSize, cy + radius); y += 1) {
|
|
56
|
+
for (let x = Math.max(1, cx - radius); x <= Math.min(gridSize, cx + radius); x += 1) {
|
|
57
|
+
out.push(cellId(x, y));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// direction -> (dx, dy). Plain labels reused verbatim from Ashcombe's
|
|
64
|
+
// mgx:has-exit-<direction> predicate (src/services/adventure.mjs) — north
|
|
65
|
+
// decreases y, south increases y, east increases x, west decreases x. Key
|
|
66
|
+
// order is the canonical direction order every consumer iterates in, for
|
|
67
|
+
// deterministic search tie-breaking.
|
|
68
|
+
export const DIRECTION_DELTA = Object.freeze({
|
|
69
|
+
north: Object.freeze({ dx: 0, dy: -1 }),
|
|
70
|
+
south: Object.freeze({ dx: 0, dy: 1 }),
|
|
71
|
+
east: Object.freeze({ dx: 1, dy: 0 }),
|
|
72
|
+
west: Object.freeze({ dx: -1, dy: 0 }),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/** The single compass direction from `fromCell` to `toCell` when `toCell` sits
|
|
76
|
+
* EXACTLY one cardinal step away — null for the same cell, a diagonal, or any
|
|
77
|
+
* multi-step gap, so a caller never overstates "adjacent". */
|
|
78
|
+
export function oneStepDirectionBetween(fromCell, toCell) {
|
|
79
|
+
for (const [direction, { dx, dy }] of Object.entries(DIRECTION_DELTA)) {
|
|
80
|
+
if (fromCell.x + dx === toCell.x && fromCell.y + dy === toCell.y) return direction;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- the prop vocabulary ------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/** The closed set of noun stems a prop id may use ("house-1" -> "house"). A
|
|
88
|
+
* town square's scenery is authored here and nowhere else, so the set is
|
|
89
|
+
* closed by construction and a reader can tell a prop from an animal or a
|
|
90
|
+
* crumb by its id alone. */
|
|
91
|
+
export const PROP_KINDS = Object.freeze([
|
|
92
|
+
"blacksmith", "bush", "cart", "fence", "house", "inn", "oak", "stall", "well",
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
/** The closed set of noun stems a food item's id may use. */
|
|
96
|
+
export const FOOD_KINDS = Object.freeze([CAST_KINDS.spawnedFood, CAST_KINDS.placedFood]);
|
|
97
|
+
|
|
98
|
+
/** The class an individual's id names — "goblin-2" -> "goblin", "crumb-10" ->
|
|
99
|
+
* "crumb". Self-contained, `.toString()`-splice safe. */
|
|
100
|
+
export function agentKindOf(id) {
|
|
101
|
+
return String(id).replace(/-\d+$/, "");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** True for a prop individual's id. A prop is placed with mgx:currently-in
|
|
105
|
+
* exactly like a live agent and is never one. */
|
|
106
|
+
export function isPropId(id) {
|
|
107
|
+
return PROP_KINDS.includes(agentKindOf(id));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** True for a food individual's id. Food is inert: a subject with a cell, no
|
|
111
|
+
* belief, no goal, no plan — the tick payload carries it in `items`, not in
|
|
112
|
+
* `agents`. */
|
|
113
|
+
export function isFoodId(id) {
|
|
114
|
+
return FOOD_KINDS.includes(agentKindOf(id));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Whether `id` belongs on a rendered AGENT roster built from `state` (a
|
|
118
|
+
* folded world state with a `.removed` Set): live, and neither a prop nor a
|
|
119
|
+
* food item. The one world rule every renderer of `state.placements` shares. */
|
|
120
|
+
export function isLiveRenderableAgent(id, state) {
|
|
121
|
+
return !state.removed.has(id) && !isPropId(id) && !isFoodId(id);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Every live id of `kind` among `agents`, sorted. `agents` is either a plain
|
|
125
|
+
* `{ id: ... }` roster or a `Map` keyed by id — pass the matching `removed`
|
|
126
|
+
* Set in the Map case to exclude anything the board has taken off it. Pure. */
|
|
127
|
+
export function liveIdsOfKind(agents, kind, removed = null) {
|
|
128
|
+
const re = new RegExp(`^${kind}-\\d+$`);
|
|
129
|
+
const ids = agents instanceof Map ? [...agents.keys()] : Object.keys(agents || {});
|
|
130
|
+
return ids.filter((id) => re.test(id) && !(removed && removed.has(id))).sort();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---- the three shipped layouts ------------------------------------------------
|
|
134
|
+
// Each is a literal prop table plus its cast counts. `model` is an asset key
|
|
135
|
+
// resolved by the render layer (data/mudiii-assets.json's `key` column), never
|
|
136
|
+
// a file path; `rotation` is degrees, written as a string because a fact object
|
|
137
|
+
// is a term.
|
|
138
|
+
|
|
139
|
+
const layout = (spec) => Object.freeze({ ...spec, props: Object.freeze(spec.props.map((p) => Object.freeze(p))), cast: Object.freeze(spec.cast) });
|
|
140
|
+
|
|
141
|
+
/** The headline square: a terrace of houses down the east side of the north
|
|
142
|
+
* edge, a well at the centre, one stall by the west wall and the inn in the
|
|
143
|
+
* south-east. The prop table is pinned by test/fixtures/mudiii-ticks.json —
|
|
144
|
+
* the ten recorded ticks are a seeded run over exactly this board, so moving
|
|
145
|
+
* a prop moves every cell in the fixture. */
|
|
146
|
+
const TOWN_SQUARE = layout({
|
|
147
|
+
name: "town-square",
|
|
148
|
+
gridSize: 12,
|
|
149
|
+
opening: "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.",
|
|
150
|
+
cast: { predators: 1, prey: 3 },
|
|
151
|
+
props: [
|
|
152
|
+
{ id: "house-1", model: "house-1", cell: "cell-8-1", rotation: "180" },
|
|
153
|
+
{ id: "house-2", model: "house-2", cell: "cell-8-2", rotation: "180" },
|
|
154
|
+
{ id: "house-3", model: "house-3", cell: "cell-8-3", rotation: "180" },
|
|
155
|
+
{ id: "well-1", model: "well", cell: "cell-6-7", rotation: "0" },
|
|
156
|
+
{ id: "stall-1", model: "market-stall-1", cell: "cell-3-9", rotation: "90" },
|
|
157
|
+
{ id: "inn-1", model: "inn", cell: "cell-11-10", rotation: "270" },
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/** Market day: two stall rows cut the square into three lanes, with gaps at
|
|
162
|
+
* the west wall, the middle and the east wall so no lane is sealed. */
|
|
163
|
+
const TOWN_SQUARE_MARKET = layout({
|
|
164
|
+
name: "town-square-market",
|
|
165
|
+
gridSize: 10,
|
|
166
|
+
opening: "market day in the town square: two rows of stalls, goblins working the lanes between them, and a fox somewhere among the crowd.",
|
|
167
|
+
cast: { predators: 1, prey: 4 },
|
|
168
|
+
props: [
|
|
169
|
+
{ id: "stall-1", model: "market-stall-1", cell: "cell-2-4", rotation: "0" },
|
|
170
|
+
{ id: "stall-2", model: "market-stall-2", cell: "cell-3-4", rotation: "0" },
|
|
171
|
+
{ id: "stall-3", model: "market-stall-1", cell: "cell-4-4", rotation: "0" },
|
|
172
|
+
{ id: "stall-4", model: "market-stall-2", cell: "cell-6-4", rotation: "0" },
|
|
173
|
+
{ id: "stall-5", model: "market-stall-1", cell: "cell-7-4", rotation: "0" },
|
|
174
|
+
{ id: "stall-6", model: "market-stall-2", cell: "cell-8-4", rotation: "0" },
|
|
175
|
+
{ id: "stall-7", model: "market-stall-2", cell: "cell-2-7", rotation: "180" },
|
|
176
|
+
{ id: "stall-8", model: "market-stall-1", cell: "cell-3-7", rotation: "180" },
|
|
177
|
+
{ id: "stall-9", model: "market-stall-2", cell: "cell-4-7", rotation: "180" },
|
|
178
|
+
{ id: "stall-10", model: "market-stall-1", cell: "cell-6-7", rotation: "180" },
|
|
179
|
+
{ id: "stall-11", model: "market-stall-2", cell: "cell-7-7", rotation: "180" },
|
|
180
|
+
{ id: "stall-12", model: "market-stall-1", cell: "cell-8-7", rotation: "180" },
|
|
181
|
+
{ id: "well-1", model: "well", cell: "cell-5-5", rotation: "0" },
|
|
182
|
+
{ id: "blacksmith-1", model: "blacksmith", cell: "cell-10-5", rotation: "270" },
|
|
183
|
+
{ id: "cart-1", model: "cart", cell: "cell-1-1", rotation: "90" },
|
|
184
|
+
{ id: "cart-2", model: "cart", cell: "cell-10-10", rotation: "270" },
|
|
185
|
+
{ id: "bush-1", model: "bush", cell: "cell-10-1", rotation: "0" },
|
|
186
|
+
{ id: "bush-2", model: "bush", cell: "cell-1-10", rotation: "0" },
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/** The chapel corner: an L of buildings in the north-west, a fence line across
|
|
191
|
+
* the south, three oaks. The only shipped layout with two predators, so it is
|
|
192
|
+
* where the avoid branch actually runs. */
|
|
193
|
+
const TOWN_SQUARE_CHAPEL = layout({
|
|
194
|
+
name: "town-square-chapel",
|
|
195
|
+
gridSize: 14,
|
|
196
|
+
opening: "the chapel corner of the town square, fenced to the south and shaded by three oaks. Two foxes hunt here, and two goblins know it.",
|
|
197
|
+
cast: { predators: 2, prey: 2 },
|
|
198
|
+
props: [
|
|
199
|
+
{ id: "inn-1", model: "inn", cell: "cell-2-2", rotation: "180" },
|
|
200
|
+
{ id: "house-1", model: "house-1", cell: "cell-3-2", rotation: "180" },
|
|
201
|
+
{ id: "house-2", model: "house-2", cell: "cell-2-3", rotation: "90" },
|
|
202
|
+
{ id: "fence-1", model: "fence", cell: "cell-7-11", rotation: "0" },
|
|
203
|
+
{ id: "fence-2", model: "fence", cell: "cell-8-11", rotation: "0" },
|
|
204
|
+
{ id: "fence-3", model: "fence", cell: "cell-9-11", rotation: "0" },
|
|
205
|
+
{ id: "oak-1", model: "oak-tree", cell: "cell-12-4", rotation: "0" },
|
|
206
|
+
{ id: "oak-2", model: "oak-tree", cell: "cell-12-12", rotation: "90" },
|
|
207
|
+
{ id: "oak-3", model: "oak-tree", cell: "cell-5-8", rotation: "180" },
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
export const TOWN_SQUARE_LAYOUTS = Object.freeze({
|
|
212
|
+
[TOWN_SQUARE.name]: TOWN_SQUARE,
|
|
213
|
+
[TOWN_SQUARE_MARKET.name]: TOWN_SQUARE_MARKET,
|
|
214
|
+
[TOWN_SQUARE_CHAPEL.name]: TOWN_SQUARE_CHAPEL,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
/** Every shipped layout name, in the order the scenario dropdown lists them —
|
|
218
|
+
* the headline square first. */
|
|
219
|
+
export const WORLD_NAMES = Object.freeze(Object.keys(TOWN_SQUARE_LAYOUTS));
|
|
220
|
+
|
|
221
|
+
/** The layout `name` denotes, or null. Never a guessed fallback: a caller that
|
|
222
|
+
* asked for a world this pack doesn't hold gets nothing, not the headline
|
|
223
|
+
* square wearing the wrong name. */
|
|
224
|
+
export function layoutNamed(name) {
|
|
225
|
+
return TOWN_SQUARE_LAYOUTS[String(name ?? "")] ?? null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---- solid cells: the one primitive the buildings mean --------------------------
|
|
229
|
+
// A prop's cell is solid. The exit-omission rule below, prey arrival, crumb
|
|
230
|
+
// spawning and the page's own click refusal all read this and nothing else.
|
|
231
|
+
|
|
232
|
+
const solidCache = new WeakMap();
|
|
233
|
+
|
|
234
|
+
/** Every cell a prop stands on, as a Set of cell ids. */
|
|
235
|
+
export function solidCells(lay) {
|
|
236
|
+
let cached = solidCache.get(lay);
|
|
237
|
+
if (!cached) {
|
|
238
|
+
cached = new Set(lay.props.map((p) => p.cell));
|
|
239
|
+
solidCache.set(lay, cached);
|
|
240
|
+
}
|
|
241
|
+
return cached;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Whether `cell` holds a prop, and so can never be entered. */
|
|
245
|
+
export function isSolid(lay, cell) {
|
|
246
|
+
return solidCells(lay).has(cell);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Every in-bounds cell no prop stands on, raster order. Where a crumb may be
|
|
250
|
+
* minted, where an agent may stand, and the set the connectivity guard floods
|
|
251
|
+
* through. */
|
|
252
|
+
export function openCells(lay) {
|
|
253
|
+
const solid = solidCells(lay);
|
|
254
|
+
const out = [];
|
|
255
|
+
for (let y = 1; y <= lay.gridSize; y += 1) {
|
|
256
|
+
for (let x = 1; x <= lay.gridSize; x += 1) {
|
|
257
|
+
const id = cellId(x, y);
|
|
258
|
+
if (!solid.has(id)) out.push(id);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Every board-edge cell no prop stands on, raster order — where a spawned
|
|
265
|
+
* prey arrives. Prop-free matters here rather than being a nicety: the
|
|
266
|
+
* headline layout has a house terrace and an inn sitting on the perimeter. */
|
|
267
|
+
export function perimeterCells(lay) {
|
|
268
|
+
const solid = solidCells(lay);
|
|
269
|
+
const out = [];
|
|
270
|
+
for (let y = 1; y <= lay.gridSize; y += 1) {
|
|
271
|
+
for (let x = 1; x <= lay.gridSize; x += 1) {
|
|
272
|
+
if (x !== 1 && x !== lay.gridSize && y !== 1 && y !== lay.gridSize) continue;
|
|
273
|
+
const id = cellId(x, y);
|
|
274
|
+
if (!solid.has(id)) out.push(id);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ---- the world's fact rows ------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
/** The seed taxonomy every town-square layout ships. It has one hard job: make
|
|
283
|
+
* objectClassChain reach "food" from both a crumb and a morsel, since that
|
|
284
|
+
* chain IS the forage read's precondition and a goblin must not be able to
|
|
285
|
+
* tell world food from player food. */
|
|
286
|
+
export const SEED_TAXONOMY = Object.freeze([
|
|
287
|
+
Object.freeze(["fox", "canine"]),
|
|
288
|
+
Object.freeze(["canine", "animal"]),
|
|
289
|
+
Object.freeze(["goblin", "humanoid"]),
|
|
290
|
+
Object.freeze(["humanoid", "creature"]),
|
|
291
|
+
Object.freeze(["creature", "animal"]),
|
|
292
|
+
Object.freeze(["crumb", "food"]),
|
|
293
|
+
Object.freeze(["morsel", "food"]),
|
|
294
|
+
Object.freeze(["food", "thing"]),
|
|
295
|
+
Object.freeze(["prop", "thing"]),
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
/** Four rows per prop: its class, its cell, the asset key a renderer resolves
|
|
299
|
+
* to a mesh, and its rotation in degrees. Every object is a string — "90",
|
|
300
|
+
* never 90 — because a fact object is a term, and the row validator takes
|
|
301
|
+
* non-empty strings only. */
|
|
302
|
+
export function* propFactRows(lay) {
|
|
303
|
+
for (const prop of lay.props) {
|
|
304
|
+
yield { world: lay.name, kind: "fact", subject: prop.id, predicate: "rdf:type", object: "prop" };
|
|
305
|
+
yield { world: lay.name, kind: "fact", subject: prop.id, predicate: "mgx:currently-in", object: prop.cell };
|
|
306
|
+
yield { world: lay.name, kind: "fact", subject: prop.id, predicate: "mgx:model", object: prop.model };
|
|
307
|
+
yield { world: lay.name, kind: "fact", subject: prop.id, predicate: "mgx:rotation", object: prop.rotation };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Every fact row the shipped world source carries: cell typing, grid
|
|
312
|
+
* adjacency with prop cells cut out of it, the prop placements, the seed
|
|
313
|
+
* taxonomy and the structural self-description.
|
|
314
|
+
*
|
|
315
|
+
* The omission rule, which is the whole of what a building does: EVERY
|
|
316
|
+
* in-bounds cell is typed, solid ones included, so "what is at cell-8-1?"
|
|
317
|
+
* grounds instead of missing on an undeclared individual. A solid cell emits
|
|
318
|
+
* no exits out, and no neighbour emits an exit into it. That is symmetric by
|
|
319
|
+
* construction, so a one-way edge into a building cannot be authored by
|
|
320
|
+
* accident, and gridApplyActions — which builds its whole successor closure
|
|
321
|
+
* from mgx:has-exit-* rows — routes around the buildings with no planner code
|
|
322
|
+
* at all. */
|
|
323
|
+
export function* worldFactRows(lay) {
|
|
324
|
+
const solid = solidCells(lay);
|
|
325
|
+
for (let y = 1; y <= lay.gridSize; y += 1) {
|
|
326
|
+
for (let x = 1; x <= lay.gridSize; x += 1) {
|
|
327
|
+
const id = cellId(x, y);
|
|
328
|
+
yield { world: lay.name, kind: "fact", subject: id, predicate: "rdf:type", object: "cell" };
|
|
329
|
+
if (solid.has(id)) continue;
|
|
330
|
+
for (const [direction, { dx, dy }] of Object.entries(DIRECTION_DELTA)) {
|
|
331
|
+
const nx = x + dx;
|
|
332
|
+
const ny = y + dy;
|
|
333
|
+
if (!inBounds(lay.gridSize, nx, ny)) continue;
|
|
334
|
+
const target = cellId(nx, ny);
|
|
335
|
+
if (solid.has(target)) continue;
|
|
336
|
+
yield { world: lay.name, kind: "fact", subject: id, predicate: `mgx:has-exit-${direction}`, object: target };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
yield* propFactRows(lay);
|
|
341
|
+
for (const [subject, superclass] of SEED_TAXONOMY) {
|
|
342
|
+
yield { world: lay.name, kind: "fact", subject, predicate: "rdfs:subClassOf", object: superclass };
|
|
343
|
+
}
|
|
344
|
+
yield* structuralFactRows(lay);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const pluralize = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
348
|
+
|
|
349
|
+
/** The board, the props and the cast restated as askable facts, so "what is
|
|
350
|
+
* the board?" and "what is the vision radius?" ground instead of missing.
|
|
351
|
+
* Every number is either fixed layout geometry or a shipped tuning default
|
|
352
|
+
* from game-config.mjs; the tunable ones say "by default", because a slider
|
|
353
|
+
* drag or a tmct.toml override moves them and a fact a slider falsifies is a
|
|
354
|
+
* lie. */
|
|
355
|
+
export function* structuralFactRows(lay) {
|
|
356
|
+
const fact = (subject, predicate, object) => ({ world: lay.name, kind: "fact", subject, predicate, object });
|
|
357
|
+
const knobs = DEFAULT_GAME_CONFIG.mudiii;
|
|
358
|
+
const { predator, prey, spawnedFood, placedFood } = CAST_KINDS;
|
|
359
|
+
const solidCount = solidCells(lay).size;
|
|
360
|
+
const cellCount = lay.gridSize * lay.gridSize;
|
|
361
|
+
|
|
362
|
+
yield fact("board", "rdf:type", "grid");
|
|
363
|
+
yield fact("board", "mgx:hasA", pluralize(lay.gridSize, "row"));
|
|
364
|
+
yield fact("board", "mgx:hasA", pluralize(lay.gridSize, "column"));
|
|
365
|
+
yield fact("board", "mgx:hasA", pluralize(cellCount, "cell"));
|
|
366
|
+
yield fact("square", "mgx:cover", `${pluralize(cellCount - solidCount, "open cell")} and ${pluralize(solidCount, "cell")} a prop stands on`);
|
|
367
|
+
yield fact("square", "mgx:start-with", `${pluralize(lay.cast.predators, predator)} and ${pluralize(lay.cast.prey, prey)} by default`);
|
|
368
|
+
yield fact("square", "mgx:hasA", `limit of ${pluralize(knobs.maxPreyPopulation, prey)} and ${pluralize(knobs.maxFoodItems, "food item")} at once by default`);
|
|
369
|
+
yield fact("prop", "mgx:cover", "one cell each, which nothing can walk into");
|
|
370
|
+
|
|
371
|
+
yield fact(predator, "mgx:hasA", `vision radius of ${pluralize(knobs.predatorVisionRadius, "cell")} by default`);
|
|
372
|
+
yield fact(predator, "mgx:start-with", `mass ${knobs.predatorInitialMass} by default`);
|
|
373
|
+
yield fact(predator, "mgx:lose", `${knobs.predatorMassDecrementPerTurn} mass per turn by default`);
|
|
374
|
+
yield fact(prey, "mgx:hasA", `vision radius of ${pluralize(knobs.preyVisionRadius, "cell")} by default`);
|
|
375
|
+
yield fact(prey, "mgx:start-with", `mass ${knobs.preyInitialMass} by default`);
|
|
376
|
+
yield fact(prey, "mgx:lose", `${knobs.preyMassDecrementPerTurn} mass per turn by default`);
|
|
377
|
+
yield fact(prey, "mgx:arrive", `every ${pluralize(knobs.preySpawnIntervalTurns, "turn")} at the edge of the board by default`);
|
|
378
|
+
yield fact(spawnedFood, "mgx:arrive", `every ${pluralize(knobs.foodSpawnIntervalTurns, "turn")} on any open cell by default`);
|
|
379
|
+
yield fact(spawnedFood, "mgx:start-with", `mass ${knobs.spawnedFoodMass} by default`);
|
|
380
|
+
yield fact(placedFood, "mgx:start-with", `mass ${knobs.placedFoodMass} by default`);
|
|
381
|
+
yield fact(placedFood, "mgx:arrive", "where the player puts it");
|
|
382
|
+
|
|
383
|
+
// The page's own slider label is "vision radius", so that exact term has to
|
|
384
|
+
// describe too, not just the per-class rows above. One combined row while
|
|
385
|
+
// the two class defaults agree, one row per class once they split — never a
|
|
386
|
+
// combined claim the config doesn't make. This cast splits them: the
|
|
387
|
+
// predator sees further than the prey, and the one-cell band that opens is
|
|
388
|
+
// the point.
|
|
389
|
+
if (knobs.predatorVisionRadius === knobs.preyVisionRadius) {
|
|
390
|
+
yield fact("vision radius", "mgx:hasProperty", `${pluralize(knobs.predatorVisionRadius, "cell")} for both the ${predator} and the ${prey} by default`);
|
|
391
|
+
} else {
|
|
392
|
+
yield fact("vision radius", "mgx:hasProperty", `${pluralize(knobs.predatorVisionRadius, "cell")} for the ${predator} by default`);
|
|
393
|
+
yield fact("vision radius", "mgx:hasProperty", `${pluralize(knobs.preyVisionRadius, "cell")} for the ${prey} by default`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** The layout's one meta row (its opening line). */
|
|
398
|
+
export function worldMetaRow(lay) {
|
|
399
|
+
return { world: lay.name, kind: "meta", opening: lay.opening };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** A minimal, inert rule-row family, so scripts/build-worlds-pack.mjs's shared
|
|
403
|
+
* validator ("every world needs at least one rule row") passes. The engine
|
|
404
|
+
* never reads these back: grid movement is hand-written pathfinding over
|
|
405
|
+
* findActionPath/findReachableSet, not the taught action-Rule DSL, whose
|
|
406
|
+
* precondition shapes cannot express grid adjacency. */
|
|
407
|
+
export function* worldRuleRows(lay) {
|
|
408
|
+
yield {
|
|
409
|
+
world: lay.name, kind: "rule", name: "go", ruleKind: "action-signature",
|
|
410
|
+
slots: { subjectClass: "animal", targetClass: "cell" },
|
|
411
|
+
};
|
|
412
|
+
yield {
|
|
413
|
+
world: lay.name, kind: "rule", name: "go", ruleKind: "action-effect",
|
|
414
|
+
slots: { predicate: "currently-in", subjectRole: "subject", objectRole: "target" },
|
|
415
|
+
};
|
|
416
|
+
}
|
|
@@ -358,6 +358,42 @@ export function planWorldEditorSync(rows, state, triples) {
|
|
|
358
358
|
return { toAppend, toRemoveIds };
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
/** The additive half of planWorldEditorSync, for ONE already-parsed triple:
|
|
362
|
+
* the rows a single taught sentence implies, and never a retraction. A whole
|
|
363
|
+
* document says what the world contains, so a fact missing from it has gone;
|
|
364
|
+
* one sentence only ever says what it says, so nothing it leaves out is
|
|
365
|
+
* evidence of anything. Re-asserting a fact the world already holds appends
|
|
366
|
+
* nothing — `reason` says which of the two happened, in the caller's own
|
|
367
|
+
* words. Pure. */
|
|
368
|
+
export function planTaughtTriple(rows, state, triple) {
|
|
369
|
+
if (!triple?.subject || !triple?.object) return { toAppend: [], reason: "nothing parsed" };
|
|
370
|
+
if (triple.kind === PLACEMENT_KIND) {
|
|
371
|
+
const current = state?.placements?.get(triple.subject);
|
|
372
|
+
if (current && current.predicate === triple.predicate && current.object === triple.object) {
|
|
373
|
+
return { toAppend: [], reason: `${triple.subject} is already ${triple.predicate} ${triple.object}` };
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
toAppend: [triple],
|
|
377
|
+
reason: current
|
|
378
|
+
? `${triple.subject} moves from ${current.object} to ${triple.object}`
|
|
379
|
+
: `${triple.subject} is placed ${triple.predicate} ${triple.object}`,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
if (triple.kind === OPENNESS_KIND) {
|
|
383
|
+
const current = state?.openness?.get(triple.subject);
|
|
384
|
+
const wantOpen = triple.object === "true";
|
|
385
|
+
if (current && current.open === wantOpen) {
|
|
386
|
+
return { toAppend: [], reason: `${triple.subject} is already ${wantOpen ? "open" : "closed"}` };
|
|
387
|
+
}
|
|
388
|
+
return { toAppend: [triple], reason: `${triple.subject} becomes ${wantOpen ? "open" : "closed"}` };
|
|
389
|
+
}
|
|
390
|
+
const key = tripleKey(triple);
|
|
391
|
+
if (editableOtherRows(rows).some((r) => tripleKey(r) === key)) {
|
|
392
|
+
return { toAppend: [], reason: `the world already says ${key}` };
|
|
393
|
+
}
|
|
394
|
+
return { toAppend: [triple], reason: `the world gains ${key}` };
|
|
395
|
+
}
|
|
396
|
+
|
|
361
397
|
// ---- cursor-driven suggestions ---------------------------------------------
|
|
362
398
|
|
|
363
399
|
export { wordBeforeCursor } from "./viz-theme.mjs";
|
|
@@ -18,6 +18,9 @@ import { compileDomain, precondHolds, roleBinding } from "../domain/domain.mjs";
|
|
|
18
18
|
import { getWorldsPackProvider } from "../adapters/corpus/worlds-pack.mjs";
|
|
19
19
|
import { appendFacts, appendRule, loadMemory, normFactTerm, readFactRows, readRuleRows } from "../adapters/memory/core.mjs";
|
|
20
20
|
import { COMPLETIONS_STORE, generateCompletion } from "./completions.mjs";
|
|
21
|
+
import { parseEditorLine, planTaughtTriple } from "./adventure-editor.mjs";
|
|
22
|
+
import { parseMudEditorLine, planTaughtMudTriple } from "./mud-editor.mjs";
|
|
23
|
+
import { worldTeachTurn } from "./world-teach.mjs";
|
|
21
24
|
|
|
22
25
|
// ---- recognizers: the closed opening/stop set --------------------------------
|
|
23
26
|
|
|
@@ -563,6 +566,20 @@ export function roomAffordances(rows, state, here, actingSubject = "player") {
|
|
|
563
566
|
|
|
564
567
|
const affordanceSuffix = (actions) => (actions.length ? ` You can: ${actions.join(", ")}.` : "");
|
|
565
568
|
|
|
569
|
+
/** The auto-relook line every state-changing turn ends on: the room read back
|
|
570
|
+
* from a FRESH load of the store, in the same shape a manual "look"
|
|
571
|
+
* produces, so nobody has to retype "look" to see what just changed. Every
|
|
572
|
+
* writer that changes the world shares this one renderer — a second copy
|
|
573
|
+
* would drift the moment the digest or the affordance list moved. */
|
|
574
|
+
export async function worldRelook(room, { memoryDir, graph = null, actingSubject = "player" }) {
|
|
575
|
+
const memory = await loadMemory(memoryDir);
|
|
576
|
+
const rows = readFactRows(memory);
|
|
577
|
+
const state = foldWorldState(worldActionRows(rows));
|
|
578
|
+
const digest = await worldDigest(room, { memoryDir, memory, rows, state, graph, actingSubject });
|
|
579
|
+
const actions = roomAffordances(rows, state, room, actingSubject);
|
|
580
|
+
return `you are in the ${room}. ${digest ?? "Nothing more about it is written down yet."}${affordanceSuffix(actions)}`;
|
|
581
|
+
}
|
|
582
|
+
|
|
566
583
|
/** The effect predicate a family writes (its action-effect row's slot, with
|
|
567
584
|
* the mgx: prefix rule readers re-attach). Null when the family carries no
|
|
568
585
|
* effect row — unlock's family stays signature-only (its instrument match
|
|
@@ -1218,7 +1235,7 @@ function freshRoomId(rows, here, direction) {
|
|
|
1218
1235
|
* whole nested dig path ("carrot-sett-1-north-east-east"), which is an id, not
|
|
1219
1236
|
* a name anyone can read. `alsoTaken` holds the ids minted earlier in this
|
|
1220
1237
|
* same dig, which are not in `rows` yet. Pure. */
|
|
1221
|
-
function freshObjectId(rows, kind, alsoTaken) {
|
|
1238
|
+
export function freshObjectId(rows, kind, alsoTaken = new Set()) {
|
|
1222
1239
|
const taken = (id) => alsoTaken.has(id) || (rows || []).some((r) => r.subject === id || r.object === id);
|
|
1223
1240
|
for (let n = 1; n <= (rows || []).length + 2; n += 1) {
|
|
1224
1241
|
if (!taken(`${kind}-${n}`)) return `${kind}-${n}`;
|
|
@@ -1237,7 +1254,7 @@ function declaredKindsOr(rows, roomClass, predicate, fallback) {
|
|
|
1237
1254
|
* nothing when the class declares no mass. eat reads the instance's mass, so a
|
|
1238
1255
|
* dug carrot with none would be worth the flat default however the world
|
|
1239
1256
|
* values a carrot. Pure. */
|
|
1240
|
-
function classMassFacts(rows, instance, kind) {
|
|
1257
|
+
export function classMassFacts(rows, instance, kind) {
|
|
1241
1258
|
const mass = factObjects(rows, kind, MASS_PREDICATE)[0];
|
|
1242
1259
|
return mass ? [{ subject: instance, predicate: MASS_PREDICATE, object: mass }] : [];
|
|
1243
1260
|
}
|
|
@@ -1911,15 +1928,7 @@ export async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache
|
|
|
1911
1928
|
const npcPass = runNpcPass({ rows, state, k, families, playerRoomAfter });
|
|
1912
1929
|
await writeWorldTurn(memoryDir, world, k, [...facts, ...npcPass.writes], cache);
|
|
1913
1930
|
const text2 = npcPass.lines.length ? `${text} ${npcPass.lines.join(" ")}` : text;
|
|
1914
|
-
|
|
1915
|
-
// manual "look" produces, read from the FRESH post-write state, so the
|
|
1916
|
-
// player is never left to retype "look" to see what just changed.
|
|
1917
|
-
const freshMemory = await loadMemory(memoryDir);
|
|
1918
|
-
const freshRows = readFactRows(freshMemory);
|
|
1919
|
-
const freshState = foldWorldState(worldActionRows(freshRows));
|
|
1920
|
-
const relookDigest = await worldDigest(playerRoomAfter, { memoryDir, memory: freshMemory, rows: freshRows, state: freshState, graph, actingSubject });
|
|
1921
|
-
const actions = roomAffordances(freshRows, freshState, playerRoomAfter, actingSubject);
|
|
1922
|
-
const relook = `you are in the ${playerRoomAfter}. ${relookDigest ?? "Nothing more about it is written down yet."}${affordanceSuffix(actions)}`;
|
|
1931
|
+
const relook = await worldRelook(playerRoomAfter, { memoryDir, graph, actingSubject });
|
|
1923
1932
|
return answer(
|
|
1924
1933
|
`${text2} ${relook}`,
|
|
1925
1934
|
noteFor(`${detail}; turn ${k} snapshots written through appendFacts${npcPass.writes.length ? `; NPC pass fired ${npcPass.writes.length} scheduled move(s)` : ""}; auto-relook appended for the ${playerRoomAfter}`),
|
|
@@ -2362,7 +2371,7 @@ async function addressedLine(line, { memoryDir }) {
|
|
|
2362
2371
|
* recognizer, injected so the two lanes can never disagree about what a plan
|
|
2363
2372
|
* frame is.
|
|
2364
2373
|
*/
|
|
2365
|
-
export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "", env, lexicon = null, graph = null, cache = null, isPlanFrameLine = () => false, discourseHolder = null, actingSubject = "player" }) {
|
|
2374
|
+
export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "", env, lexicon = null, graph = null, cache = null, isPlanFrameLine = () => false, discourseHolder = null, actingSubject = "player", gameConfig = null }) {
|
|
2366
2375
|
const slot = planHolder?.state ?? null;
|
|
2367
2376
|
const adventure = slot?.adventure ?? null;
|
|
2368
2377
|
const opening = matchAdventureOpening(line);
|
|
@@ -2418,11 +2427,11 @@ export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "
|
|
|
2418
2427
|
note: "ADVENTURE — a plan frame arrived mid-adventure; the slot holds one thing at a time",
|
|
2419
2428
|
};
|
|
2420
2429
|
}
|
|
2421
|
-
const direct = await liveWorldAnswer(line, { world: adventure.world, memoryDir, env, graph, cache, lexicon, discourseHolder, actingSubject });
|
|
2430
|
+
const direct = await liveWorldAnswer(line, { world: adventure.world, memoryDir, env, graph, cache, lexicon, discourseHolder, actingSubject, gameConfig });
|
|
2422
2431
|
if (direct) return direct;
|
|
2423
2432
|
const addressed = await addressedLine(line, { memoryDir });
|
|
2424
2433
|
if (addressed) {
|
|
2425
|
-
const readdressed = await liveWorldAnswer(addressed, { world: adventure.world, memoryDir, env, graph, cache, lexicon, discourseHolder, actingSubject });
|
|
2434
|
+
const readdressed = await liveWorldAnswer(addressed, { world: adventure.world, memoryDir, env, graph, cache, lexicon, discourseHolder, actingSubject, gameConfig });
|
|
2426
2435
|
if (readdressed) return readdressed;
|
|
2427
2436
|
}
|
|
2428
2437
|
return null; // a mid-game aside — the ordinary lanes answer, world untouched
|
|
@@ -2433,8 +2442,29 @@ export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "
|
|
|
2433
2442
|
* lets an ordinary mid-game question keep its own lane. Split out from the
|
|
2434
2443
|
* lane itself so a line carrying a vocative can be re-offered here once,
|
|
2435
2444
|
* stripped, without the two paths ever drifting apart. */
|
|
2436
|
-
async function liveWorldAnswer(line, { world, memoryDir, env, graph, cache, lexicon, discourseHolder, actingSubject }) {
|
|
2445
|
+
async function liveWorldAnswer(line, { world, memoryDir, env, graph, cache, lexicon, discourseHolder, actingSubject, gameConfig }) {
|
|
2437
2446
|
if (INVENTORY_RE.test(line)) return inventoryAnswer({ memoryDir, graph, actingSubject });
|
|
2447
|
+
// The teach switch runs BEFORE the imperative parse, and that ordering is
|
|
2448
|
+
// the point. parseImperative fuzzy-repairs a leading noun into a verb when
|
|
2449
|
+
// one is an edit away ("book" -> "look"), so a declarative sentence about
|
|
2450
|
+
// such a noun would be executed as a command and decline on its own
|
|
2451
|
+
// residue. With teach on, the sentence is read as a fact first; with teach
|
|
2452
|
+
// off, nothing here runs and the lane behaves exactly as it always has.
|
|
2453
|
+
if (gameConfig?.adventure?.teach && memoryDir) {
|
|
2454
|
+
const teachRows = readFactRows(await loadMemory(memoryDir));
|
|
2455
|
+
const taught = await worldTeachTurn(line, {
|
|
2456
|
+
// A burrow says placement with different words than a manor does, so
|
|
2457
|
+
// the sentence table follows the world, never the lane. The world's own
|
|
2458
|
+
// origin fact is the test: only a burrow measures digs from one.
|
|
2459
|
+
...(originRoomOf(teachRows)
|
|
2460
|
+
? { parseLine: parseMudEditorLine, planTriple: planTaughtMudTriple }
|
|
2461
|
+
: { parseLine: parseEditorLine, planTriple: planTaughtTriple }),
|
|
2462
|
+
rows: teachRows,
|
|
2463
|
+
state: foldWorldState(worldActionRows(teachRows)),
|
|
2464
|
+
memoryDir, world, actingSubject, cache, graph,
|
|
2465
|
+
});
|
|
2466
|
+
if (taught) return taught;
|
|
2467
|
+
}
|
|
2438
2468
|
const parsed = parseImperative(line, await worldAwareLexicon(memoryDir, lexicon));
|
|
2439
2469
|
if (parsed) {
|
|
2440
2470
|
const bound = await bindPronouns(parsed, { discourseHolder, memoryDir, actingSubject });
|
package/src/services/chat.mjs
CHANGED
|
@@ -63,6 +63,7 @@ import { subClassParents, ancestryChain, clusterSenses } from "../domain/sense-s
|
|
|
63
63
|
import { relatedForTerm } from "../domain/skos-view.mjs";
|
|
64
64
|
import { adventureTurn, unclaimedAdventureOpening, foldWorldState } from "./adventure.mjs";
|
|
65
65
|
import { spiderFlyTurn } from "./spider-fly-turn.mjs";
|
|
66
|
+
import { mudiiiTurn } from "./mudiii-turn.mjs";
|
|
66
67
|
import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
67
68
|
|
|
68
69
|
// Composition: the chat surface supplies the domain parser's default lemma/POS
|
|
@@ -15376,7 +15377,7 @@ async function dispatchTurn(input, { config, source = defaultSource, graph = nul
|
|
|
15376
15377
|
// otherwise read as a declarative or an orientation ask.
|
|
15377
15378
|
{
|
|
15378
15379
|
const advTurn = await adventureTurn(workingLine, {
|
|
15379
|
-
planHolder, memoryDir, sessionId, env, lexicon, graph, cache: factRowsCache, isPlanFrameLine, discourseHolder, actingSubject,
|
|
15380
|
+
planHolder, memoryDir, sessionId, env, lexicon, graph, cache: factRowsCache, isPlanFrameLine, discourseHolder, actingSubject, gameConfig,
|
|
15380
15381
|
});
|
|
15381
15382
|
if (advTurn) {
|
|
15382
15383
|
note(trace, `lane: ${advTurn.note}`);
|
|
@@ -15411,6 +15412,29 @@ async function dispatchTurn(input, { config, source = defaultSource, graph = nul
|
|
|
15411
15412
|
}
|
|
15412
15413
|
}
|
|
15413
15414
|
|
|
15415
|
+
// MUDIII — the town-square openers (one per shipped layout), stop, the
|
|
15416
|
+
// addressed teach-frame for agents and food alike, the bare tick, the
|
|
15417
|
+
// food-placement verb, and a live game's own turns. It sits AFTER spider-fly
|
|
15418
|
+
// and BEFORE the unclaimed-opener decline below on purpose: this lane's
|
|
15419
|
+
// opener vocabulary never includes the bare word "mudiii", so "play mudiii"
|
|
15420
|
+
// falls through to that decline and gets named honestly rather than being
|
|
15421
|
+
// silently absorbed here.
|
|
15422
|
+
{
|
|
15423
|
+
const mTurn = await mudiiiTurn(workingLine, {
|
|
15424
|
+
planHolder, memoryDir, env, cache: factRowsCache, isPlanFrameLine, gameConfig: resolvedGameConfig,
|
|
15425
|
+
});
|
|
15426
|
+
if (mTurn) {
|
|
15427
|
+
note(trace, `lane: ${mTurn.note}`);
|
|
15428
|
+
if (mTurn.goal) note(trace, `goal: ${mTurn.goal}`);
|
|
15429
|
+
const result = plainTurn(workingLine, mTurn.text, { via: "game", miss: !!mTurn.miss, focus });
|
|
15430
|
+
if (mTurn.goal) result.goal = mTurn.goal;
|
|
15431
|
+
result.lane = mTurn.lane;
|
|
15432
|
+
const rec = withLast(result, mTurn.goal ?? "watch the town square");
|
|
15433
|
+
rec.planState = planHolder.state;
|
|
15434
|
+
return rec;
|
|
15435
|
+
}
|
|
15436
|
+
}
|
|
15437
|
+
|
|
15414
15438
|
// A "play X" naming no world EITHER game lane above claimed — last resort,
|
|
15415
15439
|
// checked only once the adventure lane's own fallthrough and spider-fly's
|
|
15416
15440
|
// own opener have both passed on the line, so this never outguesses a
|