@polycode-projects/the-mechanical-code-talker 4.0.0 → 4.1.0
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 +178 -9
- package/corpus/reference/index.json.gz +0 -0
- package/corpus/reference/manifest.json +8 -8
- package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
- package/corpus/sprites/src/sprite-facts.jsonl +401 -0
- package/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +49 -9
- package/corpus/worlds/shards/greyvale-museum.jsonl.gz +0 -0
- package/corpus/worlds/shards/lantern-cottage.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
- package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
- package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
- package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
- package/corpus/worlds/src/mud-hollow.jsonl +82 -0
- package/corpus/worlds/src/mud-warren.jsonl +124 -0
- package/corpus/worlds/src/spider-fly.jsonl +1 -1
- package/data/sprites/animal-icon.toml +11 -5
- package/data/sprites/book-icon.toml +9 -5
- package/data/sprites/cabinet-icon.toml +10 -5
- package/data/sprites/cellar-icon.toml +16 -6
- package/data/sprites/container-icon.toml +7 -3
- package/data/sprites/desk-icon.toml +8 -5
- package/data/sprites/dog-icon.toml +8 -5
- package/data/sprites/dog-with-colour-icon.toml +13 -12
- package/data/sprites/drawing-room-icon.toml +14 -5
- package/data/sprites/egg-icon.toml +6 -3
- package/data/sprites/fly-icon.toml +10 -5
- package/data/sprites/furniture-icon.toml +5 -3
- package/data/sprites/garden-icon.toml +10 -3
- package/data/sprites/key-icon.toml +3 -1
- package/data/sprites/kitchen-icon.toml +15 -6
- package/data/sprites/lamp-icon.toml +9 -4
- package/data/sprites/letter-icon.toml +7 -4
- package/data/sprites/library-icon.toml +14 -3
- package/data/sprites/pan-icon.toml +7 -3
- package/data/sprites/person-icon.toml +6 -2
- package/data/sprites/poodle-icon.toml +1 -1
- package/data/sprites/portable-icon.toml +6 -4
- package/data/sprites/portrait-icon.toml +7 -4
- package/data/sprites/room-icon.toml +11 -2
- package/data/sprites/spider-icon.toml +9 -3
- package/data/sprites/study-icon.toml +10 -2
- package/package.json +5 -4
- package/src/adapters/memory/core.mjs +20 -0
- package/src/domain/ask-vocab.mjs +71 -0
- package/src/domain/ask.mjs +168 -0
- package/src/domain/game-config.mjs +11 -0
- package/src/domain/grammar/ace.mjs +40 -4
- package/src/domain/grammar/lexicon-core.json +2 -1
- package/src/domain/grammar/lexicon.mjs +18 -0
- package/src/domain/mud-facts.mjs +15 -0
- package/src/domain/reference-pack.mjs +31 -7
- package/src/domain/router/drive.mjs +35 -9
- package/src/domain/router/registry.mjs +24 -4
- package/src/domain/router/resolver.mjs +102 -40
- package/src/domain/scene-compose.mjs +117 -0
- package/src/domain/spider-fly-world.mjs +37 -1
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +169 -20
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +209 -157
- package/src/services/adventure.mjs +526 -391
- package/src/services/chat-page-viz.mjs +69 -25
- package/src/services/chat.mjs +200 -51
- package/src/services/code-explorer-viz.mjs +102 -62
- package/src/services/extract-facts.mjs +4 -7
- package/src/services/ingest-viz.mjs +68 -82
- package/src/services/ledger-viz.mjs +136 -67
- package/src/services/memory-panel-viz.mjs +62 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +1016 -208
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +123 -64
- package/src/services/research-viz.mjs +160 -108
- package/src/services/spider-fly-turn.mjs +15 -23
- package/src/services/spider-fly-viz.mjs +146 -161
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +414 -240
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +90 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +49 -33
- package/src/surfaces/web/chat-browser-entry.mjs +30 -105
- package/src/surfaces/web/code-explorer-browser-entry.mjs +168 -24
- package/src/surfaces/web/ingest-browser-entry.mjs +3 -13
- package/src/surfaces/web/ledger-browser-entry.mjs +7 -47
- package/src/surfaces/web/memory-ask-browser.bundle.js +127 -124
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +71 -29
- package/src/surfaces/web/plan-browser-entry.mjs +22 -40
- package/src/surfaces/web/research-browser-entry.mjs +26 -41
- package/src/surfaces/web/spider-fly-browser-entry.mjs +45 -28
- package/src/surfaces/web/sprites-browser-entry.mjs +14 -27
- package/src/surfaces/web/turn-session.mjs +120 -0
- package/src/tools/definitions.mjs +30 -0
- package/src/tools/handlers/index.mjs +6 -3
- package/src/tools/handlers/kit.mjs +19 -2
- package/src/tools/handlers/tmct-ask.mjs +11 -6
- package/src/tools/handlers/tmct-ingest.mjs +5 -1
- package/src/tools/handlers/tmct-related.mjs +4 -4
- package/src/tools/handlers/tmct-sprite.mjs +147 -0
- package/src/tools/memory-fallthrough.mjs +9 -2
- package/src/tools/server.mjs +25 -1
|
@@ -174,7 +174,43 @@ async function resumedPosition(memoryDir) {
|
|
|
174
174
|
|
|
175
175
|
// ---- the world-state fold ----------------------------------------------------
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
// A snapshot subject is `base@turnN`, or `base@epochE@turnN` once a recast has
|
|
178
|
+
// moved the world past epoch 0 — the fold below reads both, and epoch-0 writes
|
|
179
|
+
// keep the bare form so an unrecast store never changes shape.
|
|
180
|
+
const SNAPSHOT_RE = /^(.+?)@(?:epoch(\d+)@)?turn(\d+)$/;
|
|
181
|
+
|
|
182
|
+
/** Which run of the world a store is on. Recasting a shared world (mud.html's
|
|
183
|
+
* RESET, a slider, the scenario dropdown) reopens the same deterministic
|
|
184
|
+
* instance ids over a fresh store while peers may still hold the old run's
|
|
185
|
+
* snapshots — this marker is how every reader agrees the world has started
|
|
186
|
+
* over. It is an ordinary add-only fact: each recast appends a larger value,
|
|
187
|
+
* the fold takes the max, and merging two peers' stores converges because max
|
|
188
|
+
* is order-free. */
|
|
189
|
+
export const WORLD_EPOCH_PREDICATE = "mgx:world-epoch";
|
|
190
|
+
const WORLD_EPOCH_SUBJECT = "world";
|
|
191
|
+
|
|
192
|
+
/** The bare triple a recast writes to move the world onto `epoch`. The caller
|
|
193
|
+
* supplies provenance the same way it does for the seed facts, so the marker
|
|
194
|
+
* travels (and folds) as a world row. */
|
|
195
|
+
export function worldEpochFact(epoch) {
|
|
196
|
+
return { subject: WORLD_EPOCH_SUBJECT, predicate: WORLD_EPOCH_PREDICATE, object: String(epoch) };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** `{ base, epoch, turn }` for a snapshot subject, or null for a base subject.
|
|
200
|
+
* The one parser every reader outside this file should use — a local
|
|
201
|
+
* `@turn(\d+)$` regex reads an epoch-stamped subject's base as
|
|
202
|
+
* "mole-1@epoch2", which matches no character. */
|
|
203
|
+
export function parseSnapshotSubject(subject) {
|
|
204
|
+
const m = SNAPSHOT_RE.exec(String(subject || ""));
|
|
205
|
+
return m ? { base: m[1], epoch: m[2] ? Number(m[2]) : 0, turn: Number(m[3]) } : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The snapshot subject a turn writes: bare `@turnN` while the world is on
|
|
209
|
+
* epoch 0 (every store that has never been recast, and every fact written
|
|
210
|
+
* before epochs existed), the epoch-stamped form after. */
|
|
211
|
+
export function snapshotSubject(base, turn, epoch = 0) {
|
|
212
|
+
return epoch > 0 ? `${base}@epoch${epoch}@turn${turn}` : `${base}@turn${turn}`;
|
|
213
|
+
}
|
|
178
214
|
const PLACEMENT_PREDICATES = new Set([
|
|
179
215
|
"mgx:currently-in", "mgx:located-in", "mgx:fixed-in", "mgx:stands-locked-in", "mgx:hidden-in",
|
|
180
216
|
]);
|
|
@@ -227,6 +263,36 @@ export function worldActionRows(rows) {
|
|
|
227
263
|
});
|
|
228
264
|
}
|
|
229
265
|
|
|
266
|
+
// The predicates a played turn can write. Every one of them already exists
|
|
267
|
+
// above as a private constant; this set is the one place they are gathered so
|
|
268
|
+
// a caller outside this file can ask "is this fact part of the live world"
|
|
269
|
+
// without learning each name. rdf:type and the display name earn their place
|
|
270
|
+
// alongside the folded ones: a dug room and a dug object are minted mid-play,
|
|
271
|
+
// and a reader that never receives their class or their plain name cannot draw
|
|
272
|
+
// them at all.
|
|
273
|
+
const MUD_STATE_PREDICATES = new Set([
|
|
274
|
+
...PLACEMENT_PREDICATES,
|
|
275
|
+
...POSITION_PREDICATES,
|
|
276
|
+
OPEN_PREDICATE,
|
|
277
|
+
MASS_PREDICATE,
|
|
278
|
+
KNOWS_ABOUT_PREDICATE,
|
|
279
|
+
DISPLAY_NAME_PREDICATE,
|
|
280
|
+
"rdf:type",
|
|
281
|
+
// Which run the world is on IS live world state: a peer that misses the
|
|
282
|
+
// recast marker keeps folding the old run's snapshots as current.
|
|
283
|
+
WORLD_EPOCH_PREDICATE,
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
/** Whether `predicate` carries live world state — where a thing stands, what
|
|
287
|
+
* it weighs, whether it is open, what somebody knows, which way a room leads,
|
|
288
|
+
* and the class and name a dug thing is minted with. The P2P sync filter
|
|
289
|
+
* reads this to tell a fact a turn produced from the page chrome around it.
|
|
290
|
+
* Pure. */
|
|
291
|
+
export function isMudStatePredicate(predicate) {
|
|
292
|
+
const p = String(predicate || "");
|
|
293
|
+
return MUD_STATE_PREDICATES.has(p) || EXIT_PREDICATE_RE.test(p);
|
|
294
|
+
}
|
|
295
|
+
|
|
230
296
|
/** Every individual the world names — its rooms, its cast, its props, and
|
|
231
297
|
* anything dug up since — as the plain id strings a parser has to have
|
|
232
298
|
* DECLARED before it can resolve them. @turnN snapshots are skipped: a
|
|
@@ -242,41 +308,61 @@ export function worldIndividualNames(rows) {
|
|
|
242
308
|
}
|
|
243
309
|
|
|
244
310
|
/** Fold fact rows into the CURRENT world state: per subject, the newest
|
|
245
|
-
* placement (base row = turn 0,
|
|
246
|
-
* open/closed state, the newest mass, the exit map,
|
|
247
|
-
* largest
|
|
311
|
+
* placement (base row = turn 0 of the current epoch, snapshots override), the
|
|
312
|
+
* newest open/closed state, the newest mass, the exit map, the turn counter
|
|
313
|
+
* (the largest snapshot turn written in the current epoch — derived, never
|
|
314
|
+
* stored), and the epoch itself.
|
|
315
|
+
*
|
|
316
|
+
* Rows rank by the (epoch, turn) pair, so a recast can never be outranked by
|
|
317
|
+
* the run it replaced: a peer's turn-9 snapshot from before the recast loses
|
|
318
|
+
* to a turn-1 snapshot written after it. Base rows carry no stamp of their
|
|
319
|
+
* own and rank as turn 0 of the CURRENT epoch — a recast re-seeds the same
|
|
320
|
+
* deterministic ids, so the shard's own rows are exactly the state the new
|
|
321
|
+
* run starts from. A store with no epoch marker and no stamped snapshot is
|
|
322
|
+
* wholly on epoch 0 and folds as it always has. Pure. */
|
|
248
323
|
export function foldWorldState(factRows) {
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
324
|
+
const rows = factRows || [];
|
|
325
|
+
let epoch = 0;
|
|
326
|
+
for (const row of rows) {
|
|
327
|
+
if (row.predicate === WORLD_EPOCH_PREDICATE) {
|
|
328
|
+
const marked = Number(row.object);
|
|
329
|
+
if (Number.isInteger(marked) && marked > epoch) epoch = marked;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const m = SNAPSHOT_RE.exec(row.subject);
|
|
333
|
+
if (m && m[2] && Number(m[2]) > epoch) epoch = Number(m[2]);
|
|
334
|
+
}
|
|
335
|
+
const placements = new Map(); // subject -> { predicate, object, turn, epoch }
|
|
336
|
+
const positions = new Map(); // subject -> { predicate, object, turn, epoch }
|
|
337
|
+
const openness = new Map(); // subject -> { open, turn, epoch }
|
|
338
|
+
const masses = new Map(); // subject -> { value, turn, epoch }
|
|
253
339
|
const exits = new Map(); // room -> Map(direction -> room)
|
|
254
340
|
let turnCount = 0;
|
|
255
|
-
|
|
341
|
+
const outranks = (rowEpoch, turn, prior) =>
|
|
342
|
+
!prior || rowEpoch > prior.epoch || (rowEpoch === prior.epoch && turn >= prior.turn);
|
|
343
|
+
for (const row of rows) {
|
|
344
|
+
if (row.predicate === WORLD_EPOCH_PREDICATE) continue;
|
|
256
345
|
const m = SNAPSHOT_RE.exec(row.subject);
|
|
257
346
|
const base = m ? m[1] : row.subject;
|
|
258
|
-
const
|
|
259
|
-
|
|
347
|
+
const rowEpoch = m ? (m[2] ? Number(m[2]) : 0) : epoch;
|
|
348
|
+
const turn = m ? Number(m[3]) : 0;
|
|
349
|
+
if (m && rowEpoch === epoch) turnCount = Math.max(turnCount, turn);
|
|
260
350
|
if (PLACEMENT_PREDICATES.has(row.predicate)) {
|
|
261
|
-
|
|
262
|
-
if (!prior || turn >= prior.turn) placements.set(base, { predicate: row.predicate, object: row.object, turn });
|
|
351
|
+
if (outranks(rowEpoch, turn, placements.get(base))) placements.set(base, { predicate: row.predicate, object: row.object, turn, epoch: rowEpoch });
|
|
263
352
|
continue;
|
|
264
353
|
}
|
|
265
354
|
if (POSITION_PREDICATES.has(row.predicate)) {
|
|
266
|
-
|
|
267
|
-
if (!prior || turn >= prior.turn) positions.set(base, { predicate: row.predicate, object: row.object, turn });
|
|
355
|
+
if (outranks(rowEpoch, turn, positions.get(base))) positions.set(base, { predicate: row.predicate, object: row.object, turn, epoch: rowEpoch });
|
|
268
356
|
continue;
|
|
269
357
|
}
|
|
270
358
|
if (row.predicate === OPEN_PREDICATE) {
|
|
271
|
-
|
|
272
|
-
if (!prior || turn >= prior.turn) openness.set(base, { open: row.object === "true", turn });
|
|
359
|
+
if (outranks(rowEpoch, turn, openness.get(base))) openness.set(base, { open: row.object === "true", turn, epoch: rowEpoch });
|
|
273
360
|
continue;
|
|
274
361
|
}
|
|
275
362
|
if (row.predicate === MASS_PREDICATE) {
|
|
276
363
|
const value = Number(row.object);
|
|
277
364
|
if (!Number.isFinite(value)) continue; // masses hold numbers; an unparsable one is no mass at all
|
|
278
|
-
|
|
279
|
-
if (!prior || turn >= prior.turn) masses.set(base, { value, turn });
|
|
365
|
+
if (outranks(rowEpoch, turn, masses.get(base))) masses.set(base, { value, turn, epoch: rowEpoch });
|
|
280
366
|
continue;
|
|
281
367
|
}
|
|
282
368
|
const exit = EXIT_PREDICATE_RE.exec(row.predicate);
|
|
@@ -285,7 +371,7 @@ export function foldWorldState(factRows) {
|
|
|
285
371
|
exits.get(row.subject).set(exit[1], row.object);
|
|
286
372
|
}
|
|
287
373
|
}
|
|
288
|
-
return { placements, positions, openness, masses, exits, turnCount };
|
|
374
|
+
return { placements, positions, openness, masses, exits, turnCount, epoch };
|
|
289
375
|
}
|
|
290
376
|
|
|
291
377
|
/** A subject's CURRENT within-room position, or null. A position goes stale
|
|
@@ -296,7 +382,7 @@ export function currentPosition(state, subject) {
|
|
|
296
382
|
const pos = state.positions.get(subject);
|
|
297
383
|
if (!pos) return null;
|
|
298
384
|
const place = state.placements.get(subject);
|
|
299
|
-
if (place && pos.turn < place.turn) return null;
|
|
385
|
+
if (place && (pos.epoch < place.epoch || (pos.epoch === place.epoch && pos.turn < place.turn))) return null;
|
|
300
386
|
return pos;
|
|
301
387
|
}
|
|
302
388
|
|
|
@@ -524,7 +610,7 @@ export function runNpcPass({ rows, state, k, families, playerRoomAfter }) {
|
|
|
524
610
|
if (!covered) continue;
|
|
525
611
|
const linked = [...(state.exits.get(from)?.values() ?? [])].includes(target);
|
|
526
612
|
if (!linked) continue;
|
|
527
|
-
writes.push({ subject:
|
|
613
|
+
writes.push({ subject: snapshotSubject(npc, k, state.epoch), predicate: effectPredicate, object: target });
|
|
528
614
|
if (playerRoomAfter === target) lines.push(`the ${npc} walks in.`);
|
|
529
615
|
else if (playerRoomAfter === from) lines.push(`the ${npc} leaves.`);
|
|
530
616
|
}
|
|
@@ -677,8 +763,8 @@ export async function recordMassDrain(memoryDir, { world, subject, drainPerTurn,
|
|
|
677
763
|
const left = Math.max(0, Math.round((mass - drainPerTurn) * 100) / 100);
|
|
678
764
|
const k = state.turnCount + 1;
|
|
679
765
|
await writeWorldTurn(memoryDir, world, k, [
|
|
680
|
-
{ subject:
|
|
681
|
-
...(left > 0 ? [] : [{ subject:
|
|
766
|
+
{ subject: snapshotSubject(subject, k, state.epoch), predicate: MASS_PREDICATE, object: String(left) },
|
|
767
|
+
...(left > 0 ? [] : [{ subject: snapshotSubject(subject, k, state.epoch), predicate: "mgx:currently-in", object: STARVED_PLACE }]),
|
|
682
768
|
], cache);
|
|
683
769
|
return { mass: left, starved: left <= 0 };
|
|
684
770
|
}
|
|
@@ -697,6 +783,9 @@ export async function recordMassDrain(memoryDir, { world, subject, drainPerTurn,
|
|
|
697
783
|
// schedule) stay out of the view: hidden means hidden.
|
|
698
784
|
|
|
699
785
|
const VIEW_EXCLUDED_PREDICATES = new Set([
|
|
786
|
+
// The recast counter is bookkeeping, not scenery — "World mgx:world-epoch 2"
|
|
787
|
+
// must never read back as room prose.
|
|
788
|
+
WORLD_EPOCH_PREDICATE,
|
|
700
789
|
"mgx:hidden-in", "mgx:is-open", "mgx:is-npc", "mgx:is-container",
|
|
701
790
|
// A bare number reads as an untranslated triple in room prose ("Mole-1
|
|
702
791
|
// mgx:hasMass 8"). Mass reaches a player through the verbs that change it.
|
|
@@ -1193,7 +1282,7 @@ function containerDatatypeState(state, object) {
|
|
|
1193
1282
|
export function personKnowledgeLines(rows, state, person) {
|
|
1194
1283
|
const lines = [];
|
|
1195
1284
|
for (const objective of factObjects(rows, person, "mgx:knows-objective")) {
|
|
1196
|
-
lines.push(`the ${objective} is what you're after — find it and carry it
|
|
1285
|
+
lines.push(`the ${objective} is what you're after — find it and carry it, and the adventure is won.`);
|
|
1197
1286
|
}
|
|
1198
1287
|
for (const thing of factObjects(rows, person, "mgx:knows-where")) {
|
|
1199
1288
|
const place = state.placements.get(thing);
|
|
@@ -1239,6 +1328,410 @@ export function personRoomReport(rows, state, person, actingSubject = "player")
|
|
|
1239
1328
|
return parts.join(" ");
|
|
1240
1329
|
}
|
|
1241
1330
|
|
|
1331
|
+
// ---- state-changing verb handlers --------------------------------------------
|
|
1332
|
+
//
|
|
1333
|
+
// One function per state-changing verb (or per shared-precondition group of
|
|
1334
|
+
// verbs), keyed by name in STATE_VERB_HANDLERS below rather than chained as
|
|
1335
|
+
// if/else-if branches: adding a verb means adding a table entry, never
|
|
1336
|
+
// widening an existing conditional. Each handler takes the one `ctx` object
|
|
1337
|
+
// runWorldCommand builds once per call — cmd, the current fold, the taught
|
|
1338
|
+
// action family, and the shared `commit`/`noteFor` closures — so a handler
|
|
1339
|
+
// reads exactly like the branch it replaces, just lifted to its own name.
|
|
1340
|
+
|
|
1341
|
+
async function handleGoVerb(ctx) {
|
|
1342
|
+
const { cmd, rows, state, here, k, family, commit, noteFor, memoryDir, world, cache, actingSubject } = ctx;
|
|
1343
|
+
const target = state.exits.get(here)?.get(cmd.direction);
|
|
1344
|
+
if (!target) {
|
|
1345
|
+
return answer(
|
|
1346
|
+
`there's no exit ${cmd.direction} from the ${here}.`,
|
|
1347
|
+
noteFor(`go — no mgx:has-exit-${cmd.direction} fact on ${here}; precondition declined by name`),
|
|
1348
|
+
{ miss: true },
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
// A predator eats whatever walks in, and the room it guards is the one
|
|
1352
|
+
// room a move never comes back from — so this write bypasses commit()
|
|
1353
|
+
// entirely: the auto-relook there would describe a room the mover is no
|
|
1354
|
+
// longer standing in, and the world has nobody left to look with.
|
|
1355
|
+
const predator = predatorIn(rows, state, target);
|
|
1356
|
+
if (predator) {
|
|
1357
|
+
await writeWorldTurn(memoryDir, world, k, [
|
|
1358
|
+
{ subject: snapshotSubject(actingSubject, k, state.epoch), predicate: "mgx:currently-in", object: CONSUMED_PLACE },
|
|
1359
|
+
], cache);
|
|
1360
|
+
return answer(
|
|
1361
|
+
`you go ${cmd.direction} into the ${target} — and the ${predator} is waiting. It eats the ${actingSubject}. That's the end of its run.`,
|
|
1362
|
+
noteFor(`go — the ${target} holds the predator ${predator}; ${actingSubject} is placed out of play at turn ${k} and takes no further turns`),
|
|
1363
|
+
{ goal: `move through the world (eaten by the ${predator} in the ${target})` },
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
return commit(
|
|
1367
|
+
[{ subject: snapshotSubject(actingSubject, k, state.epoch), predicate: familyEffectPredicate(family) ?? "mgx:currently-in", object: target }],
|
|
1368
|
+
`you go ${cmd.direction}. Now in the ${target}.`,
|
|
1369
|
+
`go — the taught "go" family fired; ${actingSubject} moves ${here} -> ${target}`,
|
|
1370
|
+
`move through the world (now in the ${target})`,
|
|
1371
|
+
target,
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
async function handleTakeVerb(ctx) {
|
|
1376
|
+
const { rows, state, here, k, family, object, place, commit, noteFor, actingSubject } = ctx;
|
|
1377
|
+
if (isTyped(rows, object, "room")) {
|
|
1378
|
+
return answer(`you can't take the ${object} — it's a whole room.`, noteFor("take — the object is a room; declined"), { miss: true });
|
|
1379
|
+
}
|
|
1380
|
+
if (carriedBy(state, object, actingSubject)) {
|
|
1381
|
+
return answer(`you're already carrying the ${object}.`, noteFor("take — already carried; declined"), { miss: true });
|
|
1382
|
+
}
|
|
1383
|
+
if (place && (place.predicate === "mgx:fixed-in" || place.predicate === "mgx:stands-locked-in") && place.object === here) {
|
|
1384
|
+
return answer(
|
|
1385
|
+
`the ${object} is fixed in place — it can't be taken.`,
|
|
1386
|
+
noteFor(`take — ${object} is placed by ${place.predicate}, not portable; precondition declined by name`),
|
|
1387
|
+
{ miss: true },
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
if (place && place.predicate === "mgx:currently-in" && place.object === here) {
|
|
1391
|
+
return answer(`you can't take the ${object}.`, noteFor("take — the object is one of the cast; declined"), { miss: true });
|
|
1392
|
+
}
|
|
1393
|
+
if (visibleRoomOf(object, { rows, state }) !== here) {
|
|
1394
|
+
if (backgroundOnlyMention(rows, state, object)) {
|
|
1395
|
+
return answer(
|
|
1396
|
+
`the ${object} isn't something you can take — it's only mentioned in passing here, not a real prop in this scene.`,
|
|
1397
|
+
noteFor(`take — ${object} is a background-only mention, never a placed object; declined honestly`),
|
|
1398
|
+
{ miss: true },
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
return answer(`I don't see a ${object} here.`, noteFor(`take — ${object} isn't visible in the ${here}; declined, hidden things stay hidden`), { miss: true });
|
|
1402
|
+
}
|
|
1403
|
+
return commit(
|
|
1404
|
+
[{ subject: snapshotSubject(object, k, state.epoch), predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: actingSubject }],
|
|
1405
|
+
`you take the ${object}.`,
|
|
1406
|
+
`take — the taught "take" family fired; ${object} is now carried`,
|
|
1407
|
+
`carry the ${object}`,
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
async function handleDropOrGiveVerb(ctx) {
|
|
1412
|
+
const { cmd, rows, state, here, k, family, object, commit, noteFor, actingSubject } = ctx;
|
|
1413
|
+
if (!carriedBy(state, object, actingSubject)) {
|
|
1414
|
+
return answer(`you're not carrying the ${object}.`, noteFor(`${cmd.verb} — ${object} isn't carried; precondition declined by name`), { miss: true });
|
|
1415
|
+
}
|
|
1416
|
+
if (cmd.verb === "drop") {
|
|
1417
|
+
return commit(
|
|
1418
|
+
[{ subject: snapshotSubject(object, k, state.epoch), predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: here }],
|
|
1419
|
+
`you drop the ${object} in the ${here}.`,
|
|
1420
|
+
`drop — the taught "drop" family fired; ${object} rests in the ${here}`,
|
|
1421
|
+
`set the ${object} down`,
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
const receiver = cmd.indirectObject;
|
|
1425
|
+
if (!isCastMember(rows, state, receiver) || state.placements.get(receiver)?.object !== here) {
|
|
1426
|
+
return answer(`the ${receiver} isn't here.`, noteFor(`give — ${receiver} isn't one of the cast standing in the ${here}; precondition declined by name`), { miss: true });
|
|
1427
|
+
}
|
|
1428
|
+
return commit(
|
|
1429
|
+
[{ subject: snapshotSubject(object, k, state.epoch), predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: receiver }],
|
|
1430
|
+
`you give the ${object} to the ${receiver}.`,
|
|
1431
|
+
`give — the taught "give" family fired; the ${receiver} holds the ${object}`,
|
|
1432
|
+
`hand the ${object} over`,
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
async function handleDigVerb(ctx) {
|
|
1437
|
+
const { cmd, rows, state, here, k, commit, noteFor } = ctx;
|
|
1438
|
+
const direction = cmd.direction;
|
|
1439
|
+
if (state.exits.get(here)?.get(direction)) {
|
|
1440
|
+
return answer(
|
|
1441
|
+
`there's already an exit ${direction} from the ${here}.`,
|
|
1442
|
+
noteFor(`dig — an mgx:has-exit-${direction} fact already stands on ${here}; declined, a dig never overwrites an exit`),
|
|
1443
|
+
{ miss: true },
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
const back = OPPOSITE_DIRECTION.get(direction);
|
|
1447
|
+
if (!back) {
|
|
1448
|
+
return answer(
|
|
1449
|
+
`I don't know which way back a ${direction} tunnel would run.`,
|
|
1450
|
+
noteFor(`dig — "${direction}" has no opposite to write the return exit with; declined by name`),
|
|
1451
|
+
{ miss: true },
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
const roomKind = roomKindOf(rows, here);
|
|
1455
|
+
const dugKind = (DIGGABLE_BY_ROOM_KIND.get(roomKind) ?? new Map()).get(direction) ?? null;
|
|
1456
|
+
if (!dugKind) {
|
|
1457
|
+
return answer(
|
|
1458
|
+
DIG_DECLINE_BY_ROOM_KIND[roomKind](here, direction),
|
|
1459
|
+
noteFor(`dig — the ${here} is an ${roomKind} room, which cannot be dug ${direction}; declined by the room's own kind`),
|
|
1460
|
+
{ miss: true },
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
if (atDigBoundary(rows, state, here)) {
|
|
1464
|
+
const reach = roomDistanceFromOrigin(rows, state, here);
|
|
1465
|
+
return answer(
|
|
1466
|
+
`the earth ${direction} of the ${here} is packed hard and endless — you have reached the far edge of the burrow.`,
|
|
1467
|
+
noteFor(reach === null
|
|
1468
|
+
? `dig — no chain of exits joins the ${here} to the ${originRoomOf(rows)}, so there is no distance to measure a dig against; declined`
|
|
1469
|
+
: `dig — the ${here} stands ${reach} rooms from the ${originRoomOf(rows)}, and this world digs ${digReachOf(rows)}; declined by distance from the origin`),
|
|
1470
|
+
{ miss: true },
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
const dug = freshRoomId(rows, here, direction);
|
|
1474
|
+
const denChanceIn = declaredCountOr(rows, dugKind, DEN_CHANCE_PREDICATE, DEFAULT_DEN_CHANCE_IN);
|
|
1475
|
+
const isDen = dugKind === "underground-space" && stableIndex(`den:${dug}`, denChanceIn) === 0;
|
|
1476
|
+
const spawnMax = declaredCountOr(rows, dugKind, DIG_SPAWN_MAX_PREDICATE, DEFAULT_DIG_SPAWN_MAX);
|
|
1477
|
+
const spawnCount = DIG_SPAWN_MIN + stableIndex(dug, spawnMax - DIG_SPAWN_MIN + 1);
|
|
1478
|
+
const spawnedKinds = isDen
|
|
1479
|
+
? declaredKindsOr(rows, dugKind, DEN_SPAWN_PREDICATE, DIG_SPAWN_KINDS)
|
|
1480
|
+
: declaredKindsOr(rows, dugKind, DIG_SPAWN_PREDICATE, DIG_SPAWN_KINDS).slice(0, spawnCount);
|
|
1481
|
+
const minted = new Set();
|
|
1482
|
+
const spawned = spawnedKinds.map((kind) => {
|
|
1483
|
+
const id = freshObjectId(rows, kind, minted);
|
|
1484
|
+
minted.add(id);
|
|
1485
|
+
return id;
|
|
1486
|
+
});
|
|
1487
|
+
const residentChanceIn = declaredCountOr(rows, dugKind, DEN_RESIDENT_CHANCE_PREDICATE, DEFAULT_DEN_RESIDENT_CHANCE_IN);
|
|
1488
|
+
const residentKind = isDen && stableIndex(`resident:${dug}`, residentChanceIn) === 0
|
|
1489
|
+
? factObjects(rows, dugKind, DEN_RESIDENT_PREDICATE)[0] ?? null
|
|
1490
|
+
: null;
|
|
1491
|
+
const resident = residentKind ? freshObjectId(rows, residentKind, minted) : null;
|
|
1492
|
+
return commit(
|
|
1493
|
+
[
|
|
1494
|
+
{ subject: dug, predicate: "rdf:type", object: "room" },
|
|
1495
|
+
{ subject: dug, predicate: "rdf:type", object: dugKind },
|
|
1496
|
+
...(isDen ? [{ subject: dug, predicate: "rdf:type", object: DEN_ROOM_CLASS }] : []),
|
|
1497
|
+
{ subject: here, predicate: `mgx:has-exit-${direction}`, object: dug },
|
|
1498
|
+
{ subject: dug, predicate: `mgx:has-exit-${back}`, object: here },
|
|
1499
|
+
// Typed to its OWN kind, not a flat "portable" — a spawned kind the
|
|
1500
|
+
// world declares rdfs:subClassOf food needs its real class reachable
|
|
1501
|
+
// here for isFood's own objectClassChain walk, or digging up "carrot-1"
|
|
1502
|
+
// would still read as inedible scenery. The class's own mass copies
|
|
1503
|
+
// onto the instance for the same reason: eat reads the instance.
|
|
1504
|
+
...spawnedKinds.flatMap((kind, i) => ([
|
|
1505
|
+
{ subject: spawned[i], predicate: "rdf:type", object: kind },
|
|
1506
|
+
{ subject: spawned[i], predicate: DISPLAY_NAME_PREDICATE, object: kind },
|
|
1507
|
+
{ subject: spawned[i], predicate: "mgx:located-in", object: dug },
|
|
1508
|
+
...classMassFacts(rows, spawned[i], kind),
|
|
1509
|
+
])),
|
|
1510
|
+
// A resident is placed with currently-in, the predicate that makes an
|
|
1511
|
+
// individual one of the cast, and knows about what its own den holds —
|
|
1512
|
+
// so an animal that digs one out has somebody new to ask about food.
|
|
1513
|
+
...(resident ? [
|
|
1514
|
+
{ subject: resident, predicate: "rdf:type", object: residentKind },
|
|
1515
|
+
{ subject: resident, predicate: DISPLAY_NAME_PREDICATE, object: residentKind },
|
|
1516
|
+
{ subject: resident, predicate: "mgx:currently-in", object: dug },
|
|
1517
|
+
...classMassFacts(rows, resident, residentKind),
|
|
1518
|
+
...spawned.map((thing) => ({ subject: resident, predicate: KNOWS_ABOUT_PREDICATE, object: thing })),
|
|
1519
|
+
] : []),
|
|
1520
|
+
],
|
|
1521
|
+
digNarration(direction, { isDen, spawned, resident }),
|
|
1522
|
+
`dig — minted the ${isDen ? `${DEN_ROOM_CLASS} ` : ""}${dugKind} ${dug} with exits both ways (${direction} out, ${back} back)${spawned.length ? `, and ${spawned.length} object(s) in it` : ""}${resident ? `, lived in by ${resident}` : ""}; digging spends the turn, so the digger stays in the ${here}`,
|
|
1523
|
+
`dig ${direction} out of the ${here}`,
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
async function handleEatVerb(ctx) {
|
|
1528
|
+
const { rows, state, here, k, object, commit, noteFor, memoryDir, cache, actingSubject } = ctx;
|
|
1529
|
+
const present = visibleRoomOf(object, { rows, state }) === here || carriedBy(state, object, actingSubject);
|
|
1530
|
+
if (!present) {
|
|
1531
|
+
return answer(
|
|
1532
|
+
`I don't see a ${object} here.`,
|
|
1533
|
+
noteFor(`eat — ${object} is neither visible in the ${here} nor carried; declined`),
|
|
1534
|
+
{ miss: true },
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
if (!objectClassChain(rows, object).includes(FOOD_CLASS)) {
|
|
1538
|
+
return answer(
|
|
1539
|
+
`the ${object} isn't food.`,
|
|
1540
|
+
noteFor(`eat — ${object}'s rdf:type/rdfs:subClassOf chain never reaches "${FOOD_CLASS}"; declined by name`),
|
|
1541
|
+
{ miss: true },
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
const eaterMass = state.masses.get(actingSubject)?.value ?? null;
|
|
1545
|
+
if (eaterMass !== null && eaterMass >= ASSUMED_FULL_MASS * HUNGRY_FRACTION) {
|
|
1546
|
+
return answer(
|
|
1547
|
+
`you're too full to eat the ${object}.`,
|
|
1548
|
+
noteFor(`eat — ${actingSubject} weighs ${eaterMass}, at or over half of ${ASSUMED_FULL_MASS}; declined by name`),
|
|
1549
|
+
{ miss: true },
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
const gained = state.masses.get(object)?.value ?? DEFAULT_FOOD_MASS;
|
|
1553
|
+
const grown = Math.round(((eaterMass ?? 0) + gained) * 100) / 100;
|
|
1554
|
+
// Eating is the one act that ends a thing, so the eater is the one witness
|
|
1555
|
+
// whose knowledge of it goes out of date on the spot. Every route into the
|
|
1556
|
+
// eat verb — a typed command, a scripted mud turn — passes here, so the
|
|
1557
|
+
// claim gets written once for all of them.
|
|
1558
|
+
await recordGone(memoryDir, { observer: actingSubject, thing: object, k, cache });
|
|
1559
|
+
return commit(
|
|
1560
|
+
[
|
|
1561
|
+
{ subject: snapshotSubject(actingSubject, k, state.epoch), predicate: MASS_PREDICATE, object: String(grown) },
|
|
1562
|
+
{ subject: snapshotSubject(object, k, state.epoch), predicate: "mgx:located-in", object: CONSUMED_PLACE },
|
|
1563
|
+
],
|
|
1564
|
+
`you eat the ${object}. It adds ${gained} to your mass, so you weigh ${grown} now.`,
|
|
1565
|
+
`eat — the ${object}'s ${gained} mass moves onto ${actingSubject} (now ${grown}) and the ${object} leaves the world`,
|
|
1566
|
+
`eat the ${object}`,
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
async function handlePutVerb(ctx) {
|
|
1571
|
+
const { cmd, rows, state, here, k, family, object, commit, noteFor, actingSubject } = ctx;
|
|
1572
|
+
const container = cmd.indirectObject;
|
|
1573
|
+
if (!carriedBy(state, object, actingSubject)) {
|
|
1574
|
+
return answer(
|
|
1575
|
+
`you're not carrying the ${object}.`,
|
|
1576
|
+
noteFor(`put — ${object} isn't carried; precondition declined by name`),
|
|
1577
|
+
{ miss: true },
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
if (visibleRoomOf(container, { rows, state }) !== here) {
|
|
1581
|
+
return answer(
|
|
1582
|
+
`I don't see a ${container} here.`,
|
|
1583
|
+
noteFor(`put — ${container} isn't visible in the ${here}; declined`),
|
|
1584
|
+
{ miss: true },
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
if (!isContainer(rows, container)) {
|
|
1588
|
+
return answer(
|
|
1589
|
+
`the ${container} doesn't hold things.`,
|
|
1590
|
+
noteFor(`put — no mgx:is-container fact on ${container}; declined by name`),
|
|
1591
|
+
{ miss: true },
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
if (!state.openness.get(container)?.open) {
|
|
1595
|
+
return answer(
|
|
1596
|
+
`the ${container} is closed.`,
|
|
1597
|
+
noteFor(`put — the ${container} isn't open; precondition declined by name`),
|
|
1598
|
+
{ miss: true },
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
return commit(
|
|
1602
|
+
[{ subject: snapshotSubject(object, k, state.epoch), predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: container }],
|
|
1603
|
+
`you put the ${object} in the ${container}.`,
|
|
1604
|
+
`put — the taught "put" family fired; the ${object} now sits in the ${container}`,
|
|
1605
|
+
`put the ${object} in the ${container}`,
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// open / unlock / close — the container verbs, sharing one handler (all
|
|
1610
|
+
// three keys in STATE_VERB_HANDLERS below point here) because they share a
|
|
1611
|
+
// precondition: presence and container-ness stay hand-checked (visibility
|
|
1612
|
+
// gating, not a state precondition), the same visibleRoomOf check
|
|
1613
|
+
// examine/talk/take already use, not a hand-rolled duplicate. unlock's
|
|
1614
|
+
// instrument match stays fully hand-written below it too — it needs a
|
|
1615
|
+
// third, externally-supplied binding beyond subject/target, which this
|
|
1616
|
+
// retrofit does not attempt. open/close's lock-state and open/closed
|
|
1617
|
+
// checks, and their mgx:is-open write, are taught "fact-value" precond/
|
|
1618
|
+
// effect rows consulted through domain.mjs; only the hidden-contents
|
|
1619
|
+
// reveal (a variable-arity effect over a discovered set) stays hand-written
|
|
1620
|
+
// JS, since no shipped rule shape covers that either.
|
|
1621
|
+
async function handleContainerVerb(ctx) {
|
|
1622
|
+
const { cmd, rows, state, here, k, object, place, commit, noteFor, ruleRows } = ctx;
|
|
1623
|
+
if (visibleRoomOf(object, { rows, state }) !== here) {
|
|
1624
|
+
return answer(`I don't see a ${object} here.`, noteFor(`${cmd.verb} — ${object} isn't in the ${here}; declined`), { miss: true });
|
|
1625
|
+
}
|
|
1626
|
+
if (!isContainer(rows, object)) {
|
|
1627
|
+
return answer(`the ${object} doesn't open.`, noteFor(`${cmd.verb} — no mgx:is-container fact on ${object}; declined by name`), { miss: true });
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (cmd.verb === "open" || cmd.verb === "close") {
|
|
1631
|
+
const domain = compileDomain(rows, ruleRows);
|
|
1632
|
+
const taughtAction = domain.actions.find((a) => a.name === cmd.verb);
|
|
1633
|
+
const effect = taughtAction?.effects.find((e) => e.predicate === OPEN_PREDICATE);
|
|
1634
|
+
if (!effect) {
|
|
1635
|
+
return answer(
|
|
1636
|
+
`this world doesn't teach how ${cmd.verb === "open" ? "opening" : "closing"} changes the ${object}.`,
|
|
1637
|
+
noteFor(`${cmd.verb} — the taught "${cmd.verb}" family carries no ${OPEN_PREDICATE} effect; honest decline`),
|
|
1638
|
+
{ miss: true },
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
const factState = containerDatatypeState(state, object);
|
|
1642
|
+
const failed = taughtAction.preconds.find((p) => !precondHolds(p, ctx.actingSubject, object, factState, domain));
|
|
1643
|
+
if (failed) {
|
|
1644
|
+
const text = failed.predicate === "mgx:stands-locked-in"
|
|
1645
|
+
? `the ${object} is locked.`
|
|
1646
|
+
: cmd.verb === "open" ? `the ${object} is already open.` : `the ${object} isn't open.`;
|
|
1647
|
+
return answer(text, noteFor(`${cmd.verb} — the taught "${cmd.verb}" family's ${failed.predicate} precondition declined by name`), { miss: true });
|
|
1648
|
+
}
|
|
1649
|
+
const effSubject = roleBinding(effect.subjectRole, ctx.actingSubject, object, domain);
|
|
1650
|
+
const writeIsOpen = { subject: snapshotSubject(effSubject, k, state.epoch), predicate: effect.predicate, object: effect.value };
|
|
1651
|
+
|
|
1652
|
+
if (cmd.verb === "open") {
|
|
1653
|
+
const revealed = [...state.placements]
|
|
1654
|
+
.filter(([, p]) => p.predicate === "mgx:hidden-in" && p.object === object)
|
|
1655
|
+
.map(([thing]) => thing)
|
|
1656
|
+
.sort();
|
|
1657
|
+
return commit(
|
|
1658
|
+
[
|
|
1659
|
+
writeIsOpen,
|
|
1660
|
+
...revealed.map((thing) => ({ subject: snapshotSubject(thing, k, state.epoch), predicate: "mgx:located-in", object })),
|
|
1661
|
+
],
|
|
1662
|
+
revealed.length
|
|
1663
|
+
? `you open the ${object} — inside: the ${revealed.join(", the ")}.`
|
|
1664
|
+
: `you open the ${object}. It's empty.`,
|
|
1665
|
+
`open — ${object} opens${revealed.length ? `, revealing ${revealed.join(", ")}` : ""} via the taught "open" family's effect`,
|
|
1666
|
+
`open the ${object}`,
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
return commit(
|
|
1670
|
+
[writeIsOpen],
|
|
1671
|
+
`you close the ${object}.`,
|
|
1672
|
+
`close — ${object} closes via the taught "close" family's effect`,
|
|
1673
|
+
`close the ${object}`,
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// unlock
|
|
1678
|
+
if (place.predicate !== "mgx:stands-locked-in") {
|
|
1679
|
+
return answer(`the ${object} isn't locked.`, noteFor("unlock — not locked; declined"), { miss: true });
|
|
1680
|
+
}
|
|
1681
|
+
if (!cmd.instrument) {
|
|
1682
|
+
return answer(
|
|
1683
|
+
`unlock the ${object} with what? Name the thing to use, e.g. "unlock the ${object} with the key".`,
|
|
1684
|
+
noteFor("unlock — no instrument named; asked, never guessed"),
|
|
1685
|
+
{ miss: true },
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
const required = factObjects(rows, object, "mgx:unlocks-with")[0] ?? null;
|
|
1689
|
+
if (!required) {
|
|
1690
|
+
return answer(
|
|
1691
|
+
`nothing in this world says what unlocks the ${object}.`,
|
|
1692
|
+
noteFor(`unlock — no mgx:unlocks-with fact on ${object}; honest decline`),
|
|
1693
|
+
{ miss: true },
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
if (!carriedBy(state, cmd.instrument, ctx.actingSubject)) {
|
|
1697
|
+
return answer(
|
|
1698
|
+
`you're not carrying the ${cmd.instrument}.`,
|
|
1699
|
+
noteFor(`unlock — the ${cmd.instrument} isn't carried; precondition declined by name`),
|
|
1700
|
+
{ miss: true },
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
if (cmd.instrument !== required) {
|
|
1704
|
+
return answer(
|
|
1705
|
+
`the ${cmd.instrument} doesn't fit the ${object}'s lock.`,
|
|
1706
|
+
noteFor(`unlock — the taught instrument is ${required}, not ${cmd.instrument}; declined by name`),
|
|
1707
|
+
{ miss: true },
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
return commit(
|
|
1711
|
+
[{ subject: snapshotSubject(object, k, state.epoch), predicate: "mgx:fixed-in", object: here }],
|
|
1712
|
+
`you unlock the ${object} with the ${required}.`,
|
|
1713
|
+
`unlock — the lock releases; ${object} now stands unlocked (still fixed) in the ${here}`,
|
|
1714
|
+
`unlock the ${object}`,
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// The data-driven verb table itself: every state-changing verb this engine
|
|
1719
|
+
// knows, mapped to the handler that runs it. A world's own taught action
|
|
1720
|
+
// family (checked before this table is ever consulted) decides whether the
|
|
1721
|
+
// verb exists in THIS world; this table decides which JS runs once it does.
|
|
1722
|
+
const STATE_VERB_HANDLERS = {
|
|
1723
|
+
go: handleGoVerb,
|
|
1724
|
+
take: handleTakeVerb,
|
|
1725
|
+
drop: handleDropOrGiveVerb,
|
|
1726
|
+
give: handleDropOrGiveVerb,
|
|
1727
|
+
dig: handleDigVerb,
|
|
1728
|
+
eat: handleEatVerb,
|
|
1729
|
+
put: handlePutVerb,
|
|
1730
|
+
open: handleContainerVerb,
|
|
1731
|
+
close: handleContainerVerb,
|
|
1732
|
+
unlock: handleContainerVerb,
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1242
1735
|
export async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache, actingSubject = "player" }) {
|
|
1243
1736
|
const memory = await loadMemory(memoryDir);
|
|
1244
1737
|
const rows = readFactRows(memory);
|
|
@@ -1410,376 +1903,18 @@ export async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache
|
|
|
1410
1903
|
const object = cmd.object;
|
|
1411
1904
|
const place = object ? state.placements.get(object) ?? null : null;
|
|
1412
1905
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
if (!target) {
|
|
1416
|
-
return answer(
|
|
1417
|
-
`there's no exit ${cmd.direction} from the ${here}.`,
|
|
1418
|
-
noteFor(`go — no mgx:has-exit-${cmd.direction} fact on ${here}; precondition declined by name`),
|
|
1419
|
-
{ miss: true },
|
|
1420
|
-
);
|
|
1421
|
-
}
|
|
1422
|
-
// A predator eats whatever walks in, and the room it guards is the one
|
|
1423
|
-
// room a move never comes back from — so this write bypasses commit()
|
|
1424
|
-
// entirely: the auto-relook there would describe a room the mover is no
|
|
1425
|
-
// longer standing in, and the world has nobody left to look with.
|
|
1426
|
-
const predator = predatorIn(rows, state, target);
|
|
1427
|
-
if (predator) {
|
|
1428
|
-
await writeWorldTurn(memoryDir, world, k, [
|
|
1429
|
-
{ subject: `${actingSubject}@turn${k}`, predicate: "mgx:currently-in", object: CONSUMED_PLACE },
|
|
1430
|
-
], cache);
|
|
1431
|
-
return answer(
|
|
1432
|
-
`you go ${cmd.direction} into the ${target} — and the ${predator} is waiting. It eats the ${actingSubject}. That's the end of its run.`,
|
|
1433
|
-
noteFor(`go — the ${target} holds the predator ${predator}; ${actingSubject} is placed out of play at turn ${k} and takes no further turns`),
|
|
1434
|
-
{ goal: `move through the world (eaten by the ${predator} in the ${target})` },
|
|
1435
|
-
);
|
|
1436
|
-
}
|
|
1437
|
-
return commit(
|
|
1438
|
-
[{ subject: `${actingSubject}@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:currently-in", object: target }],
|
|
1439
|
-
`you go ${cmd.direction}. Now in the ${target}.`,
|
|
1440
|
-
`go — the taught "go" family fired; ${actingSubject} moves ${here} -> ${target}`,
|
|
1441
|
-
`move through the world (now in the ${target})`,
|
|
1442
|
-
target,
|
|
1443
|
-
);
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
if (cmd.verb === "take") {
|
|
1447
|
-
if (isTyped(rows, object, "room")) {
|
|
1448
|
-
return answer(`you can't take the ${object} — it's a whole room.`, noteFor("take — the object is a room; declined"), { miss: true });
|
|
1449
|
-
}
|
|
1450
|
-
if (carriedBy(state, object, actingSubject)) {
|
|
1451
|
-
return answer(`you're already carrying the ${object}.`, noteFor("take — already carried; declined"), { miss: true });
|
|
1452
|
-
}
|
|
1453
|
-
if (place && (place.predicate === "mgx:fixed-in" || place.predicate === "mgx:stands-locked-in") && place.object === here) {
|
|
1454
|
-
return answer(
|
|
1455
|
-
`the ${object} is fixed in place — it can't be taken.`,
|
|
1456
|
-
noteFor(`take — ${object} is placed by ${place.predicate}, not portable; precondition declined by name`),
|
|
1457
|
-
{ miss: true },
|
|
1458
|
-
);
|
|
1459
|
-
}
|
|
1460
|
-
if (place && place.predicate === "mgx:currently-in" && place.object === here) {
|
|
1461
|
-
return answer(`you can't take the ${object}.`, noteFor("take — the object is one of the cast; declined"), { miss: true });
|
|
1462
|
-
}
|
|
1463
|
-
if (visibleRoomOf(object, { rows, state }) !== here) {
|
|
1464
|
-
if (backgroundOnlyMention(rows, state, object)) {
|
|
1465
|
-
return answer(
|
|
1466
|
-
`the ${object} isn't something you can take — it's only mentioned in passing here, not a real prop in this scene.`,
|
|
1467
|
-
noteFor(`take — ${object} is a background-only mention, never a placed object; declined honestly`),
|
|
1468
|
-
{ miss: true },
|
|
1469
|
-
);
|
|
1470
|
-
}
|
|
1471
|
-
return answer(`I don't see a ${object} here.`, noteFor(`take — ${object} isn't visible in the ${here}; declined, hidden things stay hidden`), { miss: true });
|
|
1472
|
-
}
|
|
1473
|
-
return commit(
|
|
1474
|
-
[{ subject: `${object}@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: actingSubject }],
|
|
1475
|
-
`you take the ${object}.`,
|
|
1476
|
-
`take — the taught "take" family fired; ${object} is now carried`,
|
|
1477
|
-
`carry the ${object}`,
|
|
1478
|
-
);
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
if (cmd.verb === "drop" || cmd.verb === "give") {
|
|
1482
|
-
if (!carriedBy(state, object, actingSubject)) {
|
|
1483
|
-
return answer(`you're not carrying the ${object}.`, noteFor(`${cmd.verb} — ${object} isn't carried; precondition declined by name`), { miss: true });
|
|
1484
|
-
}
|
|
1485
|
-
if (cmd.verb === "drop") {
|
|
1486
|
-
return commit(
|
|
1487
|
-
[{ subject: `${object}@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: here }],
|
|
1488
|
-
`you drop the ${object} in the ${here}.`,
|
|
1489
|
-
`drop — the taught "drop" family fired; ${object} rests in the ${here}`,
|
|
1490
|
-
`set the ${object} down`,
|
|
1491
|
-
);
|
|
1492
|
-
}
|
|
1493
|
-
const receiver = cmd.indirectObject;
|
|
1494
|
-
if (!isCastMember(rows, state, receiver) || state.placements.get(receiver)?.object !== here) {
|
|
1495
|
-
return answer(`the ${receiver} isn't here.`, noteFor(`give — ${receiver} isn't one of the cast standing in the ${here}; precondition declined by name`), { miss: true });
|
|
1496
|
-
}
|
|
1497
|
-
return commit(
|
|
1498
|
-
[{ subject: `${object}@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: receiver }],
|
|
1499
|
-
`you give the ${object} to the ${receiver}.`,
|
|
1500
|
-
`give — the taught "give" family fired; the ${receiver} holds the ${object}`,
|
|
1501
|
-
`hand the ${object} over`,
|
|
1502
|
-
);
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
if (cmd.verb === "dig") {
|
|
1506
|
-
const direction = cmd.direction;
|
|
1507
|
-
if (state.exits.get(here)?.get(direction)) {
|
|
1508
|
-
return answer(
|
|
1509
|
-
`there's already an exit ${direction} from the ${here}.`,
|
|
1510
|
-
noteFor(`dig — an mgx:has-exit-${direction} fact already stands on ${here}; declined, a dig never overwrites an exit`),
|
|
1511
|
-
{ miss: true },
|
|
1512
|
-
);
|
|
1513
|
-
}
|
|
1514
|
-
const back = OPPOSITE_DIRECTION.get(direction);
|
|
1515
|
-
if (!back) {
|
|
1516
|
-
return answer(
|
|
1517
|
-
`I don't know which way back a ${direction} tunnel would run.`,
|
|
1518
|
-
noteFor(`dig — "${direction}" has no opposite to write the return exit with; declined by name`),
|
|
1519
|
-
{ miss: true },
|
|
1520
|
-
);
|
|
1521
|
-
}
|
|
1522
|
-
const roomKind = roomKindOf(rows, here);
|
|
1523
|
-
const dugKind = (DIGGABLE_BY_ROOM_KIND.get(roomKind) ?? new Map()).get(direction) ?? null;
|
|
1524
|
-
if (!dugKind) {
|
|
1525
|
-
return answer(
|
|
1526
|
-
DIG_DECLINE_BY_ROOM_KIND[roomKind](here, direction),
|
|
1527
|
-
noteFor(`dig — the ${here} is an ${roomKind} room, which cannot be dug ${direction}; declined by the room's own kind`),
|
|
1528
|
-
{ miss: true },
|
|
1529
|
-
);
|
|
1530
|
-
}
|
|
1531
|
-
if (atDigBoundary(rows, state, here)) {
|
|
1532
|
-
const reach = roomDistanceFromOrigin(rows, state, here);
|
|
1533
|
-
return answer(
|
|
1534
|
-
`the earth ${direction} of the ${here} is packed hard and endless — you have reached the far edge of the burrow.`,
|
|
1535
|
-
noteFor(reach === null
|
|
1536
|
-
? `dig — no chain of exits joins the ${here} to the ${originRoomOf(rows)}, so there is no distance to measure a dig against; declined`
|
|
1537
|
-
: `dig — the ${here} stands ${reach} rooms from the ${originRoomOf(rows)}, and this world digs ${digReachOf(rows)}; declined by distance from the origin`),
|
|
1538
|
-
{ miss: true },
|
|
1539
|
-
);
|
|
1540
|
-
}
|
|
1541
|
-
const dug = freshRoomId(rows, here, direction);
|
|
1542
|
-
const denChanceIn = declaredCountOr(rows, dugKind, DEN_CHANCE_PREDICATE, DEFAULT_DEN_CHANCE_IN);
|
|
1543
|
-
const isDen = dugKind === "underground-space" && stableIndex(`den:${dug}`, denChanceIn) === 0;
|
|
1544
|
-
const spawnMax = declaredCountOr(rows, dugKind, DIG_SPAWN_MAX_PREDICATE, DEFAULT_DIG_SPAWN_MAX);
|
|
1545
|
-
const spawnCount = DIG_SPAWN_MIN + stableIndex(dug, spawnMax - DIG_SPAWN_MIN + 1);
|
|
1546
|
-
const spawnedKinds = isDen
|
|
1547
|
-
? declaredKindsOr(rows, dugKind, DEN_SPAWN_PREDICATE, DIG_SPAWN_KINDS)
|
|
1548
|
-
: declaredKindsOr(rows, dugKind, DIG_SPAWN_PREDICATE, DIG_SPAWN_KINDS).slice(0, spawnCount);
|
|
1549
|
-
const minted = new Set();
|
|
1550
|
-
const spawned = spawnedKinds.map((kind) => {
|
|
1551
|
-
const id = freshObjectId(rows, kind, minted);
|
|
1552
|
-
minted.add(id);
|
|
1553
|
-
return id;
|
|
1554
|
-
});
|
|
1555
|
-
const residentChanceIn = declaredCountOr(rows, dugKind, DEN_RESIDENT_CHANCE_PREDICATE, DEFAULT_DEN_RESIDENT_CHANCE_IN);
|
|
1556
|
-
const residentKind = isDen && stableIndex(`resident:${dug}`, residentChanceIn) === 0
|
|
1557
|
-
? factObjects(rows, dugKind, DEN_RESIDENT_PREDICATE)[0] ?? null
|
|
1558
|
-
: null;
|
|
1559
|
-
const resident = residentKind ? freshObjectId(rows, residentKind, minted) : null;
|
|
1560
|
-
return commit(
|
|
1561
|
-
[
|
|
1562
|
-
{ subject: dug, predicate: "rdf:type", object: "room" },
|
|
1563
|
-
{ subject: dug, predicate: "rdf:type", object: dugKind },
|
|
1564
|
-
...(isDen ? [{ subject: dug, predicate: "rdf:type", object: DEN_ROOM_CLASS }] : []),
|
|
1565
|
-
{ subject: here, predicate: `mgx:has-exit-${direction}`, object: dug },
|
|
1566
|
-
{ subject: dug, predicate: `mgx:has-exit-${back}`, object: here },
|
|
1567
|
-
// Typed to its OWN kind, not a flat "portable" — a spawned kind the
|
|
1568
|
-
// world declares rdfs:subClassOf food needs its real class reachable
|
|
1569
|
-
// here for isFood's own objectClassChain walk, or digging up "carrot-1"
|
|
1570
|
-
// would still read as inedible scenery. The class's own mass copies
|
|
1571
|
-
// onto the instance for the same reason: eat reads the instance.
|
|
1572
|
-
...spawnedKinds.flatMap((kind, i) => ([
|
|
1573
|
-
{ subject: spawned[i], predicate: "rdf:type", object: kind },
|
|
1574
|
-
{ subject: spawned[i], predicate: DISPLAY_NAME_PREDICATE, object: kind },
|
|
1575
|
-
{ subject: spawned[i], predicate: "mgx:located-in", object: dug },
|
|
1576
|
-
...classMassFacts(rows, spawned[i], kind),
|
|
1577
|
-
])),
|
|
1578
|
-
// A resident is placed with currently-in, the predicate that makes an
|
|
1579
|
-
// individual one of the cast, and knows about what its own den holds —
|
|
1580
|
-
// so an animal that digs one out has somebody new to ask about food.
|
|
1581
|
-
...(resident ? [
|
|
1582
|
-
{ subject: resident, predicate: "rdf:type", object: residentKind },
|
|
1583
|
-
{ subject: resident, predicate: DISPLAY_NAME_PREDICATE, object: residentKind },
|
|
1584
|
-
{ subject: resident, predicate: "mgx:currently-in", object: dug },
|
|
1585
|
-
...classMassFacts(rows, resident, residentKind),
|
|
1586
|
-
...spawned.map((thing) => ({ subject: resident, predicate: KNOWS_ABOUT_PREDICATE, object: thing })),
|
|
1587
|
-
] : []),
|
|
1588
|
-
],
|
|
1589
|
-
digNarration(direction, { isDen, spawned, resident }),
|
|
1590
|
-
`dig — minted the ${isDen ? `${DEN_ROOM_CLASS} ` : ""}${dugKind} ${dug} with exits both ways (${direction} out, ${back} back)${spawned.length ? `, and ${spawned.length} object(s) in it` : ""}${resident ? `, lived in by ${resident}` : ""}; digging spends the turn, so the digger stays in the ${here}`,
|
|
1591
|
-
`dig ${direction} out of the ${here}`,
|
|
1592
|
-
);
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
if (cmd.verb === "eat") {
|
|
1596
|
-
const present = visibleRoomOf(object, { rows, state }) === here || carriedBy(state, object, actingSubject);
|
|
1597
|
-
if (!present) {
|
|
1598
|
-
return answer(
|
|
1599
|
-
`I don't see a ${object} here.`,
|
|
1600
|
-
noteFor(`eat — ${object} is neither visible in the ${here} nor carried; declined`),
|
|
1601
|
-
{ miss: true },
|
|
1602
|
-
);
|
|
1603
|
-
}
|
|
1604
|
-
if (!objectClassChain(rows, object).includes(FOOD_CLASS)) {
|
|
1605
|
-
return answer(
|
|
1606
|
-
`the ${object} isn't food.`,
|
|
1607
|
-
noteFor(`eat — ${object}'s rdf:type/rdfs:subClassOf chain never reaches "${FOOD_CLASS}"; declined by name`),
|
|
1608
|
-
{ miss: true },
|
|
1609
|
-
);
|
|
1610
|
-
}
|
|
1611
|
-
const eaterMass = state.masses.get(actingSubject)?.value ?? null;
|
|
1612
|
-
if (eaterMass !== null && eaterMass >= ASSUMED_FULL_MASS * HUNGRY_FRACTION) {
|
|
1613
|
-
return answer(
|
|
1614
|
-
`you're too full to eat the ${object}.`,
|
|
1615
|
-
noteFor(`eat — ${actingSubject} weighs ${eaterMass}, at or over half of ${ASSUMED_FULL_MASS}; declined by name`),
|
|
1616
|
-
{ miss: true },
|
|
1617
|
-
);
|
|
1618
|
-
}
|
|
1619
|
-
const gained = state.masses.get(object)?.value ?? DEFAULT_FOOD_MASS;
|
|
1620
|
-
const grown = Math.round(((eaterMass ?? 0) + gained) * 100) / 100;
|
|
1621
|
-
// Eating is the one act that ends a thing, so the eater is the one witness
|
|
1622
|
-
// whose knowledge of it goes out of date on the spot. Every route into the
|
|
1623
|
-
// eat verb — a typed command, a scripted mud turn — passes here, so the
|
|
1624
|
-
// claim gets written once for all of them.
|
|
1625
|
-
await recordGone(memoryDir, { observer: actingSubject, thing: object, k, cache });
|
|
1626
|
-
return commit(
|
|
1627
|
-
[
|
|
1628
|
-
{ subject: `${actingSubject}@turn${k}`, predicate: MASS_PREDICATE, object: String(grown) },
|
|
1629
|
-
{ subject: `${object}@turn${k}`, predicate: "mgx:located-in", object: CONSUMED_PLACE },
|
|
1630
|
-
],
|
|
1631
|
-
`you eat the ${object}. It adds ${gained} to your mass, so you weigh ${grown} now.`,
|
|
1632
|
-
`eat — the ${object}'s ${gained} mass moves onto ${actingSubject} (now ${grown}) and the ${object} leaves the world`,
|
|
1633
|
-
`eat the ${object}`,
|
|
1634
|
-
);
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
if (cmd.verb === "put") {
|
|
1638
|
-
const container = cmd.indirectObject;
|
|
1639
|
-
if (!carriedBy(state, object, actingSubject)) {
|
|
1640
|
-
return answer(
|
|
1641
|
-
`you're not carrying the ${object}.`,
|
|
1642
|
-
noteFor(`put — ${object} isn't carried; precondition declined by name`),
|
|
1643
|
-
{ miss: true },
|
|
1644
|
-
);
|
|
1645
|
-
}
|
|
1646
|
-
if (visibleRoomOf(container, { rows, state }) !== here) {
|
|
1647
|
-
return answer(
|
|
1648
|
-
`I don't see a ${container} here.`,
|
|
1649
|
-
noteFor(`put — ${container} isn't visible in the ${here}; declined`),
|
|
1650
|
-
{ miss: true },
|
|
1651
|
-
);
|
|
1652
|
-
}
|
|
1653
|
-
if (!isContainer(rows, container)) {
|
|
1654
|
-
return answer(
|
|
1655
|
-
`the ${container} doesn't hold things.`,
|
|
1656
|
-
noteFor(`put — no mgx:is-container fact on ${container}; declined by name`),
|
|
1657
|
-
{ miss: true },
|
|
1658
|
-
);
|
|
1659
|
-
}
|
|
1660
|
-
if (!state.openness.get(container)?.open) {
|
|
1661
|
-
return answer(
|
|
1662
|
-
`the ${container} is closed.`,
|
|
1663
|
-
noteFor(`put — the ${container} isn't open; precondition declined by name`),
|
|
1664
|
-
{ miss: true },
|
|
1665
|
-
);
|
|
1666
|
-
}
|
|
1667
|
-
return commit(
|
|
1668
|
-
[{ subject: `${object}@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: container }],
|
|
1669
|
-
`you put the ${object} in the ${container}.`,
|
|
1670
|
-
`put — the taught "put" family fired; the ${object} now sits in the ${container}`,
|
|
1671
|
-
`put the ${object} in the ${container}`,
|
|
1672
|
-
);
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
// open / unlock / close — the container verbs. presence and container-ness
|
|
1676
|
-
// stay hand-checked here (visibility gating, not a state precondition);
|
|
1677
|
-
// unlock's instrument match stays fully hand-written below it too — it
|
|
1678
|
-
// needs a third, externally-supplied binding beyond subject/target, which
|
|
1679
|
-
// this retrofit does not attempt. open/close's lock-state and open/closed
|
|
1680
|
-
// checks, and their mgx:is-open write, are now taught "fact-value"
|
|
1681
|
-
// precond/effect rows consulted through domain.mjs below; only the
|
|
1682
|
-
// hidden-contents reveal (a variable-arity effect over a discovered set)
|
|
1683
|
-
// stays hand-written JS, since no shipped rule shape covers that either.
|
|
1684
|
-
// Presence reuses visibleRoomOf (the SAME check examine/talk/take already
|
|
1685
|
-
// use), not a hand-rolled duplicate: an earlier version of this check
|
|
1686
|
-
// excluded mgx:currently-in outright, so a genuinely-present NPC (placed
|
|
1687
|
-
// that way, not fixed-in/stands-locked-in) fell into "I don't see a X
|
|
1688
|
-
// here" instead of reaching the isContainer check just below, which would
|
|
1689
|
-
// have honestly said "the X doesn't open."
|
|
1690
|
-
if (visibleRoomOf(object, { rows, state }) !== here) {
|
|
1691
|
-
return answer(`I don't see a ${object} here.`, noteFor(`${cmd.verb} — ${object} isn't in the ${here}; declined`), { miss: true });
|
|
1692
|
-
}
|
|
1693
|
-
if (!isContainer(rows, object)) {
|
|
1694
|
-
return answer(`the ${object} doesn't open.`, noteFor(`${cmd.verb} — no mgx:is-container fact on ${object}; declined by name`), { miss: true });
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
if (cmd.verb === "open" || cmd.verb === "close") {
|
|
1698
|
-
const domain = compileDomain(rows, ruleRows);
|
|
1699
|
-
const taughtAction = domain.actions.find((a) => a.name === cmd.verb);
|
|
1700
|
-
const effect = taughtAction?.effects.find((e) => e.predicate === OPEN_PREDICATE);
|
|
1701
|
-
if (!effect) {
|
|
1702
|
-
return answer(
|
|
1703
|
-
`this world doesn't teach how ${cmd.verb === "open" ? "opening" : "closing"} changes the ${object}.`,
|
|
1704
|
-
noteFor(`${cmd.verb} — the taught "${cmd.verb}" family carries no ${OPEN_PREDICATE} effect; honest decline`),
|
|
1705
|
-
{ miss: true },
|
|
1706
|
-
);
|
|
1707
|
-
}
|
|
1708
|
-
const factState = containerDatatypeState(state, object);
|
|
1709
|
-
const failed = taughtAction.preconds.find((p) => !precondHolds(p, actingSubject, object, factState, domain));
|
|
1710
|
-
if (failed) {
|
|
1711
|
-
const text = failed.predicate === "mgx:stands-locked-in"
|
|
1712
|
-
? `the ${object} is locked.`
|
|
1713
|
-
: cmd.verb === "open" ? `the ${object} is already open.` : `the ${object} isn't open.`;
|
|
1714
|
-
return answer(text, noteFor(`${cmd.verb} — the taught "${cmd.verb}" family's ${failed.predicate} precondition declined by name`), { miss: true });
|
|
1715
|
-
}
|
|
1716
|
-
const effSubject = roleBinding(effect.subjectRole, actingSubject, object, domain);
|
|
1717
|
-
const writeIsOpen = { subject: `${effSubject}@turn${k}`, predicate: effect.predicate, object: effect.value };
|
|
1718
|
-
|
|
1719
|
-
if (cmd.verb === "open") {
|
|
1720
|
-
const revealed = [...state.placements]
|
|
1721
|
-
.filter(([, p]) => p.predicate === "mgx:hidden-in" && p.object === object)
|
|
1722
|
-
.map(([thing]) => thing)
|
|
1723
|
-
.sort();
|
|
1724
|
-
return commit(
|
|
1725
|
-
[
|
|
1726
|
-
writeIsOpen,
|
|
1727
|
-
...revealed.map((thing) => ({ subject: `${thing}@turn${k}`, predicate: "mgx:located-in", object })),
|
|
1728
|
-
],
|
|
1729
|
-
revealed.length
|
|
1730
|
-
? `you open the ${object} — inside: the ${revealed.join(", the ")}.`
|
|
1731
|
-
: `you open the ${object}. It's empty.`,
|
|
1732
|
-
`open — ${object} opens${revealed.length ? `, revealing ${revealed.join(", ")}` : ""} via the taught "open" family's effect`,
|
|
1733
|
-
`open the ${object}`,
|
|
1734
|
-
);
|
|
1735
|
-
}
|
|
1736
|
-
return commit(
|
|
1737
|
-
[writeIsOpen],
|
|
1738
|
-
`you close the ${object}.`,
|
|
1739
|
-
`close — ${object} closes via the taught "close" family's effect`,
|
|
1740
|
-
`close the ${object}`,
|
|
1741
|
-
);
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
// unlock
|
|
1745
|
-
if (place.predicate !== "mgx:stands-locked-in") {
|
|
1746
|
-
return answer(`the ${object} isn't locked.`, noteFor("unlock — not locked; declined"), { miss: true });
|
|
1747
|
-
}
|
|
1748
|
-
if (!cmd.instrument) {
|
|
1749
|
-
return answer(
|
|
1750
|
-
`unlock the ${object} with what? Name the thing to use, e.g. "unlock the ${object} with the key".`,
|
|
1751
|
-
noteFor("unlock — no instrument named; asked, never guessed"),
|
|
1752
|
-
{ miss: true },
|
|
1753
|
-
);
|
|
1754
|
-
}
|
|
1755
|
-
const required = factObjects(rows, object, "mgx:unlocks-with")[0] ?? null;
|
|
1756
|
-
if (!required) {
|
|
1757
|
-
return answer(
|
|
1758
|
-
`nothing in this world says what unlocks the ${object}.`,
|
|
1759
|
-
noteFor(`unlock — no mgx:unlocks-with fact on ${object}; honest decline`),
|
|
1760
|
-
{ miss: true },
|
|
1761
|
-
);
|
|
1762
|
-
}
|
|
1763
|
-
if (!carriedBy(state, cmd.instrument, actingSubject)) {
|
|
1906
|
+
const handler = STATE_VERB_HANDLERS[cmd.verb];
|
|
1907
|
+
if (!handler) {
|
|
1764
1908
|
return answer(
|
|
1765
|
-
`
|
|
1766
|
-
noteFor(
|
|
1909
|
+
`this world doesn't know how to run the verb "${cmd.verb}" yet.`,
|
|
1910
|
+
noteFor(`${cmd.verb} — a taught family exists but no handler is wired for it; honest decline`),
|
|
1767
1911
|
{ miss: true },
|
|
1768
1912
|
);
|
|
1769
1913
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
{ miss: true },
|
|
1775
|
-
);
|
|
1776
|
-
}
|
|
1777
|
-
return commit(
|
|
1778
|
-
[{ subject: `${object}@turn${k}`, predicate: "mgx:fixed-in", object: here }],
|
|
1779
|
-
`you unlock the ${object} with the ${required}.`,
|
|
1780
|
-
`unlock — the lock releases; ${object} now stands unlocked (still fixed) in the ${here}`,
|
|
1781
|
-
`unlock the ${object}`,
|
|
1782
|
-
);
|
|
1914
|
+
return handler({
|
|
1915
|
+
cmd, rows, state, here, k, family, families, ruleRows, object, place,
|
|
1916
|
+
commit, noteFor, actingSubject, memoryDir, world, cache, graph, memory,
|
|
1917
|
+
});
|
|
1783
1918
|
}
|
|
1784
1919
|
|
|
1785
1920
|
// A mid-game "where is X" aside. Optional trailing "now": the question means
|