@polycode-projects/the-mechanical-code-talker 4.0.0 → 4.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 +176 -8
- 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 +34 -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/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/reference-pack.mjs +31 -7
- package/src/domain/spider-fly-world.mjs +1 -1
- package/src/domain/sprite-templates.mjs +8 -6
- package/src/services/adventure-viz.mjs +130 -47
- package/src/services/adventure.mjs +440 -367
- package/src/services/chat-page-viz.mjs +37 -9
- package/src/services/chat.mjs +99 -18
- package/src/services/code-explorer-viz.mjs +52 -14
- package/src/services/extract-facts.mjs +4 -7
- package/src/services/ingest-viz.mjs +54 -26
- package/src/services/ledger-viz.mjs +105 -56
- package/src/services/memory-panel-viz.mjs +24 -0
- package/src/services/mud-viz.mjs +940 -21
- package/src/services/plan-viz.mjs +119 -61
- package/src/services/research-viz.mjs +151 -57
- package/src/services/spider-fly-turn.mjs +1 -1
- package/src/services/spider-fly-viz.mjs +67 -43
- package/src/services/sprite-catalog-viz.mjs +175 -51
- package/src/services/viz-theme.mjs +15 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +107 -107
- package/src/surfaces/web/mud-browser-entry.mjs +44 -2
|
@@ -227,6 +227,33 @@ export function worldActionRows(rows) {
|
|
|
227
227
|
});
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
// The predicates a played turn can write. Every one of them already exists
|
|
231
|
+
// above as a private constant; this set is the one place they are gathered so
|
|
232
|
+
// a caller outside this file can ask "is this fact part of the live world"
|
|
233
|
+
// without learning each name. rdf:type and the display name earn their place
|
|
234
|
+
// alongside the folded ones: a dug room and a dug object are minted mid-play,
|
|
235
|
+
// and a reader that never receives their class or their plain name cannot draw
|
|
236
|
+
// them at all.
|
|
237
|
+
const MUD_STATE_PREDICATES = new Set([
|
|
238
|
+
...PLACEMENT_PREDICATES,
|
|
239
|
+
...POSITION_PREDICATES,
|
|
240
|
+
OPEN_PREDICATE,
|
|
241
|
+
MASS_PREDICATE,
|
|
242
|
+
KNOWS_ABOUT_PREDICATE,
|
|
243
|
+
DISPLAY_NAME_PREDICATE,
|
|
244
|
+
"rdf:type",
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
/** Whether `predicate` carries live world state — where a thing stands, what
|
|
248
|
+
* it weighs, whether it is open, what somebody knows, which way a room leads,
|
|
249
|
+
* and the class and name a dug thing is minted with. The P2P sync filter
|
|
250
|
+
* reads this to tell a fact a turn produced from the page chrome around it.
|
|
251
|
+
* Pure. */
|
|
252
|
+
export function isMudStatePredicate(predicate) {
|
|
253
|
+
const p = String(predicate || "");
|
|
254
|
+
return MUD_STATE_PREDICATES.has(p) || EXIT_PREDICATE_RE.test(p);
|
|
255
|
+
}
|
|
256
|
+
|
|
230
257
|
/** Every individual the world names — its rooms, its cast, its props, and
|
|
231
258
|
* anything dug up since — as the plain id strings a parser has to have
|
|
232
259
|
* DECLARED before it can resolve them. @turnN snapshots are skipped: a
|
|
@@ -1193,7 +1220,7 @@ function containerDatatypeState(state, object) {
|
|
|
1193
1220
|
export function personKnowledgeLines(rows, state, person) {
|
|
1194
1221
|
const lines = [];
|
|
1195
1222
|
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
|
|
1223
|
+
lines.push(`the ${objective} is what you're after — find it and carry it, and the adventure is won.`);
|
|
1197
1224
|
}
|
|
1198
1225
|
for (const thing of factObjects(rows, person, "mgx:knows-where")) {
|
|
1199
1226
|
const place = state.placements.get(thing);
|
|
@@ -1239,6 +1266,410 @@ export function personRoomReport(rows, state, person, actingSubject = "player")
|
|
|
1239
1266
|
return parts.join(" ");
|
|
1240
1267
|
}
|
|
1241
1268
|
|
|
1269
|
+
// ---- state-changing verb handlers --------------------------------------------
|
|
1270
|
+
//
|
|
1271
|
+
// One function per state-changing verb (or per shared-precondition group of
|
|
1272
|
+
// verbs), keyed by name in STATE_VERB_HANDLERS below rather than chained as
|
|
1273
|
+
// if/else-if branches: adding a verb means adding a table entry, never
|
|
1274
|
+
// widening an existing conditional. Each handler takes the one `ctx` object
|
|
1275
|
+
// runWorldCommand builds once per call — cmd, the current fold, the taught
|
|
1276
|
+
// action family, and the shared `commit`/`noteFor` closures — so a handler
|
|
1277
|
+
// reads exactly like the branch it replaces, just lifted to its own name.
|
|
1278
|
+
|
|
1279
|
+
async function handleGoVerb(ctx) {
|
|
1280
|
+
const { cmd, rows, state, here, k, family, commit, noteFor, memoryDir, world, cache, actingSubject } = ctx;
|
|
1281
|
+
const target = state.exits.get(here)?.get(cmd.direction);
|
|
1282
|
+
if (!target) {
|
|
1283
|
+
return answer(
|
|
1284
|
+
`there's no exit ${cmd.direction} from the ${here}.`,
|
|
1285
|
+
noteFor(`go — no mgx:has-exit-${cmd.direction} fact on ${here}; precondition declined by name`),
|
|
1286
|
+
{ miss: true },
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
// A predator eats whatever walks in, and the room it guards is the one
|
|
1290
|
+
// room a move never comes back from — so this write bypasses commit()
|
|
1291
|
+
// entirely: the auto-relook there would describe a room the mover is no
|
|
1292
|
+
// longer standing in, and the world has nobody left to look with.
|
|
1293
|
+
const predator = predatorIn(rows, state, target);
|
|
1294
|
+
if (predator) {
|
|
1295
|
+
await writeWorldTurn(memoryDir, world, k, [
|
|
1296
|
+
{ subject: `${actingSubject}@turn${k}`, predicate: "mgx:currently-in", object: CONSUMED_PLACE },
|
|
1297
|
+
], cache);
|
|
1298
|
+
return answer(
|
|
1299
|
+
`you go ${cmd.direction} into the ${target} — and the ${predator} is waiting. It eats the ${actingSubject}. That's the end of its run.`,
|
|
1300
|
+
noteFor(`go — the ${target} holds the predator ${predator}; ${actingSubject} is placed out of play at turn ${k} and takes no further turns`),
|
|
1301
|
+
{ goal: `move through the world (eaten by the ${predator} in the ${target})` },
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
return commit(
|
|
1305
|
+
[{ subject: `${actingSubject}@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:currently-in", object: target }],
|
|
1306
|
+
`you go ${cmd.direction}. Now in the ${target}.`,
|
|
1307
|
+
`go — the taught "go" family fired; ${actingSubject} moves ${here} -> ${target}`,
|
|
1308
|
+
`move through the world (now in the ${target})`,
|
|
1309
|
+
target,
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
async function handleTakeVerb(ctx) {
|
|
1314
|
+
const { rows, state, here, k, family, object, place, commit, noteFor, actingSubject } = ctx;
|
|
1315
|
+
if (isTyped(rows, object, "room")) {
|
|
1316
|
+
return answer(`you can't take the ${object} — it's a whole room.`, noteFor("take — the object is a room; declined"), { miss: true });
|
|
1317
|
+
}
|
|
1318
|
+
if (carriedBy(state, object, actingSubject)) {
|
|
1319
|
+
return answer(`you're already carrying the ${object}.`, noteFor("take — already carried; declined"), { miss: true });
|
|
1320
|
+
}
|
|
1321
|
+
if (place && (place.predicate === "mgx:fixed-in" || place.predicate === "mgx:stands-locked-in") && place.object === here) {
|
|
1322
|
+
return answer(
|
|
1323
|
+
`the ${object} is fixed in place — it can't be taken.`,
|
|
1324
|
+
noteFor(`take — ${object} is placed by ${place.predicate}, not portable; precondition declined by name`),
|
|
1325
|
+
{ miss: true },
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
if (place && place.predicate === "mgx:currently-in" && place.object === here) {
|
|
1329
|
+
return answer(`you can't take the ${object}.`, noteFor("take — the object is one of the cast; declined"), { miss: true });
|
|
1330
|
+
}
|
|
1331
|
+
if (visibleRoomOf(object, { rows, state }) !== here) {
|
|
1332
|
+
if (backgroundOnlyMention(rows, state, object)) {
|
|
1333
|
+
return answer(
|
|
1334
|
+
`the ${object} isn't something you can take — it's only mentioned in passing here, not a real prop in this scene.`,
|
|
1335
|
+
noteFor(`take — ${object} is a background-only mention, never a placed object; declined honestly`),
|
|
1336
|
+
{ miss: true },
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
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 });
|
|
1340
|
+
}
|
|
1341
|
+
return commit(
|
|
1342
|
+
[{ subject: `${object}@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: actingSubject }],
|
|
1343
|
+
`you take the ${object}.`,
|
|
1344
|
+
`take — the taught "take" family fired; ${object} is now carried`,
|
|
1345
|
+
`carry the ${object}`,
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
async function handleDropOrGiveVerb(ctx) {
|
|
1350
|
+
const { cmd, rows, state, here, k, family, object, commit, noteFor, actingSubject } = ctx;
|
|
1351
|
+
if (!carriedBy(state, object, actingSubject)) {
|
|
1352
|
+
return answer(`you're not carrying the ${object}.`, noteFor(`${cmd.verb} — ${object} isn't carried; precondition declined by name`), { miss: true });
|
|
1353
|
+
}
|
|
1354
|
+
if (cmd.verb === "drop") {
|
|
1355
|
+
return commit(
|
|
1356
|
+
[{ subject: `${object}@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: here }],
|
|
1357
|
+
`you drop the ${object} in the ${here}.`,
|
|
1358
|
+
`drop — the taught "drop" family fired; ${object} rests in the ${here}`,
|
|
1359
|
+
`set the ${object} down`,
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const receiver = cmd.indirectObject;
|
|
1363
|
+
if (!isCastMember(rows, state, receiver) || state.placements.get(receiver)?.object !== here) {
|
|
1364
|
+
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 });
|
|
1365
|
+
}
|
|
1366
|
+
return commit(
|
|
1367
|
+
[{ subject: `${object}@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: receiver }],
|
|
1368
|
+
`you give the ${object} to the ${receiver}.`,
|
|
1369
|
+
`give — the taught "give" family fired; the ${receiver} holds the ${object}`,
|
|
1370
|
+
`hand the ${object} over`,
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
async function handleDigVerb(ctx) {
|
|
1375
|
+
const { cmd, rows, state, here, k, commit, noteFor } = ctx;
|
|
1376
|
+
const direction = cmd.direction;
|
|
1377
|
+
if (state.exits.get(here)?.get(direction)) {
|
|
1378
|
+
return answer(
|
|
1379
|
+
`there's already an exit ${direction} from the ${here}.`,
|
|
1380
|
+
noteFor(`dig — an mgx:has-exit-${direction} fact already stands on ${here}; declined, a dig never overwrites an exit`),
|
|
1381
|
+
{ miss: true },
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
const back = OPPOSITE_DIRECTION.get(direction);
|
|
1385
|
+
if (!back) {
|
|
1386
|
+
return answer(
|
|
1387
|
+
`I don't know which way back a ${direction} tunnel would run.`,
|
|
1388
|
+
noteFor(`dig — "${direction}" has no opposite to write the return exit with; declined by name`),
|
|
1389
|
+
{ miss: true },
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
const roomKind = roomKindOf(rows, here);
|
|
1393
|
+
const dugKind = (DIGGABLE_BY_ROOM_KIND.get(roomKind) ?? new Map()).get(direction) ?? null;
|
|
1394
|
+
if (!dugKind) {
|
|
1395
|
+
return answer(
|
|
1396
|
+
DIG_DECLINE_BY_ROOM_KIND[roomKind](here, direction),
|
|
1397
|
+
noteFor(`dig — the ${here} is an ${roomKind} room, which cannot be dug ${direction}; declined by the room's own kind`),
|
|
1398
|
+
{ miss: true },
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
if (atDigBoundary(rows, state, here)) {
|
|
1402
|
+
const reach = roomDistanceFromOrigin(rows, state, here);
|
|
1403
|
+
return answer(
|
|
1404
|
+
`the earth ${direction} of the ${here} is packed hard and endless — you have reached the far edge of the burrow.`,
|
|
1405
|
+
noteFor(reach === null
|
|
1406
|
+
? `dig — no chain of exits joins the ${here} to the ${originRoomOf(rows)}, so there is no distance to measure a dig against; declined`
|
|
1407
|
+
: `dig — the ${here} stands ${reach} rooms from the ${originRoomOf(rows)}, and this world digs ${digReachOf(rows)}; declined by distance from the origin`),
|
|
1408
|
+
{ miss: true },
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
const dug = freshRoomId(rows, here, direction);
|
|
1412
|
+
const denChanceIn = declaredCountOr(rows, dugKind, DEN_CHANCE_PREDICATE, DEFAULT_DEN_CHANCE_IN);
|
|
1413
|
+
const isDen = dugKind === "underground-space" && stableIndex(`den:${dug}`, denChanceIn) === 0;
|
|
1414
|
+
const spawnMax = declaredCountOr(rows, dugKind, DIG_SPAWN_MAX_PREDICATE, DEFAULT_DIG_SPAWN_MAX);
|
|
1415
|
+
const spawnCount = DIG_SPAWN_MIN + stableIndex(dug, spawnMax - DIG_SPAWN_MIN + 1);
|
|
1416
|
+
const spawnedKinds = isDen
|
|
1417
|
+
? declaredKindsOr(rows, dugKind, DEN_SPAWN_PREDICATE, DIG_SPAWN_KINDS)
|
|
1418
|
+
: declaredKindsOr(rows, dugKind, DIG_SPAWN_PREDICATE, DIG_SPAWN_KINDS).slice(0, spawnCount);
|
|
1419
|
+
const minted = new Set();
|
|
1420
|
+
const spawned = spawnedKinds.map((kind) => {
|
|
1421
|
+
const id = freshObjectId(rows, kind, minted);
|
|
1422
|
+
minted.add(id);
|
|
1423
|
+
return id;
|
|
1424
|
+
});
|
|
1425
|
+
const residentChanceIn = declaredCountOr(rows, dugKind, DEN_RESIDENT_CHANCE_PREDICATE, DEFAULT_DEN_RESIDENT_CHANCE_IN);
|
|
1426
|
+
const residentKind = isDen && stableIndex(`resident:${dug}`, residentChanceIn) === 0
|
|
1427
|
+
? factObjects(rows, dugKind, DEN_RESIDENT_PREDICATE)[0] ?? null
|
|
1428
|
+
: null;
|
|
1429
|
+
const resident = residentKind ? freshObjectId(rows, residentKind, minted) : null;
|
|
1430
|
+
return commit(
|
|
1431
|
+
[
|
|
1432
|
+
{ subject: dug, predicate: "rdf:type", object: "room" },
|
|
1433
|
+
{ subject: dug, predicate: "rdf:type", object: dugKind },
|
|
1434
|
+
...(isDen ? [{ subject: dug, predicate: "rdf:type", object: DEN_ROOM_CLASS }] : []),
|
|
1435
|
+
{ subject: here, predicate: `mgx:has-exit-${direction}`, object: dug },
|
|
1436
|
+
{ subject: dug, predicate: `mgx:has-exit-${back}`, object: here },
|
|
1437
|
+
// Typed to its OWN kind, not a flat "portable" — a spawned kind the
|
|
1438
|
+
// world declares rdfs:subClassOf food needs its real class reachable
|
|
1439
|
+
// here for isFood's own objectClassChain walk, or digging up "carrot-1"
|
|
1440
|
+
// would still read as inedible scenery. The class's own mass copies
|
|
1441
|
+
// onto the instance for the same reason: eat reads the instance.
|
|
1442
|
+
...spawnedKinds.flatMap((kind, i) => ([
|
|
1443
|
+
{ subject: spawned[i], predicate: "rdf:type", object: kind },
|
|
1444
|
+
{ subject: spawned[i], predicate: DISPLAY_NAME_PREDICATE, object: kind },
|
|
1445
|
+
{ subject: spawned[i], predicate: "mgx:located-in", object: dug },
|
|
1446
|
+
...classMassFacts(rows, spawned[i], kind),
|
|
1447
|
+
])),
|
|
1448
|
+
// A resident is placed with currently-in, the predicate that makes an
|
|
1449
|
+
// individual one of the cast, and knows about what its own den holds —
|
|
1450
|
+
// so an animal that digs one out has somebody new to ask about food.
|
|
1451
|
+
...(resident ? [
|
|
1452
|
+
{ subject: resident, predicate: "rdf:type", object: residentKind },
|
|
1453
|
+
{ subject: resident, predicate: DISPLAY_NAME_PREDICATE, object: residentKind },
|
|
1454
|
+
{ subject: resident, predicate: "mgx:currently-in", object: dug },
|
|
1455
|
+
...classMassFacts(rows, resident, residentKind),
|
|
1456
|
+
...spawned.map((thing) => ({ subject: resident, predicate: KNOWS_ABOUT_PREDICATE, object: thing })),
|
|
1457
|
+
] : []),
|
|
1458
|
+
],
|
|
1459
|
+
digNarration(direction, { isDen, spawned, resident }),
|
|
1460
|
+
`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}`,
|
|
1461
|
+
`dig ${direction} out of the ${here}`,
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
async function handleEatVerb(ctx) {
|
|
1466
|
+
const { rows, state, here, k, object, commit, noteFor, memoryDir, cache, actingSubject } = ctx;
|
|
1467
|
+
const present = visibleRoomOf(object, { rows, state }) === here || carriedBy(state, object, actingSubject);
|
|
1468
|
+
if (!present) {
|
|
1469
|
+
return answer(
|
|
1470
|
+
`I don't see a ${object} here.`,
|
|
1471
|
+
noteFor(`eat — ${object} is neither visible in the ${here} nor carried; declined`),
|
|
1472
|
+
{ miss: true },
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
if (!objectClassChain(rows, object).includes(FOOD_CLASS)) {
|
|
1476
|
+
return answer(
|
|
1477
|
+
`the ${object} isn't food.`,
|
|
1478
|
+
noteFor(`eat — ${object}'s rdf:type/rdfs:subClassOf chain never reaches "${FOOD_CLASS}"; declined by name`),
|
|
1479
|
+
{ miss: true },
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
const eaterMass = state.masses.get(actingSubject)?.value ?? null;
|
|
1483
|
+
if (eaterMass !== null && eaterMass >= ASSUMED_FULL_MASS * HUNGRY_FRACTION) {
|
|
1484
|
+
return answer(
|
|
1485
|
+
`you're too full to eat the ${object}.`,
|
|
1486
|
+
noteFor(`eat — ${actingSubject} weighs ${eaterMass}, at or over half of ${ASSUMED_FULL_MASS}; declined by name`),
|
|
1487
|
+
{ miss: true },
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
const gained = state.masses.get(object)?.value ?? DEFAULT_FOOD_MASS;
|
|
1491
|
+
const grown = Math.round(((eaterMass ?? 0) + gained) * 100) / 100;
|
|
1492
|
+
// Eating is the one act that ends a thing, so the eater is the one witness
|
|
1493
|
+
// whose knowledge of it goes out of date on the spot. Every route into the
|
|
1494
|
+
// eat verb — a typed command, a scripted mud turn — passes here, so the
|
|
1495
|
+
// claim gets written once for all of them.
|
|
1496
|
+
await recordGone(memoryDir, { observer: actingSubject, thing: object, k, cache });
|
|
1497
|
+
return commit(
|
|
1498
|
+
[
|
|
1499
|
+
{ subject: `${actingSubject}@turn${k}`, predicate: MASS_PREDICATE, object: String(grown) },
|
|
1500
|
+
{ subject: `${object}@turn${k}`, predicate: "mgx:located-in", object: CONSUMED_PLACE },
|
|
1501
|
+
],
|
|
1502
|
+
`you eat the ${object}. It adds ${gained} to your mass, so you weigh ${grown} now.`,
|
|
1503
|
+
`eat — the ${object}'s ${gained} mass moves onto ${actingSubject} (now ${grown}) and the ${object} leaves the world`,
|
|
1504
|
+
`eat the ${object}`,
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
async function handlePutVerb(ctx) {
|
|
1509
|
+
const { cmd, rows, state, here, k, family, object, commit, noteFor, actingSubject } = ctx;
|
|
1510
|
+
const container = cmd.indirectObject;
|
|
1511
|
+
if (!carriedBy(state, object, actingSubject)) {
|
|
1512
|
+
return answer(
|
|
1513
|
+
`you're not carrying the ${object}.`,
|
|
1514
|
+
noteFor(`put — ${object} isn't carried; precondition declined by name`),
|
|
1515
|
+
{ miss: true },
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
if (visibleRoomOf(container, { rows, state }) !== here) {
|
|
1519
|
+
return answer(
|
|
1520
|
+
`I don't see a ${container} here.`,
|
|
1521
|
+
noteFor(`put — ${container} isn't visible in the ${here}; declined`),
|
|
1522
|
+
{ miss: true },
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
if (!isContainer(rows, container)) {
|
|
1526
|
+
return answer(
|
|
1527
|
+
`the ${container} doesn't hold things.`,
|
|
1528
|
+
noteFor(`put — no mgx:is-container fact on ${container}; declined by name`),
|
|
1529
|
+
{ miss: true },
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
if (!state.openness.get(container)?.open) {
|
|
1533
|
+
return answer(
|
|
1534
|
+
`the ${container} is closed.`,
|
|
1535
|
+
noteFor(`put — the ${container} isn't open; precondition declined by name`),
|
|
1536
|
+
{ miss: true },
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
return commit(
|
|
1540
|
+
[{ subject: `${object}@turn${k}`, predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: container }],
|
|
1541
|
+
`you put the ${object} in the ${container}.`,
|
|
1542
|
+
`put — the taught "put" family fired; the ${object} now sits in the ${container}`,
|
|
1543
|
+
`put the ${object} in the ${container}`,
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// open / unlock / close — the container verbs, sharing one handler (all
|
|
1548
|
+
// three keys in STATE_VERB_HANDLERS below point here) because they share a
|
|
1549
|
+
// precondition: presence and container-ness stay hand-checked (visibility
|
|
1550
|
+
// gating, not a state precondition), the same visibleRoomOf check
|
|
1551
|
+
// examine/talk/take already use, not a hand-rolled duplicate. unlock's
|
|
1552
|
+
// instrument match stays fully hand-written below it too — it needs a
|
|
1553
|
+
// third, externally-supplied binding beyond subject/target, which this
|
|
1554
|
+
// retrofit does not attempt. open/close's lock-state and open/closed
|
|
1555
|
+
// checks, and their mgx:is-open write, are taught "fact-value" precond/
|
|
1556
|
+
// effect rows consulted through domain.mjs; only the hidden-contents
|
|
1557
|
+
// reveal (a variable-arity effect over a discovered set) stays hand-written
|
|
1558
|
+
// JS, since no shipped rule shape covers that either.
|
|
1559
|
+
async function handleContainerVerb(ctx) {
|
|
1560
|
+
const { cmd, rows, state, here, k, object, place, commit, noteFor, ruleRows } = ctx;
|
|
1561
|
+
if (visibleRoomOf(object, { rows, state }) !== here) {
|
|
1562
|
+
return answer(`I don't see a ${object} here.`, noteFor(`${cmd.verb} — ${object} isn't in the ${here}; declined`), { miss: true });
|
|
1563
|
+
}
|
|
1564
|
+
if (!isContainer(rows, object)) {
|
|
1565
|
+
return answer(`the ${object} doesn't open.`, noteFor(`${cmd.verb} — no mgx:is-container fact on ${object}; declined by name`), { miss: true });
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (cmd.verb === "open" || cmd.verb === "close") {
|
|
1569
|
+
const domain = compileDomain(rows, ruleRows);
|
|
1570
|
+
const taughtAction = domain.actions.find((a) => a.name === cmd.verb);
|
|
1571
|
+
const effect = taughtAction?.effects.find((e) => e.predicate === OPEN_PREDICATE);
|
|
1572
|
+
if (!effect) {
|
|
1573
|
+
return answer(
|
|
1574
|
+
`this world doesn't teach how ${cmd.verb === "open" ? "opening" : "closing"} changes the ${object}.`,
|
|
1575
|
+
noteFor(`${cmd.verb} — the taught "${cmd.verb}" family carries no ${OPEN_PREDICATE} effect; honest decline`),
|
|
1576
|
+
{ miss: true },
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
const factState = containerDatatypeState(state, object);
|
|
1580
|
+
const failed = taughtAction.preconds.find((p) => !precondHolds(p, ctx.actingSubject, object, factState, domain));
|
|
1581
|
+
if (failed) {
|
|
1582
|
+
const text = failed.predicate === "mgx:stands-locked-in"
|
|
1583
|
+
? `the ${object} is locked.`
|
|
1584
|
+
: cmd.verb === "open" ? `the ${object} is already open.` : `the ${object} isn't open.`;
|
|
1585
|
+
return answer(text, noteFor(`${cmd.verb} — the taught "${cmd.verb}" family's ${failed.predicate} precondition declined by name`), { miss: true });
|
|
1586
|
+
}
|
|
1587
|
+
const effSubject = roleBinding(effect.subjectRole, ctx.actingSubject, object, domain);
|
|
1588
|
+
const writeIsOpen = { subject: `${effSubject}@turn${k}`, predicate: effect.predicate, object: effect.value };
|
|
1589
|
+
|
|
1590
|
+
if (cmd.verb === "open") {
|
|
1591
|
+
const revealed = [...state.placements]
|
|
1592
|
+
.filter(([, p]) => p.predicate === "mgx:hidden-in" && p.object === object)
|
|
1593
|
+
.map(([thing]) => thing)
|
|
1594
|
+
.sort();
|
|
1595
|
+
return commit(
|
|
1596
|
+
[
|
|
1597
|
+
writeIsOpen,
|
|
1598
|
+
...revealed.map((thing) => ({ subject: `${thing}@turn${k}`, predicate: "mgx:located-in", object })),
|
|
1599
|
+
],
|
|
1600
|
+
revealed.length
|
|
1601
|
+
? `you open the ${object} — inside: the ${revealed.join(", the ")}.`
|
|
1602
|
+
: `you open the ${object}. It's empty.`,
|
|
1603
|
+
`open — ${object} opens${revealed.length ? `, revealing ${revealed.join(", ")}` : ""} via the taught "open" family's effect`,
|
|
1604
|
+
`open the ${object}`,
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
return commit(
|
|
1608
|
+
[writeIsOpen],
|
|
1609
|
+
`you close the ${object}.`,
|
|
1610
|
+
`close — ${object} closes via the taught "close" family's effect`,
|
|
1611
|
+
`close the ${object}`,
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// unlock
|
|
1616
|
+
if (place.predicate !== "mgx:stands-locked-in") {
|
|
1617
|
+
return answer(`the ${object} isn't locked.`, noteFor("unlock — not locked; declined"), { miss: true });
|
|
1618
|
+
}
|
|
1619
|
+
if (!cmd.instrument) {
|
|
1620
|
+
return answer(
|
|
1621
|
+
`unlock the ${object} with what? Name the thing to use, e.g. "unlock the ${object} with the key".`,
|
|
1622
|
+
noteFor("unlock — no instrument named; asked, never guessed"),
|
|
1623
|
+
{ miss: true },
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
const required = factObjects(rows, object, "mgx:unlocks-with")[0] ?? null;
|
|
1627
|
+
if (!required) {
|
|
1628
|
+
return answer(
|
|
1629
|
+
`nothing in this world says what unlocks the ${object}.`,
|
|
1630
|
+
noteFor(`unlock — no mgx:unlocks-with fact on ${object}; honest decline`),
|
|
1631
|
+
{ miss: true },
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
if (!carriedBy(state, cmd.instrument, ctx.actingSubject)) {
|
|
1635
|
+
return answer(
|
|
1636
|
+
`you're not carrying the ${cmd.instrument}.`,
|
|
1637
|
+
noteFor(`unlock — the ${cmd.instrument} isn't carried; precondition declined by name`),
|
|
1638
|
+
{ miss: true },
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
if (cmd.instrument !== required) {
|
|
1642
|
+
return answer(
|
|
1643
|
+
`the ${cmd.instrument} doesn't fit the ${object}'s lock.`,
|
|
1644
|
+
noteFor(`unlock — the taught instrument is ${required}, not ${cmd.instrument}; declined by name`),
|
|
1645
|
+
{ miss: true },
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
return commit(
|
|
1649
|
+
[{ subject: `${object}@turn${k}`, predicate: "mgx:fixed-in", object: here }],
|
|
1650
|
+
`you unlock the ${object} with the ${required}.`,
|
|
1651
|
+
`unlock — the lock releases; ${object} now stands unlocked (still fixed) in the ${here}`,
|
|
1652
|
+
`unlock the ${object}`,
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// The data-driven verb table itself: every state-changing verb this engine
|
|
1657
|
+
// knows, mapped to the handler that runs it. A world's own taught action
|
|
1658
|
+
// family (checked before this table is ever consulted) decides whether the
|
|
1659
|
+
// verb exists in THIS world; this table decides which JS runs once it does.
|
|
1660
|
+
const STATE_VERB_HANDLERS = {
|
|
1661
|
+
go: handleGoVerb,
|
|
1662
|
+
take: handleTakeVerb,
|
|
1663
|
+
drop: handleDropOrGiveVerb,
|
|
1664
|
+
give: handleDropOrGiveVerb,
|
|
1665
|
+
dig: handleDigVerb,
|
|
1666
|
+
eat: handleEatVerb,
|
|
1667
|
+
put: handlePutVerb,
|
|
1668
|
+
open: handleContainerVerb,
|
|
1669
|
+
close: handleContainerVerb,
|
|
1670
|
+
unlock: handleContainerVerb,
|
|
1671
|
+
};
|
|
1672
|
+
|
|
1242
1673
|
export async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache, actingSubject = "player" }) {
|
|
1243
1674
|
const memory = await loadMemory(memoryDir);
|
|
1244
1675
|
const rows = readFactRows(memory);
|
|
@@ -1410,376 +1841,18 @@ export async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache
|
|
|
1410
1841
|
const object = cmd.object;
|
|
1411
1842
|
const place = object ? state.placements.get(object) ?? null : null;
|
|
1412
1843
|
|
|
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)) {
|
|
1844
|
+
const handler = STATE_VERB_HANDLERS[cmd.verb];
|
|
1845
|
+
if (!handler) {
|
|
1764
1846
|
return answer(
|
|
1765
|
-
`
|
|
1766
|
-
noteFor(
|
|
1847
|
+
`this world doesn't know how to run the verb "${cmd.verb}" yet.`,
|
|
1848
|
+
noteFor(`${cmd.verb} — a taught family exists but no handler is wired for it; honest decline`),
|
|
1767
1849
|
{ miss: true },
|
|
1768
1850
|
);
|
|
1769
1851
|
}
|
|
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
|
-
);
|
|
1852
|
+
return handler({
|
|
1853
|
+
cmd, rows, state, here, k, family, families, ruleRows, object, place,
|
|
1854
|
+
commit, noteFor, actingSubject, memoryDir, world, cache, graph, memory,
|
|
1855
|
+
});
|
|
1783
1856
|
}
|
|
1784
1857
|
|
|
1785
1858
|
// A mid-game "where is X" aside. Optional trailing "now": the question means
|