@polycode-projects/the-mechanical-code-talker 5.0.0 → 5.0.2
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/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +35 -5
- package/corpus/worlds/shards/town-square-chapel.jsonl.gz +0 -0
- package/corpus/worlds/shards/town-square-market.jsonl.gz +0 -0
- package/corpus/worlds/shards/town-square.jsonl.gz +0 -0
- package/corpus/worlds/src/town-square-chapel.jsonl +929 -0
- package/corpus/worlds/src/town-square-market.jsonl +455 -0
- package/corpus/worlds/src/town-square.jsonl +687 -0
- package/package.json +5 -3
- package/src/domain/inflect.mjs +67 -0
- package/src/domain/memory/compaction.mjs +2 -0
- package/src/domain/town-square-world.mjs +416 -0
- package/src/services/chat.mjs +24 -0
- package/src/services/mud-editor.mjs +27 -0
- package/src/services/mudiii-scene.mjs +766 -0
- package/src/services/mudiii-turn.mjs +670 -0
- package/src/services/mudiii-viz.mjs +1247 -0
- package/src/services/predator-prey.mjs +916 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +127 -127
- package/src/surfaces/web/mudiii-browser-entry.mjs +224 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// mudiii-browser-entry.mjs — the esbuild entry for mudiii.html's live,
|
|
2
|
+
// one-player, one-shared-world session (public/mudiii-browser.bundle.js),
|
|
3
|
+
// mirroring mud-browser-entry.mjs's own session-factory shape. mudiii differs
|
|
4
|
+
// from mud in the two ways mudiii-viz.mjs's own header explains: ONE shared
|
|
5
|
+
// conversation rather than one pane per character (`turn(line)` never routes
|
|
6
|
+
// on `{ as: character }` — the addressee is in the sentence, "@fox-1 look"),
|
|
7
|
+
// and the simulation itself advances as ONE whole-world tick
|
|
8
|
+
// (`runTownSquareTick`) rather than mud's per-character `autoplayTick`.
|
|
9
|
+
//
|
|
10
|
+
// `src/services/predator-prey.mjs` — the engine this file drives — does not
|
|
11
|
+
// exist in every worktree yet; a concurrent track owns it. The import below
|
|
12
|
+
// is guarded (dynamic, try/catch) so this module still loads, and every
|
|
13
|
+
// test that imports mudiii-viz.mjs (which never imports this file) is
|
|
14
|
+
// unaffected either way. `createMudiiiSession` throws a clear "the engine
|
|
15
|
+
// isn't built yet" error if actually called before that track lands, rather
|
|
16
|
+
// than a bare ERR_MODULE_NOT_FOUND with no context.
|
|
17
|
+
import {
|
|
18
|
+
createInMemoryStore, appendFacts, appendRule, loadMemory, readFactRows, removeFacts,
|
|
19
|
+
} from "../../adapters/memory/core.mjs";
|
|
20
|
+
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
21
|
+
import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
|
|
22
|
+
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
23
|
+
import { worldProvenanceTag } from "../../domain/worlds-pack.mjs";
|
|
24
|
+
import { layoutNamed } from "../../domain/town-square-world.mjs";
|
|
25
|
+
import { parseMudEditorText, planMudEditorSync, gridWorldEditorState } from "../../services/mud-editor.mjs";
|
|
26
|
+
import { createTurnSession } from "./turn-session.mjs";
|
|
27
|
+
import { publishTmctSurface } from "./tmct-surface.mjs";
|
|
28
|
+
import { graphAsk, enginePlan } from "./engine-surface.mjs";
|
|
29
|
+
|
|
30
|
+
let engine = null;
|
|
31
|
+
async function loadEngine() {
|
|
32
|
+
if (engine) return engine;
|
|
33
|
+
try {
|
|
34
|
+
engine = await import("../../services/predator-prey.mjs");
|
|
35
|
+
} catch (err) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"mudiii's engine (src/services/predator-prey.mjs) is not built in this worktree yet "
|
|
38
|
+
+ `(${err && err.message ? err.message : err})`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return engine;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `count` entries drawn at random from `roster`, in random order, without
|
|
45
|
+
* repeats — mirrors mud-browser-entry.mjs's own `pickMudRoster`. `random` is
|
|
46
|
+
* injectable so a caller can pin the draw. Pure. */
|
|
47
|
+
export function pickMudiiiRoster(roster, { count = 1, random = Math.random } = {}) {
|
|
48
|
+
const pool = [...(roster || [])];
|
|
49
|
+
for (let i = pool.length - 1; i > 0; i -= 1) {
|
|
50
|
+
const j = Math.floor(random() * (i + 1));
|
|
51
|
+
[pool[i], pool[j]] = [pool[j], pool[i]];
|
|
52
|
+
}
|
|
53
|
+
return pool.slice(0, Math.min(count, pool.length));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The roster arguments `startTownSquareGame` needs, from whatever shape a
|
|
57
|
+
* caller cast. A page casts by NAME — `["fox-1", "goblin-3"]`, or the
|
|
58
|
+
* `[{ id, role }]` rows its scenario data carries — because that is what its
|
|
59
|
+
* own dropdown and HUD are keyed on. The engine seeds by COUNT: it mints
|
|
60
|
+
* `<prefix>-1..N` at seeded cells, which is the only place cells are chosen.
|
|
61
|
+
* So a named roster is translated to counts here, and the engine's mint is
|
|
62
|
+
* what the page then reads back through `board()`.
|
|
63
|
+
*
|
|
64
|
+
* A caller that already knows its cells (a recorded fixture pinning a starting
|
|
65
|
+
* board) passes the engine's own keyed object and it travels through
|
|
66
|
+
* untouched. Pure. */
|
|
67
|
+
export function townSquareRosterArgs(agents, roleOf) {
|
|
68
|
+
if (!agents || (Array.isArray(agents) && agents.length === 0)) return {};
|
|
69
|
+
if (!Array.isArray(agents)) return { agents };
|
|
70
|
+
let predatorCount = 0;
|
|
71
|
+
let preyCount = 0;
|
|
72
|
+
for (const entry of agents) {
|
|
73
|
+
const id = typeof entry === "string" ? entry : entry?.id;
|
|
74
|
+
const role = (typeof entry === "object" && entry?.role) || roleOf(id);
|
|
75
|
+
if (role === "predator") predatorCount += 1;
|
|
76
|
+
else if (role === "prey") preyCount += 1;
|
|
77
|
+
}
|
|
78
|
+
return { predatorCount, preyCount };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A live, shared town-square world one visitor watches and talks over.
|
|
82
|
+
* `worldPayload` is `{ name, facts, rules, opening }`, read once at build
|
|
83
|
+
* time the same way every other viz page's world payload is. `agents` is
|
|
84
|
+
* the roster this page casts — `[{ id, role }]`, `role` "predator" or
|
|
85
|
+
* "prey" (MUDIII_ROLES). Unlike mud-browser-entry.mjs's `createMudSession`,
|
|
86
|
+
* this returns ONE `turn`/`tick` pair for the whole world, never a
|
|
87
|
+
* per-character `windows` map: mudiii's simulation is a single global step
|
|
88
|
+
* (`runTownSquareTick`) and its conversation is a single shared dock.
|
|
89
|
+
*
|
|
90
|
+
* Returns `{ memoryDir, codeGraph, graph, refreshGraph, turn, tick, board,
|
|
91
|
+
* snapshot, applyEdit, placeFood }`. A reset is not a method here: the
|
|
92
|
+
* page re-opens a whole session for it, which is what a reset means when the
|
|
93
|
+
* store is in memory and belongs to one visitor. */
|
|
94
|
+
export async function createMudiiiSession(worldPayload, { agents = [], epoch = 0 } = {}) {
|
|
95
|
+
const {
|
|
96
|
+
startTownSquareGame, runTownSquareTick, townSquareBoard, foldTownSquareState,
|
|
97
|
+
placeFood: engPlaceFood, roleOfId,
|
|
98
|
+
} = await loadEngine();
|
|
99
|
+
|
|
100
|
+
// Every engine call is layout-scoped: the world pack ships the BOARD, and
|
|
101
|
+
// the layout carries the geometry plus the cast counts the engine mints the
|
|
102
|
+
// animals from. Resolved once here so no call site has to remember it.
|
|
103
|
+
const layout = layoutNamed(worldPayload.name);
|
|
104
|
+
if (!layout) throw new Error(`createMudiiiSession: no town-square layout named "${worldPayload.name}"`);
|
|
105
|
+
|
|
106
|
+
const memoryDir = createInMemoryStore();
|
|
107
|
+
const tag = worldProvenanceTag(worldPayload.name);
|
|
108
|
+
await appendFacts(memoryDir, (worldPayload.facts || []).map((f) => ({
|
|
109
|
+
subject: f.subject, predicate: f.predicate, object: f.object, provenance: tag,
|
|
110
|
+
})));
|
|
111
|
+
for (const rule of worldPayload.rules || []) {
|
|
112
|
+
await appendRule(memoryDir, { name: rule.name, kind: rule.ruleKind, slots: rule.slots, provenance: tag });
|
|
113
|
+
}
|
|
114
|
+
await startTownSquareGame(memoryDir, {
|
|
115
|
+
layout, epoch, ...townSquareRosterArgs(agents, (id) => roleOfId(id)),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// The slot holds one game at a time, and the lane a line reaches is decided
|
|
119
|
+
// by which key is in it. This session's world is a town square, so the slot
|
|
120
|
+
// says `mudiii` — the same shape mudiii-turn.mjs's own opening turn writes.
|
|
121
|
+
// Labelled `adventure` instead, every line here is claimed by the adventure
|
|
122
|
+
// lane first, and the town square's own verbs ("put food at cell-3-4",
|
|
123
|
+
// "@fox-1 the goblin is east", "tick") are answered with "an adventure is
|
|
124
|
+
// running" rather than run.
|
|
125
|
+
const planHolder = { state: { mudiii: { world: worldPayload.name, turn: 0 } } };
|
|
126
|
+
const codeGraph = parseEntities({ individuals: [], objectProperties: [] });
|
|
127
|
+
const lexicon = loadLexicon();
|
|
128
|
+
|
|
129
|
+
let memoryGraph = parseEntities({ individuals: [], objectProperties: [] });
|
|
130
|
+
async function refreshGraph() {
|
|
131
|
+
const rows = readFactRows(await loadMemory(memoryDir));
|
|
132
|
+
memoryGraph = parseEntities(memoryFactGraphPayload(rows));
|
|
133
|
+
return memoryGraph;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const turnSession = createTurnSession({
|
|
137
|
+
memoryDir, graph: codeGraph, lexicon, sessionId: "town-square",
|
|
138
|
+
vocabHint: 'Try "@fox-1 look", or "what does fox-1 believe".',
|
|
139
|
+
buildExtraOptions: () => ({ planState: planHolder.state }),
|
|
140
|
+
captureExtraState: async (result, state) => {
|
|
141
|
+
if ("planState" in result) planHolder.state = state.planState;
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/** ONE whole-world step (`runTownSquareTick`) — every live agent moves,
|
|
146
|
+
* the ecology pass runs, and the result names what happened this turn.
|
|
147
|
+
* `k` is the caller's own global turn counter, mirroring mud-viz.mjs's
|
|
148
|
+
* page-level serialization. */
|
|
149
|
+
async function tick(k) {
|
|
150
|
+
return runTownSquareTick(memoryDir, { layout });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The board as it stands, in the same payload shape `tick` returns, with no
|
|
154
|
+
* turn spent. A page opens a session and then draws THIS — otherwise its
|
|
155
|
+
* first sight of where anything stands is the first tick, and every mesh
|
|
156
|
+
* sits unplaced until the visitor presses play. `ecology` is empty and
|
|
157
|
+
* `turn` is the last tick played (0 on a fresh board). */
|
|
158
|
+
async function board() {
|
|
159
|
+
return townSquareBoard(memoryDir, { layout });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The one OMNISCIENT read this module exposes — every agent, every item,
|
|
163
|
+
* no fog of war, mirroring mud-browser-entry.mjs's own `snapshot`. */
|
|
164
|
+
async function snapshot() {
|
|
165
|
+
const rows = readFactRows(await loadMemory(memoryDir));
|
|
166
|
+
return { rows, state: foldTownSquareState(rows) };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The world editor's own store sync, reusing mud-editor.mjs UNCHANGED —
|
|
170
|
+
* it already round-trips `mgx:model`/`mgx:rotation` (a concurrent track
|
|
171
|
+
* added those), so nothing here is town-square-specific beyond the
|
|
172
|
+
* provenance scoping mud-browser-entry.mjs's own `applyEdit` performs. */
|
|
173
|
+
async function applyEdit(text) {
|
|
174
|
+
const allRows = readFactRows(await loadMemory(memoryDir));
|
|
175
|
+
const worldRows = allRows.filter((r) => typeof r.provenance === "string" && r.provenance.indexOf(tag) === 0);
|
|
176
|
+
const state = gridWorldEditorState(foldTownSquareState(worldRows));
|
|
177
|
+
const { triples, unrecognized } = parseMudEditorText(text);
|
|
178
|
+
const { toAppend, toRemoveIds } = planMudEditorSync(worldRows, state, triples);
|
|
179
|
+
if (toAppend.length) {
|
|
180
|
+
await appendFacts(memoryDir, toAppend.map((f) => ({
|
|
181
|
+
subject: f.subject, predicate: f.predicate, object: f.object, provenance: tag,
|
|
182
|
+
})));
|
|
183
|
+
}
|
|
184
|
+
const removed = unrecognized.length === 0 && toRemoveIds.length
|
|
185
|
+
? (await removeFacts(memoryDir, toRemoveIds)).removed.length
|
|
186
|
+
: 0;
|
|
187
|
+
return { unrecognized, added: toAppend.length, removed };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Places one player-placed food item, with player provenance rather than
|
|
191
|
+
* the world tag — the same "who put that there?" grounding
|
|
192
|
+
* test/fixtures/mudiii-ticks.json's turn-8 `place-food` event names. */
|
|
193
|
+
async function placeFood(opts) {
|
|
194
|
+
return engPlaceFood(memoryDir, { layout, ...opts });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
memoryDir,
|
|
199
|
+
codeGraph,
|
|
200
|
+
get graph() { return memoryGraph; },
|
|
201
|
+
refreshGraph,
|
|
202
|
+
turn: turnSession.turn,
|
|
203
|
+
tick,
|
|
204
|
+
board,
|
|
205
|
+
snapshot,
|
|
206
|
+
applyEdit,
|
|
207
|
+
placeFood,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// tmct.turn(line) reaches the one shared conversation directly (no `{ as }`
|
|
212
|
+
// routing — the default publishTmctSurface behaviour, `session.turn(line,
|
|
213
|
+
// options)`, is exactly right here, so no `turn` override is passed).
|
|
214
|
+
publishTmctSurface({
|
|
215
|
+
open: createMudiiiSession,
|
|
216
|
+
ask: async (request, options, session) => {
|
|
217
|
+
await session.refreshGraph();
|
|
218
|
+
return graphAsk(request, options, session);
|
|
219
|
+
},
|
|
220
|
+
plan: enginePlan,
|
|
221
|
+
page: {
|
|
222
|
+
pickMudiiiRoster,
|
|
223
|
+
},
|
|
224
|
+
});
|