@polycode-projects/the-mechanical-code-talker 3.3.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 +52 -0
- package/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +53 -13
- 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-garden.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-garden.jsonl +34 -1
- 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/p2p/webrtc-transport.mjs +146 -0
- package/src/domain/game-config.mjs +26 -5
- package/src/domain/grammar/ace.mjs +40 -4
- package/src/domain/grammar/lexicon-core.json +2 -1
- package/src/domain/grammar/lexicon.mjs +31 -0
- package/src/domain/memory/trust.mjs +6 -4
- package/src/domain/p2p/facts.mjs +81 -0
- package/src/domain/p2p/peer-id.mjs +32 -0
- package/src/domain/p2p/provenance-relabel.mjs +26 -0
- package/src/domain/p2p/sync-filter.mjs +31 -0
- package/src/domain/p2p/wire.mjs +123 -0
- package/src/domain/reference-pack.mjs +31 -7
- package/src/domain/spider-fly-world.mjs +1 -1
- package/src/domain/sprite-map.mjs +10 -2
- package/src/domain/sprite-templates.mjs +8 -6
- package/src/services/adventure-editor.mjs +10 -2
- package/src/services/adventure-viz.mjs +309 -73
- package/src/services/adventure.mjs +1034 -330
- package/src/services/chat-page-viz.mjs +1097 -16
- 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-editor.mjs +313 -0
- package/src/services/mud-turn.mjs +264 -87
- package/src/services/mud-viz.mjs +2606 -384
- package/src/services/p2p-room.mjs +559 -0
- 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 +109 -109
- package/src/surfaces/web/mud-browser-entry.mjs +226 -18
- package/src/surfaces/web/p2p-browser-entry.mjs +39 -0
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
// mud-turn.mjs — one acting character's whole turn in a mud world: investigate
|
|
2
|
-
// the room it stands in, walk toward food it actually knows about,
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// .mjs owns the read/fold/write primitives, this
|
|
6
|
-
// decisions that drive them. Nothing here writes a fact
|
|
7
|
-
// change goes out through runWorldCommand, recordTold
|
|
2
|
+
// the room it stands in, walk toward food it actually knows about, roll for
|
|
3
|
+
// digging out of the room's frontier, and (when none of that came to anything)
|
|
4
|
+
// set off for a room it has never stood in. The split mirrors spider-fly.mjs /
|
|
5
|
+
// spider-fly-turn.mjs: adventure.mjs owns the read/fold/write primitives, this
|
|
6
|
+
// file owns the per-tick decisions that drive them. Nothing here writes a fact
|
|
7
|
+
// of its own — every change goes out through runWorldCommand, recordTold,
|
|
8
|
+
// recordExamined or recordMassDrain.
|
|
8
9
|
//
|
|
9
10
|
// Every roll is seeded from (character, turn, room, decision name) through
|
|
10
11
|
// fnv1a32/mulberry32, so a run reproduces exactly from its inputs. The world
|
|
11
12
|
// layer writes no bare Math.random anywhere, and this file keeps that.
|
|
12
13
|
//
|
|
13
|
-
//
|
|
14
|
+
// Four things the design leaves open, settled here:
|
|
14
15
|
//
|
|
15
|
-
// A character moves once per turn. The directed walk
|
|
16
|
-
// on the same budget,
|
|
17
|
-
//
|
|
18
|
-
// reach an edge.
|
|
16
|
+
// A character moves once per turn. The directed walk, the edge rolls and the
|
|
17
|
+
// explore step draw on the same budget, in that order, so each only runs when
|
|
18
|
+
// the one before it came up empty. A walk that stepped reached the next room on
|
|
19
|
+
// a known path; it did not reach an edge.
|
|
19
20
|
//
|
|
20
|
-
// There are two separate reasons to dig —
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// The second ranges over the four lateral directions only: the frontier of the
|
|
21
|
+
// There are two separate reasons to dig — plain exploration, and following
|
|
22
|
+
// the level's own frontier toward food — and both cash out as a dig. They are
|
|
23
|
+
// kept apart by candidate set. The first ranges over every direction the
|
|
24
|
+
// room's kind allows and no exit already covers, vertical ones included. The
|
|
25
|
+
// second ranges over the lateral part of that same set: the frontier of the
|
|
26
26
|
// level the character already stands on. So a lateral direction draws two
|
|
27
27
|
// independent rolls and gets dug more often than a vertical one.
|
|
28
28
|
//
|
|
@@ -31,22 +31,37 @@
|
|
|
31
31
|
// the one case where an exit is worth a gamble and the walk still had nothing
|
|
32
32
|
// to follow. A character that knows about no food at all rolls nothing: that
|
|
33
33
|
// silence is the honest miss, and it holds all the way down.
|
|
34
|
+
//
|
|
35
|
+
// A turn that found nothing to do anywhere above ends on the explore step, and
|
|
36
|
+
// that step is a different claim from the food walk. It never guesses where
|
|
37
|
+
// food is. It reads which rooms this character has itself stood in — its own
|
|
38
|
+
// placement history, written turn by turn — and steps toward the nearest one it
|
|
39
|
+
// has not. "I have not been down there yet" is something an animal genuinely
|
|
40
|
+
// knows about itself, so acting on it invents nothing; without it a character
|
|
41
|
+
// that digs itself into a quiet corner stands there for the rest of the run.
|
|
42
|
+
//
|
|
43
|
+
// The two limits that make that stop being a treadmill live elsewhere:
|
|
44
|
+
// adventure.mjs bounds how far from the origin a dig may reach, so the set of
|
|
45
|
+
// rooms to explore is finite, and every turn charges mass. An animal really out
|
|
46
|
+
// of world eventually starves, which is an ending, not a freeze.
|
|
34
47
|
|
|
35
48
|
import { mulberry32 } from "../domain/seeded-random.mjs";
|
|
36
49
|
import { fnv1a32 } from "../domain/hash.mjs";
|
|
37
50
|
import { bfsLevels } from "../domain/planning.mjs";
|
|
38
51
|
import { loadMemory, readFactRows } from "../adapters/memory/core.mjs";
|
|
52
|
+
import { DEFAULT_GAME_CONFIG, mudMassDrainPerTurn } from "../domain/game-config.mjs";
|
|
39
53
|
import {
|
|
40
54
|
foldWorldState, worldActionRows, runWorldCommand, recordTold, recordExamined,
|
|
41
|
-
personKnowledgeLines, objectClassChain,
|
|
55
|
+
recordMassDrain, personKnowledgeLines, objectClassChain, diggableDirections,
|
|
56
|
+
isOutOfPlay, outOfPlayReasonOf, outOfPlayPhrase, massDrainPerTurnOf,
|
|
42
57
|
} from "./adventure.mjs";
|
|
43
58
|
|
|
44
59
|
const FOOD_CLASS = "food";
|
|
45
60
|
const LATERAL_DIRECTIONS = ["north", "south", "east", "west"];
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
// character's pathfinder walk a whole grown world every tick.
|
|
61
|
+
// Deep enough to cross a whole burrow without letting one character's
|
|
62
|
+
// pathfinder walk a whole grown world every tick.
|
|
49
63
|
const WALK_SEARCH_DEPTH = 8;
|
|
64
|
+
const SNAPSHOT_RE = /^(.+)@turn(\d+)$/;
|
|
50
65
|
|
|
51
66
|
const EXIT_TOWARD_FOOD_CHANCE = 0.5;
|
|
52
67
|
const EDGE_FOLLOW_DIG_CHANCE = 0.25;
|
|
@@ -116,6 +131,16 @@ function knownFood(rows, state, character) {
|
|
|
116
131
|
return [...new Set(aboutTopics)].filter((thing) => isFood(rows, thing)).sort();
|
|
117
132
|
}
|
|
118
133
|
|
|
134
|
+
/** The food a character knows about that is still somewhere it could be
|
|
135
|
+
* reached — on a floor, or in an open container. An eater's own claim about a
|
|
136
|
+
* carrot ages out the moment it eats it, but a claim only ever HEARD stands
|
|
137
|
+
* until somebody says otherwise, and that animal has no way to know the meal
|
|
138
|
+
* is over. Anything deciding where to GO reads this list rather than the raw
|
|
139
|
+
* one, or an animal spends the rest of the run crossing the burrow after
|
|
140
|
+
* somebody else's meal. */
|
|
141
|
+
const standingKnownFood = (rows, state, character) =>
|
|
142
|
+
knownFood(rows, state, character).filter((thing) => visibleRoomOf(rows, state, thing) !== null);
|
|
143
|
+
|
|
119
144
|
const knownTopics = (rows, state, character) =>
|
|
120
145
|
new Set(personKnowledgeLines(rows, state, character).aboutTopics);
|
|
121
146
|
|
|
@@ -132,20 +157,14 @@ const castIn = (state, room, exclude) => [...state.placements]
|
|
|
132
157
|
|
|
133
158
|
/**
|
|
134
159
|
* The first step of a shortest room-graph path from `here` to the nearest room
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
160
|
+
* `wanted` accepts, as `{ direction, room, hops }`. Null when nothing within
|
|
161
|
+
* the search depth qualifies. Ties break on the room name, so a fork never
|
|
162
|
+
* turns on fact-row order. Rooms in `avoid` are neither entered nor routed
|
|
163
|
+
* through.
|
|
139
164
|
*/
|
|
140
|
-
function
|
|
141
|
-
const foodRooms = new Set(
|
|
142
|
-
knownFood(rows, state, character)
|
|
143
|
-
.map((thing) => visibleRoomOf(rows, state, thing))
|
|
144
|
-
.filter(Boolean),
|
|
145
|
-
);
|
|
146
|
-
if (!foodRooms.size || foodRooms.has(here)) return null;
|
|
147
|
-
|
|
165
|
+
function firstStepToward(state, here, wanted, avoid = new Set()) {
|
|
148
166
|
const successorsOf = (room) => [...(state.exits.get(room)?.entries() ?? [])]
|
|
167
|
+
.filter(([, target]) => !avoid.has(target))
|
|
149
168
|
.map(([direction, target]) => ({ room: target, direction, from: room }));
|
|
150
169
|
|
|
151
170
|
const cameFrom = new Map(); // room -> { via, from }
|
|
@@ -153,7 +172,7 @@ function stepTowardKnownFood(rows, state, character, here) {
|
|
|
153
172
|
for (const level of bfsLevels(here, successorsOf, { maxDepth: WALK_SEARCH_DEPTH, keyOf: (item) => item.room })) {
|
|
154
173
|
hops += 1;
|
|
155
174
|
for (const item of level) cameFrom.set(item.room, { via: item.direction, from: item.from });
|
|
156
|
-
const goal = level.map((item) => item.room).filter(
|
|
175
|
+
const goal = level.map((item) => item.room).filter(wanted).sort()[0];
|
|
157
176
|
if (!goal) continue;
|
|
158
177
|
let room = goal;
|
|
159
178
|
let firstStep = null;
|
|
@@ -168,15 +187,56 @@ function stepTowardKnownFood(rows, state, character, here) {
|
|
|
168
187
|
return null;
|
|
169
188
|
}
|
|
170
189
|
|
|
171
|
-
|
|
190
|
+
/**
|
|
191
|
+
* The first step toward the nearest room holding a food-classed thing
|
|
192
|
+
* `character` knows about. Null when the character knows of no food, or when
|
|
193
|
+
* none of it sits in a room reachable within the search depth — the honest
|
|
194
|
+
* "I don't know where any food is", never a fallback wander.
|
|
195
|
+
*/
|
|
196
|
+
function stepTowardKnownFood(rows, state, character, here) {
|
|
197
|
+
const foodRooms = new Set(
|
|
198
|
+
standingKnownFood(rows, state, character).map((thing) => visibleRoomOf(rows, state, thing)),
|
|
199
|
+
);
|
|
200
|
+
if (!foodRooms.size || foodRooms.has(here)) return null;
|
|
201
|
+
return firstStepToward(state, here, (room) => foodRooms.has(room));
|
|
202
|
+
}
|
|
172
203
|
|
|
173
|
-
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
204
|
+
/** Every room `character` has itself stood in: its base placement and every
|
|
205
|
+
* @turnN snapshot of one, read over the world's OWN rows so a taught locative
|
|
206
|
+
* can never write a visit that never happened. */
|
|
207
|
+
function roomsStoodIn(rows, character) {
|
|
208
|
+
const visited = new Set();
|
|
209
|
+
for (const row of worldActionRows(rows)) {
|
|
210
|
+
if (row.predicate !== "mgx:currently-in") continue;
|
|
211
|
+
const snapshot = SNAPSHOT_RE.exec(row.subject);
|
|
212
|
+
if ((snapshot ? snapshot[1] : row.subject) !== character) continue;
|
|
213
|
+
visited.add(row.object);
|
|
214
|
+
}
|
|
215
|
+
return visited;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Every room a predator is standing in. An animal with nothing better to do
|
|
219
|
+
* does not go and look into one: leaving them in would make the explore step a
|
|
220
|
+
* funnel straight to the den, since the unvisited room nearest the burrow is
|
|
221
|
+
* exactly where the fox lives. The den stays reachable by the food gamble
|
|
222
|
+
* below, which is a gamble and is meant to be. */
|
|
223
|
+
const predatorRooms = (rows, state) => new Set((rows || [])
|
|
224
|
+
.filter((r) => r.predicate === "mgx:is-predator" && r.object === "true")
|
|
225
|
+
.map((r) => state.placements.get(r.subject)?.object)
|
|
226
|
+
.filter(Boolean));
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The first step toward the nearest room `character` has never stood in. This
|
|
230
|
+
* is the one move that runs on the character's OWN history rather than on
|
|
231
|
+
* anything it knows about the world's contents — it says "I have not been that
|
|
232
|
+
* way yet", which is true whatever turns out to be there.
|
|
233
|
+
*/
|
|
234
|
+
function stepTowardUnvisitedRoom(rows, state, character, here) {
|
|
235
|
+
const visited = roomsStoodIn(rows, character);
|
|
236
|
+
return firstStepToward(state, here, (room) => !visited.has(room), predatorRooms(rows, state));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---- the turn ----------------------------------------------------------------
|
|
180
240
|
|
|
181
241
|
/**
|
|
182
242
|
* Run one acting character's whole turn against a live mud world.
|
|
@@ -186,13 +246,24 @@ const edgeDirectionsOf = (state, room) => {
|
|
|
186
246
|
* turn. Room-mates are discovered from the fold, so the caller passes no cast
|
|
187
247
|
* list.
|
|
188
248
|
*
|
|
189
|
-
* Returns `{ character, k, room, roomAfter, actions, learned, text,
|
|
190
|
-
* `actions` is the machine-readable spine — one entry per sub-step
|
|
191
|
-
* each `{ step, kind, ..., text, miss }` — and a sub-step whose
|
|
192
|
-
* did not hold this turn records `kind: "none"` with its reason
|
|
193
|
-
* dropping out silently or being narrated as a success.
|
|
249
|
+
* Returns `{ character, k, room, roomAfter, actions, learned, mass, text,
|
|
250
|
+
* note }`. `actions` is the machine-readable spine — one entry per sub-step
|
|
251
|
+
* that fired, each `{ step, kind, ..., text, miss }` — and a sub-step whose
|
|
252
|
+
* precondition did not hold this turn records `kind: "none"` with its reason
|
|
253
|
+
* rather than dropping out silently or being narrated as a success. `mass` is
|
|
254
|
+
* what the character weighs once this turn's drain is charged, or null when the
|
|
255
|
+
* world writes it no mass.
|
|
256
|
+
*
|
|
257
|
+
* A turn that ends the character's run also carries `outOfPlay: true` and
|
|
258
|
+
* `outOfPlayReason` ("eaten" or "starved"), and so does every turn after it.
|
|
259
|
+
*
|
|
260
|
+
* `mudConfig` is the resolved `mud` section of the game config — the fallback
|
|
261
|
+
* for the per-species mass drain a turn charges, used only when the world
|
|
262
|
+
* itself writes the character no `mgx:mass-drain-per-turn` fact.
|
|
194
263
|
*/
|
|
195
|
-
export async function runMudTurn(character, {
|
|
264
|
+
export async function runMudTurn(character, {
|
|
265
|
+
world, memoryDir, env, graph, cache, k = null, mudConfig = DEFAULT_GAME_CONFIG.mud,
|
|
266
|
+
} = {}) {
|
|
196
267
|
const opened = await readWorld(memoryDir);
|
|
197
268
|
const room = opened.state.placements.get(character)?.object ?? null;
|
|
198
269
|
const turn = k ?? opened.state.turnCount + 1;
|
|
@@ -207,6 +278,15 @@ export async function runMudTurn(character, { world, memoryDir, env, graph, cach
|
|
|
207
278
|
note: `MUD — ${character} has no placement fact; the turn is declined rather than guessed`,
|
|
208
279
|
};
|
|
209
280
|
}
|
|
281
|
+
if (isOutOfPlay(opened.state, character)) {
|
|
282
|
+
const reason = outOfPlayReasonOf(opened.state, character);
|
|
283
|
+
return {
|
|
284
|
+
character, k: turn, room: null, roomAfter: null, actions, learned: [],
|
|
285
|
+
outOfPlay: true, outOfPlayReason: reason,
|
|
286
|
+
text: `${outOfPlayPhrase(character, reason)}. It takes no more turns.`,
|
|
287
|
+
note: `MUD — ${character} is placed out of play (${reason}); the turn is declined, and every later one will be too`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
210
290
|
|
|
211
291
|
const commandContext = { world, memoryDir, env, graph, cache, actingSubject: character };
|
|
212
292
|
const runCommand = async (step, cmd, detail) => {
|
|
@@ -232,21 +312,39 @@ export async function runMudTurn(character, { world, memoryDir, env, graph, cach
|
|
|
232
312
|
});
|
|
233
313
|
moved = !res.miss;
|
|
234
314
|
} else {
|
|
235
|
-
recordSkip(
|
|
236
|
-
"walk",
|
|
237
|
-
knownFood(walked.rows, walked.state, character).length
|
|
238
|
-
? "no room it knows holds food is reachable from here"
|
|
239
|
-
: "it knows of no food to walk toward",
|
|
240
|
-
`the ${character} has nowhere it knows to walk to.`,
|
|
241
|
-
);
|
|
315
|
+
recordSkip("walk", walkSkipReason(walked.rows, walked.state, character), `the ${character} has nowhere it knows to walk to.`);
|
|
242
316
|
}
|
|
243
317
|
|
|
244
318
|
if (!moved) {
|
|
245
|
-
await rollAtEdge({ character, turn, memoryDir, runCommand, recordSkip });
|
|
319
|
+
const edge = await rollAtEdge({ character, turn, memoryDir, runCommand, recordSkip });
|
|
320
|
+
if (!edge.acted) await exploreUnvisited({ character, memoryDir, runCommand, recordSkip });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// The world's own word first: a `mgx:mass-drain-per-turn` fact on the
|
|
324
|
+
// character or its species is something a player can read and change, and the
|
|
325
|
+
// shipped config table is only what a world that writes none falls back to.
|
|
326
|
+
const writtenDrain = massDrainPerTurnOf(opened.rows, character);
|
|
327
|
+
const drained = await recordMassDrain(memoryDir, {
|
|
328
|
+
world,
|
|
329
|
+
subject: character,
|
|
330
|
+
drainPerTurn: writtenDrain === null ? mudMassDrainPerTurn(mudConfig, character) : writtenDrain,
|
|
331
|
+
cache,
|
|
332
|
+
});
|
|
333
|
+
if (drained.mass === null) {
|
|
334
|
+
notes.push(`MUD — mass: the world writes ${character} no mass, so a turn costs it nothing`);
|
|
335
|
+
} else {
|
|
336
|
+
notes.push(`MUD — mass: ${character} is down to ${drained.mass}${drained.starved ? " and starves" : ""}`);
|
|
246
337
|
}
|
|
247
338
|
|
|
248
339
|
const closed = await readWorld(memoryDir);
|
|
249
340
|
const learned = [...knownTopics(closed.rows, closed.state, character)].filter((t) => !learnedBefore.has(t)).sort();
|
|
341
|
+
const texts = actions.map((a) => a.text).filter(Boolean);
|
|
342
|
+
if (drained.starved) texts.push(`the ${character} runs out of mass and starves. It takes no more turns.`);
|
|
343
|
+
// Read off the closed world rather than off the drain alone: a character can
|
|
344
|
+
// also end this turn eaten, by walking into the predator's room on it, and a
|
|
345
|
+
// caller watching for a fate should not have to know which of the two ways it
|
|
346
|
+
// was to find out that one happened.
|
|
347
|
+
const fate = outOfPlayReasonOf(closed.state, character);
|
|
250
348
|
return {
|
|
251
349
|
character,
|
|
252
350
|
k: turn,
|
|
@@ -254,35 +352,86 @@ export async function runMudTurn(character, { world, memoryDir, env, graph, cach
|
|
|
254
352
|
roomAfter: closed.state.placements.get(character)?.object ?? room,
|
|
255
353
|
actions,
|
|
256
354
|
learned,
|
|
257
|
-
|
|
355
|
+
mass: drained.mass,
|
|
356
|
+
...(fate ? { outOfPlay: true, outOfPlayReason: fate } : {}),
|
|
357
|
+
text: texts.join(" "),
|
|
258
358
|
note: notes.join("; "),
|
|
259
359
|
};
|
|
260
360
|
}
|
|
261
361
|
|
|
262
|
-
/**
|
|
263
|
-
*
|
|
264
|
-
|
|
265
|
-
|
|
362
|
+
/** Why a turn's food walk took no step — three different situations, each
|
|
363
|
+
* named on its own terms rather than collapsed into one shrug. */
|
|
364
|
+
function walkSkipReason(rows, state, character) {
|
|
365
|
+
if (!knownFood(rows, state, character).length) return "it knows of no food to walk toward";
|
|
366
|
+
if (!standingKnownFood(rows, state, character).length) return "every food it knows about has already been eaten";
|
|
367
|
+
return "no room it knows holds food is reachable from here";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** The last move of a turn that found nothing else to do: one step toward the
|
|
371
|
+
* nearest room this character has never stood in. Reached only when the walk
|
|
372
|
+
* took no step and the edge rolls fired nothing, so it never competes for the
|
|
373
|
+
* move budget — and when every room within reach has already been walked, it
|
|
374
|
+
* says so and the character stands still. */
|
|
375
|
+
async function exploreUnvisited({ character, memoryDir, runCommand, recordSkip }) {
|
|
376
|
+
const { rows, state } = await readWorld(memoryDir);
|
|
377
|
+
const here = state.placements.get(character)?.object ?? null;
|
|
378
|
+
if (!here) return recordSkip("explore", "it has no position to set out from", "");
|
|
379
|
+
const step = stepTowardUnvisitedRoom(rows, state, character, here);
|
|
380
|
+
if (!step) {
|
|
381
|
+
return recordSkip(
|
|
382
|
+
"explore",
|
|
383
|
+
"it has already stood in every room it can reach",
|
|
384
|
+
`the ${character} has been everywhere this burrow goes.`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
return runCommand("explore", { pattern: "imperative", verb: "go", direction: step.direction }, {
|
|
388
|
+
direction: step.direction, toward: step.room, hops: step.hops, reason: "unvisited",
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Step one: talk to a room-mate, examine something unexamined, then make one
|
|
393
|
+
* seeded attempt at take/put/eat. Talking comes first, and an animal speaks
|
|
394
|
+
* to a room-mate it has not met yet or that has food news for it. Once a
|
|
395
|
+
* partner has neither, this one has nothing left to say and spends the step
|
|
396
|
+
* on the room instead — two animals that stay put would otherwise trade the
|
|
397
|
+
* same empty greeting every turn for the rest of the run. The talk and the
|
|
398
|
+
* examine write testimony, which never folds into the playable state; only
|
|
399
|
+
* the manipulation touches the world. */
|
|
266
400
|
async function investigateRoom({ character, turn, room, memoryDir, cache, recordSkip, runCommand, actions, notes }) {
|
|
267
401
|
const { rows, state } = await readWorld(memoryDir);
|
|
268
402
|
const roomMates = castIn(state, room, character);
|
|
269
403
|
const alreadyKnown = knownTopics(rows, state, character);
|
|
404
|
+
const foodNewsFrom = (mate) => knownFood(rows, state, mate).filter((thing) => !alreadyKnown.has(thing));
|
|
405
|
+
const worthSpeakingTo = roomMates.filter((mate) => !alreadyKnown.has(mate) || foodNewsFrom(mate).length);
|
|
270
406
|
|
|
271
|
-
const teller = pickSeeded(
|
|
407
|
+
const teller = pickSeeded(worthSpeakingTo, character, turn, room, "ask-who");
|
|
272
408
|
if (!teller) {
|
|
273
|
-
recordSkip(
|
|
409
|
+
recordSkip(
|
|
410
|
+
"investigate",
|
|
411
|
+
roomMates.length
|
|
412
|
+
? `the ${character} has already heard everything the animals standing here can tell it about food`
|
|
413
|
+
: "no other character stands here to ask",
|
|
414
|
+
"",
|
|
415
|
+
);
|
|
274
416
|
} else {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
417
|
+
const told = pickSeeded(foodNewsFrom(teller), character, turn, room, `ask-${teller}`);
|
|
418
|
+
// Speaking to an animal is how this one comes to know it, and that note is
|
|
419
|
+
// the whole escape from the loop: a pair with no news left for each other
|
|
420
|
+
// drops out of worthSpeakingTo, and drops back in the moment either side
|
|
421
|
+
// learns a food the other has not heard of.
|
|
422
|
+
if (!alreadyKnown.has(teller)) {
|
|
423
|
+
await recordExamined(memoryDir, { observer: character, thing: teller, k: turn, cache });
|
|
424
|
+
alreadyKnown.add(teller);
|
|
425
|
+
}
|
|
278
426
|
if (!told) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
);
|
|
427
|
+
// Still a real exchange, and still narrated as one: an animal that meets
|
|
428
|
+
// another animal always says something. Only the testimony is missing,
|
|
429
|
+
// because the teller had nothing this one didn't already know.
|
|
430
|
+
actions.push({
|
|
431
|
+
step: "investigate", kind: "ask", teller, thing: null, miss: false,
|
|
432
|
+
text: `the ${character} greets the ${teller}, but hears nothing new about food.`,
|
|
433
|
+
});
|
|
434
|
+
notes.push(`MUD — talk: ${character} greeted ${teller}; ${teller} knows of no food to share`);
|
|
286
435
|
} else {
|
|
287
436
|
await recordTold(memoryDir, { asker: character, teller, thing: told, k: turn, cache });
|
|
288
437
|
alreadyKnown.add(told);
|
|
@@ -323,9 +472,15 @@ async function manipulateSomething({ character, turn, room, memoryDir, runComman
|
|
|
323
472
|
const { rows, state } = await readWorld(memoryDir);
|
|
324
473
|
const chosen = MANIPULATIONS[Math.floor(rollFor(character, turn, room, "manipulate") * MANIPULATIONS.length)];
|
|
325
474
|
|
|
326
|
-
|
|
327
|
-
|
|
475
|
+
// Everything the take verb would really accept here: loose on the floor, or
|
|
476
|
+
// sitting in an open container standing in this room. Reading only the floor
|
|
477
|
+
// made putting something into the basket a one-way trip, and in a room whose
|
|
478
|
+
// only food went into the basket that left nothing to take and nothing to
|
|
479
|
+
// eat for the rest of the run.
|
|
480
|
+
const withinReach = [...state.placements]
|
|
481
|
+
.filter(([, place]) => place.predicate === "mgx:located-in")
|
|
328
482
|
.map(([thing]) => thing)
|
|
483
|
+
.filter((thing) => visibleRoomOf(rows, state, thing) === room)
|
|
329
484
|
.sort();
|
|
330
485
|
const carried = [...state.placements]
|
|
331
486
|
.filter(([, place]) => place.predicate === "mgx:located-in" && place.object === character)
|
|
@@ -333,8 +488,8 @@ async function manipulateSomething({ character, turn, room, memoryDir, runComman
|
|
|
333
488
|
.sort();
|
|
334
489
|
|
|
335
490
|
if (chosen === "take") {
|
|
336
|
-
const target = pickSeeded(
|
|
337
|
-
if (!target) return recordSkip("investigate", "nothing
|
|
491
|
+
const target = pickSeeded(withinReach, character, turn, room, "take-what");
|
|
492
|
+
if (!target) return recordSkip("investigate", "nothing here it could pick up", "");
|
|
338
493
|
return runCommand("investigate", { pattern: "imperative", verb: "take", object: target }, { object: target });
|
|
339
494
|
}
|
|
340
495
|
|
|
@@ -356,7 +511,7 @@ async function manipulateSomething({ character, turn, room, memoryDir, runComman
|
|
|
356
511
|
);
|
|
357
512
|
}
|
|
358
513
|
|
|
359
|
-
const edible = [...new Set([...
|
|
514
|
+
const edible = [...new Set([...withinReach, ...carried])].filter((thing) => isFood(rows, thing)).sort();
|
|
360
515
|
const target = pickSeeded(edible, character, turn, room, "eat-what");
|
|
361
516
|
if (!target) return recordSkip("investigate", "no food is within reach here", "");
|
|
362
517
|
return runCommand("investigate", { pattern: "imperative", verb: "eat", object: target }, { object: target });
|
|
@@ -364,32 +519,54 @@ async function manipulateSomething({ character, turn, room, memoryDir, runComman
|
|
|
364
519
|
|
|
365
520
|
/** Step three: the room's unexplored sides, one independent roll per reason
|
|
366
521
|
* per direction. Only reached on a turn whose walk took no step, so the move
|
|
367
|
-
* budget is still unspent.
|
|
522
|
+
* budget is still unspent. Reports `{ acted }` — whether a roll actually came
|
|
523
|
+
* up — so the caller knows whether the turn still has a move to spend. */
|
|
368
524
|
async function rollAtEdge({ character, turn, memoryDir, runCommand, recordSkip }) {
|
|
369
525
|
const { rows, state } = await readWorld(memoryDir);
|
|
370
526
|
const room = state.placements.get(character)?.object ?? null;
|
|
371
|
-
if (!room)
|
|
372
|
-
|
|
373
|
-
|
|
527
|
+
if (!room) {
|
|
528
|
+
recordSkip("edge", "it has no position to roll from", "");
|
|
529
|
+
return { acted: false };
|
|
530
|
+
}
|
|
374
531
|
|
|
375
|
-
|
|
532
|
+
// The exit gamble runs before the dig rolls and whether or not this room can
|
|
533
|
+
// still be dug at all. A room mapped on every side its kind allows is
|
|
534
|
+
// exactly where a character with food somewhere else has nothing left but
|
|
535
|
+
// its existing exits to gamble on, so gating it behind a diggable direction
|
|
536
|
+
// stranded animals in a finished room for the rest of the run.
|
|
537
|
+
const foodItKnows = standingKnownFood(rows, state, character);
|
|
376
538
|
const foodStandsHere = foodItKnows.some((thing) => visibleRoomOf(rows, state, thing) === room);
|
|
377
539
|
if (foodItKnows.length && !foodStandsHere) {
|
|
378
540
|
for (const direction of [...(state.exits.get(room)?.keys() ?? [])].sort()) {
|
|
379
541
|
if (rollFor(character, turn, room, `exit-${direction}`) >= EXIT_TOWARD_FOOD_CHANCE) continue;
|
|
380
|
-
|
|
542
|
+
await runCommand("edge", { pattern: "imperative", verb: "go", direction }, { direction, reason: "exit-toward-food" });
|
|
543
|
+
return { acted: true };
|
|
381
544
|
}
|
|
382
545
|
}
|
|
383
546
|
|
|
547
|
+
// The room's OWN kind decides which way a dig can go — there is nothing to
|
|
548
|
+
// tunnel sideways through above ground, the burrow runs one level deep, and
|
|
549
|
+
// past the world's dig boundary nothing is offered at all — so the candidate
|
|
550
|
+
// set is the dig verb's own, never a flat compass. A roll on a direction the
|
|
551
|
+
// verb would refuse spends the turn on a decline.
|
|
552
|
+
const edges = diggableDirections(rows, state, room);
|
|
553
|
+
if (!edges.length) {
|
|
554
|
+
recordSkip("edge", `there is nowhere left to dig from the ${room}`, "");
|
|
555
|
+
return { acted: false };
|
|
556
|
+
}
|
|
557
|
+
|
|
384
558
|
for (const direction of edges.filter((d) => LATERAL_DIRECTIONS.includes(d))) {
|
|
385
559
|
if (rollFor(character, turn, room, `edge-follow-${direction}`) >= EDGE_FOLLOW_DIG_CHANCE) continue;
|
|
386
|
-
|
|
560
|
+
await runCommand("edge", { pattern: "imperative", verb: "dig", direction }, { direction, reason: "edge-follow" });
|
|
561
|
+
return { acted: true };
|
|
387
562
|
}
|
|
388
563
|
|
|
389
564
|
for (const direction of edges) {
|
|
390
565
|
if (rollFor(character, turn, room, `dig-${direction}`) >= EXPLORATORY_DIG_CHANCE) continue;
|
|
391
|
-
|
|
566
|
+
await runCommand("edge", { pattern: "imperative", verb: "dig", direction }, { direction, reason: "explore" });
|
|
567
|
+
return { acted: true };
|
|
392
568
|
}
|
|
393
569
|
|
|
394
|
-
|
|
570
|
+
recordSkip("edge", `no roll came up at the ${room}'s edge`, "");
|
|
571
|
+
return { acted: false };
|
|
395
572
|
}
|