@polycode-projects/the-mechanical-code-talker 4.1.8 → 5.0.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 +1 -1
- package/data/mudiii-assets.json +201 -0
- package/package.json +4 -1
- package/src/domain/agent-belief.mjs +96 -0
- package/src/domain/answer-variants.json +1 -1
- package/src/domain/game-config.mjs +65 -0
- package/src/services/adventure-editor.mjs +36 -0
- package/src/services/adventure.mjs +45 -15
- package/src/services/chat.mjs +1 -1
- package/src/services/mud-editor.mjs +53 -0
- package/src/services/pill-complete.mjs +495 -0
- package/src/services/spider-fly.mjs +14 -64
- package/src/services/world-teach.mjs +214 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +18 -3
- package/src/surfaces/web/memory-ask-browser.bundle.js +105 -100
|
@@ -4,16 +4,21 @@
|
|
|
4
4
|
// rows through the shared memory store. No chat, no rendering — a later
|
|
5
5
|
// piece of work wraps runSpiderFlyTick's return shape for a chat turn.
|
|
6
6
|
//
|
|
7
|
-
// Grid geometry (cellId, parseCellId,
|
|
7
|
+
// Grid geometry (cellId, parseCellId, chebyshevDistance, isInWebBlock,
|
|
8
8
|
// perimeterCells, DIRECTION_DELTA) is never redefined here — it all comes
|
|
9
9
|
// from spider-fly-world.mjs, the one source of truth both the shipped world
|
|
10
|
-
// pack and this engine read from.
|
|
10
|
+
// pack and this engine read from. Vision-gated belief is the same deal: it
|
|
11
|
+
// lives in the board-size-agnostic domain/agent-belief.mjs and is re-exported
|
|
12
|
+
// below, so this file's public surface is unchanged by where it sits.
|
|
11
13
|
|
|
12
14
|
import {
|
|
13
15
|
WORLD_NAME, WEB_HOME, WEB_DURATION_TURNS, SPIDER_INITIAL_MASS, SPIDER_MASS_DECREMENT_PER_TURN,
|
|
14
|
-
cellId, parseCellId, chebyshevDistance,
|
|
16
|
+
cellId, parseCellId, chebyshevDistance, isInWebBlock, perimeterCells,
|
|
15
17
|
DIRECTION_DELTA, oneStepDirectionBetween,
|
|
16
18
|
} from "../domain/spider-fly-world.mjs";
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_VISION_RADIUS, believedCellOf, nearestBelievedTarget, beliefSnapshotFor,
|
|
21
|
+
} from "../domain/agent-belief.mjs";
|
|
17
22
|
import { findActionPath, findReachableSet } from "../domain/planning.mjs";
|
|
18
23
|
import { appendFacts, loadMemory, readFactRows } from "../adapters/memory/core.mjs";
|
|
19
24
|
import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
|
|
@@ -24,7 +29,6 @@ import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
|
24
29
|
// ---- tunable constants (starting values, not fixed — the vision radius and
|
|
25
30
|
// mass economy all want checking against a real playable board) -------------
|
|
26
31
|
|
|
27
|
-
export const DEFAULT_VISION_RADIUS = 4;
|
|
28
32
|
export const FLY_INITIAL_MASS = 10;
|
|
29
33
|
export const FLY_MASS_DECREMENT_PER_TURN = 1;
|
|
30
34
|
export const EGG_HATCH_DELAY_TURNS = 3;
|
|
@@ -33,6 +37,7 @@ export const EGG_LAY_MASS_THRESHOLD = 25;
|
|
|
33
37
|
export const EGG_HATCH_COUNT = 2;
|
|
34
38
|
export const MIN_HATCHLING_MASS = 3;
|
|
35
39
|
export { SPIDER_INITIAL_MASS, SPIDER_MASS_DECREMENT_PER_TURN, WEB_DURATION_TURNS };
|
|
40
|
+
export { DEFAULT_VISION_RADIUS, believedCellOf, nearestBelievedTarget, beliefSnapshotFor };
|
|
36
41
|
|
|
37
42
|
// ---- seeded "randomness" (never Math.random) ---------------------------------
|
|
38
43
|
// Every "random" decision (fly wander, fly/spawn placement) is a mulberry32
|
|
@@ -316,45 +321,11 @@ export function greedySpiderAvoid(spiderCell, believedOtherSpiderCell, applyActi
|
|
|
316
321
|
}
|
|
317
322
|
|
|
318
323
|
// ---- visibility and belief (§4): static grid topology is common knowledge
|
|
319
|
-
// to both agents; only dynamic entity positions are gated by vision
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
/** Whether `observerSubject` currently believes `targetSubject` to be at a
|
|
326
|
-
* particular cell: ground truth when the target's real cell is within the
|
|
327
|
-
* observer's own visibleCells radius, else the newest told fact addressed
|
|
328
|
-
* to this observer about this target, else null (unknown). A removed
|
|
329
|
-
* target (eaten/starved/hatched) is never believed present. */
|
|
330
|
-
export function believedCellOf(targetSubject, observerSubject, observerCell, state, opts = {}) {
|
|
331
|
-
const { visionRadius = DEFAULT_VISION_RADIUS, toldFacts = [] } = opts;
|
|
332
|
-
const place = state.placements.get(targetSubject);
|
|
333
|
-
if (place && !state.removed.has(targetSubject)) {
|
|
334
|
-
const seen = visibleCells(observerCell.x, observerCell.y, visionRadius);
|
|
335
|
-
if (seen.includes(place.cell)) return parseCellId(place.cell);
|
|
336
|
-
}
|
|
337
|
-
const told = toldFacts
|
|
338
|
-
.filter((f) => f.toAgent === observerSubject && f.subject === targetSubject)
|
|
339
|
-
.sort((a, b) => (b.turn ?? 0) - (a.turn ?? 0))[0];
|
|
340
|
-
return told ? parseCellId(told.cell) : null;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/** The nearest candidate (by believed Chebyshev distance) an observer has
|
|
344
|
-
* any belief about at all — null when the observer believes nothing about
|
|
345
|
-
* any candidate. `candidates` must already be in a deterministic order
|
|
346
|
-
* (ties favor the earlier candidate). */
|
|
347
|
-
export function nearestBelievedTarget(observerSubject, observerCell, candidates, state, opts = {}) {
|
|
348
|
-
let best = null;
|
|
349
|
-
let bestDist = Infinity;
|
|
350
|
-
for (const subject of candidates) {
|
|
351
|
-
const cell = believedCellOf(subject, observerSubject, observerCell, state, opts);
|
|
352
|
-
if (!cell) continue;
|
|
353
|
-
const dist = chebyshevDistance(observerCell.x, observerCell.y, cell.x, cell.y);
|
|
354
|
-
if (dist < bestDist) { bestDist = dist; best = { subject, cell }; }
|
|
355
|
-
}
|
|
356
|
-
return best;
|
|
357
|
-
}
|
|
324
|
+
// to both agents; only dynamic entity positions are gated by vision, in
|
|
325
|
+
// domain/agent-belief.mjs (imported and re-exported above). A told fact —
|
|
326
|
+
// shaped { subject, toAgent, cell, turn } — is the chat-integration channel
|
|
327
|
+
// into it, defaulting to empty so belief with no chat is exactly "what's
|
|
328
|
+
// currently visible." --------------------------------------------------------
|
|
358
329
|
|
|
359
330
|
// ---- the ecology pass (§10): catch, eat, lay, hatch, spawn, starve, all as
|
|
360
331
|
// ordinary turn-gated checks in one fixed-order pass. Order matters and is
|
|
@@ -656,27 +627,6 @@ function stepPlan(fromCell, toCell) {
|
|
|
656
627
|
return direction ? [direction] : [];
|
|
657
628
|
}
|
|
658
629
|
|
|
659
|
-
/** A full "world knowledge graph" snapshot for one observer this tick: every
|
|
660
|
-
* OTHER named candidate's believed cell (believedCellOf — ground truth
|
|
661
|
-
* inside vision, else the newest told fact addressed to this observer,
|
|
662
|
-
* else null for "unknown") — feeds a per-agent belief panel. Deliberately
|
|
663
|
-
* never ground truth: showing the observer's own honest gap
|
|
664
|
-
* between belief and reality (visibly widened by a deceiving pill or a fed
|
|
665
|
-
* false fact) is the whole point of that panel. Returns a plain
|
|
666
|
-
* `{ [candidateId]: cellId | null }` map. Exported as the read path for
|
|
667
|
-
* what one agent can currently observe — every caller (this file's own
|
|
668
|
-
* tick loop, a viz panel, a future chat lane) reads the same computation,
|
|
669
|
-
* never a re-derived copy of it. */
|
|
670
|
-
export function beliefSnapshotFor(observerSubject, observerCell, candidateIds, state, opts) {
|
|
671
|
-
const belief = {};
|
|
672
|
-
for (const candidateId of candidateIds) {
|
|
673
|
-
if (candidateId === observerSubject) continue;
|
|
674
|
-
const cell = believedCellOf(candidateId, observerSubject, observerCell, state, opts);
|
|
675
|
-
belief[candidateId] = cell ? cellId(cell.x, cell.y) : null;
|
|
676
|
-
}
|
|
677
|
-
return belief;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
630
|
/** Live (unexpired, by `turn`) dynamic webs from a `Map(webId -> {cell,
|
|
681
631
|
* builtAtTurn})` (either a folded state's own `.webs`, or that widened with
|
|
682
632
|
* web(s) minted THIS tick before they've been written/read back), as a
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// world-teach.mjs — a declarative sentence read as a fact against the LIVE
|
|
2
|
+
// world, and written into it on the spot. This is the engine half of the
|
|
3
|
+
// adventure/mud pages' "teach" switch: with it off nothing here runs and the
|
|
4
|
+
// lane behaves exactly as it always has; with it on, a sentence the surface's
|
|
5
|
+
// own editor grammar can express becomes a world fact this turn.
|
|
6
|
+
//
|
|
7
|
+
// It owns the gates, the id minting, the snapshot stamping and the decline
|
|
8
|
+
// wording. It owns NO grammar. The sentence table belongs to the surface —
|
|
9
|
+
// adventure-editor.mjs for a manor, mud-editor.mjs for a burrow, which say
|
|
10
|
+
// placement with different words — so the parser arrives as `parseLine` and
|
|
11
|
+
// this module never learns either vocabulary. Both tables are the inverse of
|
|
12
|
+
// their own page's right-hand "the world's own account" renderer, so every
|
|
13
|
+
// sentence a page has already SHOWN somebody is a sentence they can type back.
|
|
14
|
+
//
|
|
15
|
+
// This is deliberately NOT chat.mjs's teachLane, and does not widen it. A
|
|
16
|
+
// chat teach carries `teach:chat:*` provenance, which worldActionRows filters
|
|
17
|
+
// out of the playable fold on purpose: a locative note somebody made mid-game
|
|
18
|
+
// must never silently move a prop. A world teach is a different act — the
|
|
19
|
+
// player asserting what the world IS — so it carries its own
|
|
20
|
+
// `world:<name>:taught:turnK` provenance, passes that same filter by the
|
|
21
|
+
// `world:` prefix every played turn already uses, and scores on the world's
|
|
22
|
+
// own Source with no change to the trust model. The two stay apart.
|
|
23
|
+
//
|
|
24
|
+
// Semantics are general on purpose. Anything the table can say becomes a
|
|
25
|
+
// fact: a new thing, a world-authored one moved, a locked cabinet opened, the
|
|
26
|
+
// player put somewhere else. The fold has no invariant layer, so a
|
|
27
|
+
// contradicting row does not conflict — the newer write simply wins, exactly
|
|
28
|
+
// as a played turn's does.
|
|
29
|
+
//
|
|
30
|
+
// One consequence worth stating plainly: a teach spends a turn number (every
|
|
31
|
+
// fold-versioned write is stamped `@turnK`, or it would rank as turn 0 and
|
|
32
|
+
// lose to anything already played) but does NOT run the NPC pass. Nobody in
|
|
33
|
+
// the cast moves in response to a sentence. That is the same trade the
|
|
34
|
+
// page's own edit mode already makes.
|
|
35
|
+
import { appendFacts, loadMemory, readFactRows } from "../adapters/memory/core.mjs";
|
|
36
|
+
import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
|
|
37
|
+
import { correctMisspellings } from "../domain/interpret/normalize.mjs";
|
|
38
|
+
import {
|
|
39
|
+
classMassFacts, foldWorldState, freshObjectId, snapshotSubject, worldActionRows, worldRelook,
|
|
40
|
+
} from "./adventure.mjs";
|
|
41
|
+
|
|
42
|
+
// A trailing "?" is an unambiguous "this is a question", and a question must
|
|
43
|
+
// never reach a write boundary. A leading interrogative is the same signal one
|
|
44
|
+
// word earlier, run over the closed misspelling repair so a typo'd "wat is the
|
|
45
|
+
// lamp" goes back to the question side rather than reading as a declarative.
|
|
46
|
+
// Both mirror chat.mjs's own teach lane, which stands the whole lane down on
|
|
47
|
+
// either — a world teach has exactly the same reason to.
|
|
48
|
+
const QUESTION_LEAD_RE = /^(?:what|who|which|where|when|why|how|is|are|do|does|did|can|could|should|would|will|has|have)\b/i;
|
|
49
|
+
|
|
50
|
+
// A subject with no referent of its own. The generic "X is a Y." fallback in
|
|
51
|
+
// both sentence tables would otherwise read "There is a book in the study" as
|
|
52
|
+
// the claim that `there` is a `book in the study` — a real fact, about
|
|
53
|
+
// nothing. These decline rather than fall through: the sentence is plainly
|
|
54
|
+
// meant as a teach, and quietly handing it to the ordinary lanes to answer as
|
|
55
|
+
// conversation is the guess this product doesn't make.
|
|
56
|
+
const EMPTY_SUBJECTS = new Set(["it", "they", "them", "there", "this", "that", "these", "those", "he", "she"]);
|
|
57
|
+
|
|
58
|
+
const PLACEMENT_PREDICATES = new Set([
|
|
59
|
+
"mgx:currently-in", "mgx:located-in", "mgx:fixed-in", "mgx:stands-locked-in", "mgx:hidden-in",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const SNAPSHOT_RE = /^(.+?)@(?:epoch(\d+)@)?turn(\d+)$/;
|
|
63
|
+
|
|
64
|
+
const decline = (text, note) => ({ text, lane: "game-answer", note: `ADVENTURE — world-teach: ${note}`, miss: true });
|
|
65
|
+
|
|
66
|
+
/** Which subject is which class, for the one genuinely ambiguous placement
|
|
67
|
+
* sentence ("X is in the Y." reads as a person standing or a prop resting,
|
|
68
|
+
* depending on X). The adventure table takes this as its second argument;
|
|
69
|
+
* the burrow table says the two with different words and ignores it. */
|
|
70
|
+
function typesFromRows(rows) {
|
|
71
|
+
const types = new Map();
|
|
72
|
+
for (const row of rows || []) {
|
|
73
|
+
if (row?.predicate === "rdf:type" && row.subject && row.object) {
|
|
74
|
+
types.set(String(row.subject).trim().toLowerCase(), String(row.object).trim().toLowerCase());
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return types;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const roomsOf = (rows) => [...new Set((rows || [])
|
|
81
|
+
.filter((r) => r.predicate === "rdf:type" && r.object === "room" && !SNAPSHOT_RE.test(r.subject))
|
|
82
|
+
.map((r) => r.subject))].sort();
|
|
83
|
+
|
|
84
|
+
/** Every name the world already resolves — anything typed, anything placed,
|
|
85
|
+
* and either end of an exit. A subject already on this list is moved or
|
|
86
|
+
* re-described by a teach; one that isn't is minted. */
|
|
87
|
+
function knownNames(rows, state) {
|
|
88
|
+
const names = new Set();
|
|
89
|
+
for (const row of rows || []) {
|
|
90
|
+
if (SNAPSHOT_RE.test(row.subject)) continue;
|
|
91
|
+
if (row.predicate === "rdf:type" || PLACEMENT_PREDICATES.has(row.predicate)) names.add(row.subject);
|
|
92
|
+
if (/^mgx:has-exit-[a-z]+$/.test(row.predicate)) { names.add(row.subject); names.add(row.object); }
|
|
93
|
+
}
|
|
94
|
+
for (const subject of state?.placements?.keys() ?? []) names.add(subject);
|
|
95
|
+
return names;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** An id nothing in the world answers to yet. The plain noun wherever it is
|
|
99
|
+
* free, so "Candle is in the study" leaves a thing called `candle` and the
|
|
100
|
+
* pill rail offers "take candle" on the very next redraw — the world's own
|
|
101
|
+
* class-default minting picks ids the same way. A collision falls back to
|
|
102
|
+
* the numbered id every mid-play spawn already uses. */
|
|
103
|
+
function mintedIdFor(rows, noun) {
|
|
104
|
+
const taken = (rows || []).some((r) => r.subject === noun || r.object === noun);
|
|
105
|
+
return taken ? freshObjectId(rows, noun) : noun;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function confirmation(triple, { thing, minted, rooms }) {
|
|
109
|
+
if (triple.predicate === "mgx:is-open") {
|
|
110
|
+
return `noted — the ${thing} is ${triple.object === "true" ? "open" : "closed"} now.`;
|
|
111
|
+
}
|
|
112
|
+
if (PLACEMENT_PREDICATES.has(triple.predicate)) {
|
|
113
|
+
const where = rooms.includes(triple.object) ? `in the ${triple.object}` : `with the ${triple.object}`;
|
|
114
|
+
return minted ? `noted — there's a ${thing} ${where} now.` : `noted — the ${thing} is ${where} now.`;
|
|
115
|
+
}
|
|
116
|
+
if (triple.predicate === "rdf:type") return `noted — the ${thing} is a ${triple.object} now.`;
|
|
117
|
+
return `noted — the world says that now.`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* One line read as a fact about the live world, or null when it is not a
|
|
122
|
+
* teach sentence at all and the ordinary lane should have it. `parseLine` is
|
|
123
|
+
* the surface's own editor parser (`parseEditorLine` / `parseMudEditorLine`);
|
|
124
|
+
* `planTriple` is that same editor's additive planner. `rows`/`state` are the
|
|
125
|
+
* caller's already-loaded fact rows and fold, so this never re-reads the
|
|
126
|
+
* store to decide.
|
|
127
|
+
*
|
|
128
|
+
* Returns the ordinary lane answer shape — `{ text, lane, note }`, plus
|
|
129
|
+
* `miss: true` on every decline so the transcript styles it like any other
|
|
130
|
+
* honest refusal, and `taught` (the rows actually written) on a success.
|
|
131
|
+
*/
|
|
132
|
+
export async function worldTeachTurn(line, {
|
|
133
|
+
parseLine, planTriple, rows, state, memoryDir, world, actingSubject = "player", cache = null, graph = null,
|
|
134
|
+
}) {
|
|
135
|
+
const trimmed = String(line || "").trim();
|
|
136
|
+
if (!trimmed || !parseLine || !planTriple || !memoryDir) return null;
|
|
137
|
+
if (/\?\s*$/.test(trimmed)) return null;
|
|
138
|
+
if (QUESTION_LEAD_RE.test(correctMisspellings(trimmed))) return null;
|
|
139
|
+
|
|
140
|
+
const triple = parseLine(trimmed, typesFromRows(rows));
|
|
141
|
+
if (!triple) return null;
|
|
142
|
+
|
|
143
|
+
const rooms = roomsOf(rows);
|
|
144
|
+
if (EMPTY_SUBJECTS.has(triple.subject)) {
|
|
145
|
+
return decline(
|
|
146
|
+
`"${triple.subject}" doesn't name anything I can write a fact about — name the thing itself, like "candle is in the study".`,
|
|
147
|
+
`"${trimmed}" parses with "${triple.subject}" as its subject, which refers to nothing this world holds; declined rather than stored against a pronoun`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (triple.kind === "placement" && !rooms.includes(triple.object) && !knownNames(rows, state).has(triple.object)) {
|
|
151
|
+
return decline(
|
|
152
|
+
`I don't know a place called "${triple.object}" — this world has: ${rooms.join(", ")}.`,
|
|
153
|
+
`"${trimmed}" places ${triple.subject} in "${triple.object}", which names no room and nothing else the world holds; declined by name, alternatives listed`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const { toAppend, reason } = planTriple(rows, state, triple);
|
|
158
|
+
const known = knownNames(rows, state);
|
|
159
|
+
const minting = triple.kind === "placement" && !known.has(triple.subject);
|
|
160
|
+
// Re-asserting something the world already holds is true, so it is not a
|
|
161
|
+
// miss — but it writes nothing, and saying so is the whole answer.
|
|
162
|
+
if (!toAppend.length && !minting) {
|
|
163
|
+
const room = state?.placements?.get(actingSubject)?.object ?? null;
|
|
164
|
+
return {
|
|
165
|
+
text: `the world already said that.${room ? ` ${await worldRelook(room, { memoryDir, graph, actingSubject })}` : ""}`,
|
|
166
|
+
lane: "game-answer",
|
|
167
|
+
miss: false,
|
|
168
|
+
note: `ADVENTURE — world-teach: "${trimmed}" asserts a fact the world already holds (${reason}); nothing written`,
|
|
169
|
+
taught: [],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const k = (state?.turnCount ?? 0) + 1;
|
|
174
|
+
const epoch = state?.epoch ?? 0;
|
|
175
|
+
const id = minting ? mintedIdFor(rows, triple.subject) : triple.subject;
|
|
176
|
+
// Only the fold-versioned families are stamped. foldWorldState ranks an
|
|
177
|
+
// unstamped row as turn 0, so an unstamped placement would silently lose to
|
|
178
|
+
// anything already played about the same thing. A type/exit/flag row is read
|
|
179
|
+
// raw by every reader and must keep its bare subject — a stamped one names a
|
|
180
|
+
// subject no verb resolves.
|
|
181
|
+
const stamp = (t) => (t.kind === "other"
|
|
182
|
+
? { subject: id, predicate: t.predicate, object: t.object }
|
|
183
|
+
: { subject: snapshotSubject(id, k, epoch), predicate: t.predicate, object: t.object });
|
|
184
|
+
const facts = [
|
|
185
|
+
// A minted thing arrives with the same rows the world's own class-default
|
|
186
|
+
// and dig spawns write: its own class (which resolves its sprite), the
|
|
187
|
+
// portable class (which is what makes the take verb accept it), a plain
|
|
188
|
+
// reading name, and whatever mass its class declares. A class with no
|
|
189
|
+
// sprite of its own falls back to the parcel icon already drawn for it.
|
|
190
|
+
...(minting ? [
|
|
191
|
+
{ subject: id, predicate: "rdf:type", object: triple.subject },
|
|
192
|
+
{ subject: id, predicate: "rdf:type", object: "portable" },
|
|
193
|
+
{ subject: id, predicate: "mgx:display-name", object: triple.subject },
|
|
194
|
+
...classMassFacts(rows, id, triple.subject),
|
|
195
|
+
] : []),
|
|
196
|
+
...toAppend.map(stamp),
|
|
197
|
+
];
|
|
198
|
+
const provenance = `${worldProvenanceTag(world)}:taught:turn${k}`;
|
|
199
|
+
await appendFacts(memoryDir, facts.map((f) => ({ ...f, provenance })));
|
|
200
|
+
if (cache) cache.rows = null;
|
|
201
|
+
|
|
202
|
+
const freshState = foldWorldState(worldActionRows(readFactRows(await loadMemory(memoryDir))));
|
|
203
|
+
const here = freshState.placements.get(actingSubject)?.object ?? null;
|
|
204
|
+
const said = confirmation(triple, { thing: triple.subject, minted: minting, rooms });
|
|
205
|
+
const relook = here ? ` ${await worldRelook(here, { memoryDir, graph, actingSubject })}` : "";
|
|
206
|
+
return {
|
|
207
|
+
text: `${said}${relook}`,
|
|
208
|
+
lane: "game-answer",
|
|
209
|
+
miss: false,
|
|
210
|
+
goal: minting ? `put a ${triple.subject} in the world` : `change what the world says about the ${triple.subject}`,
|
|
211
|
+
note: `ADVENTURE — world-teach: ${reason}; ${minting ? `minted ${id} and ` : ""}wrote ${facts.length} row(s) at turn ${k} with provenance ${provenance}; no NPC pass rides a taught fact${here ? `; auto-relook appended for the ${here}` : ""}`,
|
|
212
|
+
taught: facts,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
44
44
|
import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
|
|
45
45
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
46
|
-
import { foldWorldState, worldDigestRows, roomAffordances, worldActionRows } from "../../services/adventure.mjs";
|
|
46
|
+
import { foldWorldState, worldDigestRows, roomAffordances, worldActionRows, snapshotSubject } from "../../services/adventure.mjs";
|
|
47
47
|
import { runAdventureAutoplayTick, exposedFacts } from "../../services/adventure-autoplay.mjs";
|
|
48
48
|
import { parseWorldEditorText, planWorldEditorSync } from "../../services/adventure-editor.mjs";
|
|
49
49
|
import { resolveSpriteForClass, SPRITE_REGISTRY, classAncestorChain } from "../../domain/sprite-map.mjs";
|
|
@@ -194,15 +194,30 @@ export async function createAdventureSession(worldPayload, { restoredPayload = n
|
|
|
194
194
|
* fact as "not in this text" and try to retract it). Retractions
|
|
195
195
|
* (removeFacts) only ever run when the WHOLE document parsed cleanly —
|
|
196
196
|
* see adventure-editor.mjs's own header for why a typo must never be
|
|
197
|
-
* read as "this fact is gone". Returns `{ unrecognized, added, removed }`.
|
|
197
|
+
* read as "this fact is gone". Returns `{ unrecognized, added, removed }`.
|
|
198
|
+
*
|
|
199
|
+
* A placement or openness edit is written as a TURN SNAPSHOT, the way
|
|
200
|
+
* every played turn writes one. foldWorldState ranks an unstamped row as
|
|
201
|
+
* turn 0, so an edit to something already played — the lamp taken on
|
|
202
|
+
* turn 4, then moved back to the desk in the editor — would be outranked
|
|
203
|
+
* by the very move it was meant to correct, and the edit would land in
|
|
204
|
+
* the store and change nothing anyone can see. Type/exit/puzzle rows are
|
|
205
|
+
* read raw and keep their bare subject: a stamped one names a subject no
|
|
206
|
+
* verb resolves. */
|
|
198
207
|
async applyEdit(text) {
|
|
199
208
|
const allRows = readFactRows(await loadMemory(memoryDir));
|
|
200
209
|
const worldRows = allRows.filter((r) => typeof r.provenance === "string" && r.provenance.indexOf(tag) === 0);
|
|
201
210
|
const state = foldWorldState(worldRows);
|
|
202
211
|
const { triples, unrecognized } = parseWorldEditorText(text, worldRows);
|
|
203
212
|
const { toAppend, toRemoveIds } = planWorldEditorSync(worldRows, state, triples);
|
|
213
|
+
const editTurn = state.turnCount + 1;
|
|
204
214
|
if (toAppend.length) {
|
|
205
|
-
await appendFacts(memoryDir, toAppend.map((f) => ({
|
|
215
|
+
await appendFacts(memoryDir, toAppend.map((f) => ({
|
|
216
|
+
subject: f.kind === "other" ? f.subject : snapshotSubject(f.subject, editTurn, state.epoch),
|
|
217
|
+
predicate: f.predicate,
|
|
218
|
+
object: f.object,
|
|
219
|
+
provenance: f.kind === "other" ? tag : `${tag}:turn${editTurn}`,
|
|
220
|
+
})));
|
|
206
221
|
}
|
|
207
222
|
const removedCount = unrecognized.length === 0 && toRemoveIds.length ? (await removeFacts(memoryDir, toRemoveIds)).removed.length : 0;
|
|
208
223
|
const here = foldWorldState(readFactRows(await loadMemory(memoryDir))).placements.get("player")?.object ?? null;
|