@forbocai/test-game 0.6.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 +49 -0
- package/dist/chunk-27XG6FHM.mjs +751 -0
- package/dist/chunk-4XIOL463.mjs +745 -0
- package/dist/chunk-NNRCMKGA.mjs +694 -0
- package/dist/cli.d.mts +17 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +823 -0
- package/dist/cli.mjs +56 -0
- package/dist/index.d.mts +54 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +774 -0
- package/dist/index.mjs +8 -0
- package/package.json +36 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
REQUIRED_GROUPS: () => REQUIRED_GROUPS,
|
|
24
|
+
runGame: () => runGame
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/game.ts
|
|
29
|
+
var import_promises = require("readline/promises");
|
|
30
|
+
var import_node_process = require("process");
|
|
31
|
+
|
|
32
|
+
// src/features/autoplay/slices/harnessSlice.ts
|
|
33
|
+
var import_toolkit = require("@reduxjs/toolkit");
|
|
34
|
+
|
|
35
|
+
// src/lib/coverage.ts
|
|
36
|
+
var REQUIRED_GROUPS = [
|
|
37
|
+
"status",
|
|
38
|
+
"npc_lifecycle",
|
|
39
|
+
"npc_process_chat",
|
|
40
|
+
"memory_list",
|
|
41
|
+
"memory_recall",
|
|
42
|
+
"memory_store",
|
|
43
|
+
"memory_clear",
|
|
44
|
+
"memory_export",
|
|
45
|
+
"bridge_rules",
|
|
46
|
+
"bridge_validate",
|
|
47
|
+
"bridge_preset",
|
|
48
|
+
"soul_export",
|
|
49
|
+
"soul_import",
|
|
50
|
+
"soul_list",
|
|
51
|
+
"soul_chat",
|
|
52
|
+
"ghost_lifecycle",
|
|
53
|
+
"cortex_init"
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// src/features/autoplay/slices/harnessSlice.ts
|
|
57
|
+
var initialState = {
|
|
58
|
+
covered: {}
|
|
59
|
+
};
|
|
60
|
+
var harnessSlice = (0, import_toolkit.createSlice)({
|
|
61
|
+
name: "harness",
|
|
62
|
+
initialState,
|
|
63
|
+
reducers: {
|
|
64
|
+
markCovered: (state, action) => {
|
|
65
|
+
state.covered[action.payload] = true;
|
|
66
|
+
},
|
|
67
|
+
resetCoverage: (state) => {
|
|
68
|
+
state.covered = {};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
var harnessReducer = harnessSlice.reducer;
|
|
73
|
+
var harnessActions = harnessSlice.actions;
|
|
74
|
+
var selectMissingGroups = (covered) => REQUIRED_GROUPS.filter((g) => !covered[g]);
|
|
75
|
+
|
|
76
|
+
// src/features/mechanics/slices/socialSlice.ts
|
|
77
|
+
var import_toolkit2 = require("@reduxjs/toolkit");
|
|
78
|
+
var initialState2 = {};
|
|
79
|
+
var socialSlice = (0, import_toolkit2.createSlice)({
|
|
80
|
+
name: "social",
|
|
81
|
+
initialState: initialState2,
|
|
82
|
+
reducers: {
|
|
83
|
+
setDialogue: (state, action) => {
|
|
84
|
+
state.activeDialogue = action.payload;
|
|
85
|
+
},
|
|
86
|
+
setTradeOffer: (state, action) => {
|
|
87
|
+
state.activeTrade = action.payload;
|
|
88
|
+
},
|
|
89
|
+
clearSocialState: (state) => {
|
|
90
|
+
state.activeDialogue = void 0;
|
|
91
|
+
state.activeTrade = void 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
var socialReducer = socialSlice.reducer;
|
|
96
|
+
var socialActions = socialSlice.actions;
|
|
97
|
+
|
|
98
|
+
// src/features/mechanics/slices/stealthSlice.ts
|
|
99
|
+
var import_toolkit3 = require("@reduxjs/toolkit");
|
|
100
|
+
var initialState3 = {
|
|
101
|
+
doorOpen: false,
|
|
102
|
+
alertLevel: 0
|
|
103
|
+
};
|
|
104
|
+
var stealthSlice = (0, import_toolkit3.createSlice)({
|
|
105
|
+
name: "stealth",
|
|
106
|
+
initialState: initialState3,
|
|
107
|
+
reducers: {
|
|
108
|
+
setDoorOpen: (state, action) => {
|
|
109
|
+
state.doorOpen = action.payload;
|
|
110
|
+
},
|
|
111
|
+
bumpAlert: (state, action) => {
|
|
112
|
+
state.alertLevel = Math.max(0, Math.min(100, state.alertLevel + action.payload));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
var stealthReducer = stealthSlice.reducer;
|
|
117
|
+
var stealthActions = stealthSlice.actions;
|
|
118
|
+
|
|
119
|
+
// src/features/entities/slices/npcsSlice.ts
|
|
120
|
+
var import_toolkit4 = require("@reduxjs/toolkit");
|
|
121
|
+
var npcsAdapter = (0, import_toolkit4.createEntityAdapter)();
|
|
122
|
+
var npcsSlice = (0, import_toolkit4.createSlice)({
|
|
123
|
+
name: "npcs",
|
|
124
|
+
initialState: npcsAdapter.getInitialState(),
|
|
125
|
+
reducers: {
|
|
126
|
+
upsertNPC: npcsAdapter.upsertOne,
|
|
127
|
+
moveNPC: (state, action) => {
|
|
128
|
+
npcsAdapter.updateOne(state, {
|
|
129
|
+
id: action.payload.id,
|
|
130
|
+
changes: { position: action.payload.position }
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
patchNPC: (state, action) => {
|
|
134
|
+
npcsAdapter.updateOne(state, {
|
|
135
|
+
id: action.payload.id,
|
|
136
|
+
changes: action.payload.patch
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
/**
|
|
140
|
+
* Applies a validated verdict handed back via the SDK (real CLI output).
|
|
141
|
+
* This is a "fat reducer" containing business logic for state transitions.
|
|
142
|
+
*/
|
|
143
|
+
applyNpcVerdict: (state, action) => {
|
|
144
|
+
const { id, verdict } = action.payload;
|
|
145
|
+
const npc = state.entities[id];
|
|
146
|
+
if (!npc) return;
|
|
147
|
+
npcsAdapter.updateOne(state, {
|
|
148
|
+
id,
|
|
149
|
+
changes: {
|
|
150
|
+
...verdict.stateDelta,
|
|
151
|
+
// If the action is a MOVE, update the position from the payload
|
|
152
|
+
position: verdict.action.type === "MOVE" ? verdict.action.payload.targetHex : npc.position
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
var npcsReducer = npcsSlice.reducer;
|
|
159
|
+
var npcsActions = npcsSlice.actions;
|
|
160
|
+
var npcsSelectors = npcsAdapter.getSelectors(
|
|
161
|
+
(root) => root.npcs
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// src/features/entities/slices/playerSlice.ts
|
|
165
|
+
var import_toolkit5 = require("@reduxjs/toolkit");
|
|
166
|
+
var initialState4 = {
|
|
167
|
+
name: "Scout",
|
|
168
|
+
hp: 100,
|
|
169
|
+
hidden: true,
|
|
170
|
+
position: { x: 1, y: 1 },
|
|
171
|
+
inventory: ["coin-pouch"]
|
|
172
|
+
};
|
|
173
|
+
var playerSlice = (0, import_toolkit5.createSlice)({
|
|
174
|
+
name: "player",
|
|
175
|
+
initialState: initialState4,
|
|
176
|
+
reducers: {
|
|
177
|
+
setPosition: (state, action) => {
|
|
178
|
+
state.position = action.payload;
|
|
179
|
+
},
|
|
180
|
+
setHidden: (state, action) => {
|
|
181
|
+
state.hidden = action.payload;
|
|
182
|
+
},
|
|
183
|
+
patchPlayer: (state, action) => {
|
|
184
|
+
Object.assign(state, action.payload);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
var playerReducer = playerSlice.reducer;
|
|
189
|
+
var playerActions = playerSlice.actions;
|
|
190
|
+
|
|
191
|
+
// src/features/store/slices/memorySlice.ts
|
|
192
|
+
var import_toolkit6 = require("@reduxjs/toolkit");
|
|
193
|
+
var memoryAdapter = (0, import_toolkit6.createEntityAdapter)();
|
|
194
|
+
var memorySlice = (0, import_toolkit6.createSlice)({
|
|
195
|
+
name: "memory",
|
|
196
|
+
initialState: memoryAdapter.getInitialState(),
|
|
197
|
+
reducers: {
|
|
198
|
+
storeMemory: memoryAdapter.addOne,
|
|
199
|
+
clearMemoryForNpc: (state, action) => {
|
|
200
|
+
const npcMemories = Object.values(state.entities).filter((r) => r?.npcId === action.payload).map((r) => r.id);
|
|
201
|
+
memoryAdapter.removeMany(state, npcMemories);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
var memoryReducer = memorySlice.reducer;
|
|
206
|
+
var memoryActions = memorySlice.actions;
|
|
207
|
+
var memorySelectors = memoryAdapter.getSelectors(
|
|
208
|
+
(root) => root.memory
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// src/features/store/slices/soulSlice.ts
|
|
212
|
+
var import_toolkit7 = require("@reduxjs/toolkit");
|
|
213
|
+
var initialState5 = {
|
|
214
|
+
exportsByNpc: {},
|
|
215
|
+
importedSoulTxIds: []
|
|
216
|
+
};
|
|
217
|
+
var soulSlice = (0, import_toolkit7.createSlice)({
|
|
218
|
+
name: "soul",
|
|
219
|
+
initialState: initialState5,
|
|
220
|
+
reducers: {
|
|
221
|
+
markSoulExported: (state, action) => {
|
|
222
|
+
state.exportsByNpc[action.payload.npcId] = action.payload.txId;
|
|
223
|
+
},
|
|
224
|
+
markSoulImported: (state, action) => {
|
|
225
|
+
state.importedSoulTxIds.push(action.payload);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
var soulReducer = soulSlice.reducer;
|
|
230
|
+
var soulActions = soulSlice.actions;
|
|
231
|
+
|
|
232
|
+
// src/features/terminal/slices/transcriptSlice.ts
|
|
233
|
+
var import_toolkit8 = require("@reduxjs/toolkit");
|
|
234
|
+
var initialState6 = {
|
|
235
|
+
entries: []
|
|
236
|
+
};
|
|
237
|
+
var transcriptSlice = (0, import_toolkit8.createSlice)({
|
|
238
|
+
name: "transcript",
|
|
239
|
+
initialState: initialState6,
|
|
240
|
+
reducers: {
|
|
241
|
+
recordTranscript: {
|
|
242
|
+
reducer: (state, action) => {
|
|
243
|
+
state.entries.push(action.payload);
|
|
244
|
+
},
|
|
245
|
+
prepare: (payload) => ({
|
|
246
|
+
payload: {
|
|
247
|
+
...payload,
|
|
248
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
249
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
},
|
|
253
|
+
resetTranscript: (state) => {
|
|
254
|
+
state.entries = [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
var transcriptReducer = transcriptSlice.reducer;
|
|
259
|
+
var transcriptActions = transcriptSlice.actions;
|
|
260
|
+
|
|
261
|
+
// src/features/terminal/slices/uiSlice.ts
|
|
262
|
+
var import_toolkit9 = require("@reduxjs/toolkit");
|
|
263
|
+
var initialState7 = {
|
|
264
|
+
mode: "autoplay",
|
|
265
|
+
messages: ["SYSTEM_OVERRIDE :: terminal HUD online"]
|
|
266
|
+
};
|
|
267
|
+
var uiSlice = (0, import_toolkit9.createSlice)({
|
|
268
|
+
name: "ui",
|
|
269
|
+
initialState: initialState7,
|
|
270
|
+
reducers: {
|
|
271
|
+
setMode: (state, action) => {
|
|
272
|
+
state.mode = action.payload;
|
|
273
|
+
},
|
|
274
|
+
addMessage: (state, action) => {
|
|
275
|
+
state.messages.push(action.payload);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
var uiReducer = uiSlice.reducer;
|
|
280
|
+
var uiActions = uiSlice.actions;
|
|
281
|
+
|
|
282
|
+
// src/lib/commandRunner.ts
|
|
283
|
+
var import_node_child_process = require("child_process");
|
|
284
|
+
var import_node_util = require("util");
|
|
285
|
+
var execAsync = (0, import_node_util.promisify)(import_node_child_process.exec);
|
|
286
|
+
var runCommand = async (command) => {
|
|
287
|
+
try {
|
|
288
|
+
const { stdout, stderr } = await execAsync(command.command, { timeout: 1e4 });
|
|
289
|
+
const output2 = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
290
|
+
return { status: "ok", output: output2 || "Command completed." };
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const output2 = [error?.stdout, error?.stderr, error?.message].filter(Boolean).join("\n").trim();
|
|
293
|
+
return { status: "error", output: output2 || "Command failed." };
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// src/lib/render.ts
|
|
298
|
+
var cellAt = (pos, state) => {
|
|
299
|
+
const isBlocked = state.grid.blocked.some((b) => b.x === pos.x && b.y === pos.y);
|
|
300
|
+
if (isBlocked) return "#";
|
|
301
|
+
if (state.player.position.x === pos.x && state.player.position.y === pos.y) {
|
|
302
|
+
return "P";
|
|
303
|
+
}
|
|
304
|
+
for (const npc of Object.values(state.npcs.entities)) {
|
|
305
|
+
if (!npc) continue;
|
|
306
|
+
if (npc.position.x === pos.x && npc.position.y === pos.y) return npc.id === "miller" ? "M" : "D";
|
|
307
|
+
}
|
|
308
|
+
return ".";
|
|
309
|
+
};
|
|
310
|
+
var renderGrid = (state) => {
|
|
311
|
+
const rows = [];
|
|
312
|
+
for (let y = 0; y < state.grid.height; y += 1) {
|
|
313
|
+
const cells = [];
|
|
314
|
+
for (let x = 0; x < state.grid.width; x += 1) {
|
|
315
|
+
cells.push(cellAt({ x, y }, state));
|
|
316
|
+
}
|
|
317
|
+
rows.push(cells.join(" "));
|
|
318
|
+
}
|
|
319
|
+
return rows.join("\n");
|
|
320
|
+
};
|
|
321
|
+
var renderLegend = () => "Legend :: P=Scout D=Doomguard M=Miller #=Blocked Tile .=Open Tile";
|
|
322
|
+
|
|
323
|
+
// src/store.ts
|
|
324
|
+
var import_toolkit15 = require("@reduxjs/toolkit");
|
|
325
|
+
|
|
326
|
+
// src/features/mechanics/slices/gridSlice.ts
|
|
327
|
+
var import_toolkit10 = require("@reduxjs/toolkit");
|
|
328
|
+
var initialState8 = {
|
|
329
|
+
width: 8,
|
|
330
|
+
height: 8,
|
|
331
|
+
blocked: [{ x: 4, y: 4 }, { x: 4, y: 5 }, { x: 6, y: 2 }]
|
|
332
|
+
};
|
|
333
|
+
var gridSlice = (0, import_toolkit10.createSlice)({
|
|
334
|
+
name: "grid",
|
|
335
|
+
initialState: initialState8,
|
|
336
|
+
reducers: {
|
|
337
|
+
setGridSize: (state, action) => {
|
|
338
|
+
state.width = action.payload.width;
|
|
339
|
+
state.height = action.payload.height;
|
|
340
|
+
},
|
|
341
|
+
setBlocked: (state, action) => {
|
|
342
|
+
state.blocked = action.payload;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
var gridReducer = gridSlice.reducer;
|
|
347
|
+
var gridActions = gridSlice.actions;
|
|
348
|
+
|
|
349
|
+
// src/features/mechanics/slices/bridgeSlice.ts
|
|
350
|
+
var import_toolkit11 = require("@reduxjs/toolkit");
|
|
351
|
+
var initialState9 = {
|
|
352
|
+
maxJumpForce: 500,
|
|
353
|
+
maxMoveDistance: 2,
|
|
354
|
+
activePreset: "default"
|
|
355
|
+
};
|
|
356
|
+
var bridgeSlice = (0, import_toolkit11.createSlice)({
|
|
357
|
+
name: "bridge",
|
|
358
|
+
initialState: initialState9,
|
|
359
|
+
reducers: {
|
|
360
|
+
setBridgeRules: (state, action) => {
|
|
361
|
+
Object.assign(state, action.payload);
|
|
362
|
+
},
|
|
363
|
+
loadBridgePreset: (state, action) => {
|
|
364
|
+
state.activePreset = action.payload;
|
|
365
|
+
if (action.payload === "social") {
|
|
366
|
+
state.maxMoveDistance = 1;
|
|
367
|
+
}
|
|
368
|
+
if (action.payload === "default") {
|
|
369
|
+
state.maxMoveDistance = 2;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
var bridgeReducer = bridgeSlice.reducer;
|
|
375
|
+
var bridgeActions = bridgeSlice.actions;
|
|
376
|
+
|
|
377
|
+
// src/features/store/slices/inventorySlice.ts
|
|
378
|
+
var import_toolkit12 = require("@reduxjs/toolkit");
|
|
379
|
+
var initialState10 = {
|
|
380
|
+
byOwner: {
|
|
381
|
+
player: ["coin-pouch"],
|
|
382
|
+
miller: ["medkit"]
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
var inventorySlice = (0, import_toolkit12.createSlice)({
|
|
386
|
+
name: "inventory",
|
|
387
|
+
initialState: initialState10,
|
|
388
|
+
reducers: {
|
|
389
|
+
setOwnerInventory: (state, action) => {
|
|
390
|
+
state.byOwner[action.payload.ownerId] = action.payload.items;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
var inventoryReducer = inventorySlice.reducer;
|
|
395
|
+
var inventoryActions = inventorySlice.actions;
|
|
396
|
+
|
|
397
|
+
// src/features/autoplay/slices/scenarioSlice.ts
|
|
398
|
+
var import_toolkit13 = require("@reduxjs/toolkit");
|
|
399
|
+
var steps = [
|
|
400
|
+
{
|
|
401
|
+
id: "stealth-door-open",
|
|
402
|
+
title: "Spatial Strategy & Stealth",
|
|
403
|
+
description: "Armory door is left open. Doomguard patrol processes observation and stores memory.",
|
|
404
|
+
eventType: "stealth",
|
|
405
|
+
commands: [
|
|
406
|
+
{ group: "status", command: "forbocai status", expectedRoutes: ["GET /status"] },
|
|
407
|
+
{
|
|
408
|
+
group: "npc_lifecycle",
|
|
409
|
+
command: "forbocai npc create doomguard",
|
|
410
|
+
expectedRoutes: ["local only"]
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
group: "npc_lifecycle",
|
|
414
|
+
command: "forbocai npc state doomguard",
|
|
415
|
+
expectedRoutes: ["local only"]
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
group: "npc_lifecycle",
|
|
419
|
+
command: "forbocai npc update doomguard faction Doomguards",
|
|
420
|
+
expectedRoutes: ["local only"]
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
group: "npc_process_chat",
|
|
424
|
+
command: 'forbocai npc process doomguard "The armory door is open"',
|
|
425
|
+
expectedRoutes: ["POST /npcs/{id}/directive", "POST /npcs/{id}/context", "POST /npcs/{id}/verdict"]
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
group: "memory_store",
|
|
429
|
+
command: 'forbocai memory store doomguard "Armory door found open at x:5, y:12"',
|
|
430
|
+
expectedRoutes: ["POST /npcs/{id}/memory"]
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
group: "memory_list",
|
|
434
|
+
command: "forbocai memory list doomguard",
|
|
435
|
+
expectedRoutes: ["GET /npcs/{id}/memory"]
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
group: "bridge_rules",
|
|
439
|
+
command: "forbocai bridge rules",
|
|
440
|
+
expectedRoutes: ["GET /bridge/rules"]
|
|
441
|
+
}
|
|
442
|
+
]
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
id: "social-miller-encounter",
|
|
446
|
+
title: "Social Simulation",
|
|
447
|
+
description: "Miller encounter triggers recall, dialogue, and trade offer.",
|
|
448
|
+
eventType: "social",
|
|
449
|
+
commands: [
|
|
450
|
+
{
|
|
451
|
+
group: "npc_lifecycle",
|
|
452
|
+
command: "forbocai npc create miller",
|
|
453
|
+
expectedRoutes: ["local only"]
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
group: "npc_process_chat",
|
|
457
|
+
command: 'forbocai npc chat miller --text "I come in peace"',
|
|
458
|
+
expectedRoutes: ["POST /npcs/{id}/directive", "POST /npcs/{id}/context", "POST /npcs/{id}/verdict"]
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
group: "memory_recall",
|
|
462
|
+
command: 'forbocai memory recall miller "player interaction"',
|
|
463
|
+
expectedRoutes: ["POST /npcs/{id}/memory/recall"]
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
group: "bridge_preset",
|
|
467
|
+
command: "forbocai bridge preset social",
|
|
468
|
+
expectedRoutes: ["POST /rules/presets/{id}"]
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
group: "soul_export",
|
|
472
|
+
command: "forbocai soul export miller",
|
|
473
|
+
expectedRoutes: ["POST /npcs/{id}/soul/export"]
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
group: "soul_list",
|
|
477
|
+
command: "forbocai soul list",
|
|
478
|
+
expectedRoutes: ["GET /souls"]
|
|
479
|
+
}
|
|
480
|
+
]
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
id: "escape-realtime-pursuit",
|
|
484
|
+
title: "Real-Time Escape",
|
|
485
|
+
description: "Harness loop fires process commands and validates jump force via bridge rules.",
|
|
486
|
+
eventType: "escape",
|
|
487
|
+
commands: [
|
|
488
|
+
{
|
|
489
|
+
group: "bridge_validate",
|
|
490
|
+
command: "forbocai bridge validate doomguard-jump",
|
|
491
|
+
expectedRoutes: ["POST /bridge/validate", "POST /bridge/validate/{id}"]
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
group: "npc_process_chat",
|
|
495
|
+
command: 'forbocai npc process doomguard "Player is escaping"',
|
|
496
|
+
expectedRoutes: ["POST /npcs/{id}/directive", "POST /npcs/{id}/context", "POST /npcs/{id}/verdict"]
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
group: "npc_process_chat",
|
|
500
|
+
command: 'forbocai npc process doomguard "Player jumped the gap"',
|
|
501
|
+
expectedRoutes: ["POST /npcs/{id}/directive", "POST /npcs/{id}/context", "POST /npcs/{id}/verdict"]
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
group: "npc_process_chat",
|
|
505
|
+
command: 'forbocai npc process doomguard "Player reached the gate"',
|
|
506
|
+
expectedRoutes: ["POST /npcs/{id}/directive", "POST /npcs/{id}/context", "POST /npcs/{id}/verdict"]
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
group: "ghost_lifecycle",
|
|
510
|
+
command: "forbocai ghost run smoke",
|
|
511
|
+
expectedRoutes: ["POST /ghost/run"]
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
group: "ghost_lifecycle",
|
|
515
|
+
command: "forbocai ghost status ghost-001",
|
|
516
|
+
expectedRoutes: ["GET /ghost/{id}/status"]
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
group: "ghost_lifecycle",
|
|
520
|
+
command: "forbocai ghost results ghost-001",
|
|
521
|
+
expectedRoutes: ["GET /ghost/{id}/results"]
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
group: "ghost_lifecycle",
|
|
525
|
+
command: "forbocai ghost stop ghost-001",
|
|
526
|
+
expectedRoutes: ["POST /ghost/{id}/stop"]
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
group: "ghost_lifecycle",
|
|
530
|
+
command: "forbocai ghost history",
|
|
531
|
+
expectedRoutes: ["GET /ghost/history"]
|
|
532
|
+
}
|
|
533
|
+
]
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
id: "persistence-recovery",
|
|
537
|
+
title: "Persistence & Recovery",
|
|
538
|
+
description: "Exercises memory export/clear and soul import/chat continuity.",
|
|
539
|
+
eventType: "persistence",
|
|
540
|
+
commands: [
|
|
541
|
+
{
|
|
542
|
+
group: "memory_export",
|
|
543
|
+
command: "forbocai memory export doomguard",
|
|
544
|
+
expectedRoutes: ["local only"]
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
group: "memory_clear",
|
|
548
|
+
command: "forbocai memory clear doomguard",
|
|
549
|
+
expectedRoutes: ["DELETE /npcs/{id}/memory/clear"]
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
group: "soul_import",
|
|
553
|
+
command: "forbocai soul import tx-demo-001",
|
|
554
|
+
expectedRoutes: ["GET /souls/{txId}", "POST /npcs/import"]
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
group: "soul_chat",
|
|
558
|
+
command: 'forbocai soul chat doomguard --text "What do you remember?"',
|
|
559
|
+
expectedRoutes: ["POST /npcs/{id}/directive", "POST /npcs/{id}/context", "POST /npcs/{id}/verdict"]
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
group: "cortex_init",
|
|
563
|
+
command: "forbocai cortex init --remote",
|
|
564
|
+
expectedRoutes: ["POST /cortex/init"]
|
|
565
|
+
}
|
|
566
|
+
]
|
|
567
|
+
}
|
|
568
|
+
];
|
|
569
|
+
var scenarioSlice = (0, import_toolkit13.createSlice)({
|
|
570
|
+
name: "scenario",
|
|
571
|
+
initialState: {
|
|
572
|
+
steps
|
|
573
|
+
},
|
|
574
|
+
reducers: {}
|
|
575
|
+
});
|
|
576
|
+
var scenarioReducer = scenarioSlice.reducer;
|
|
577
|
+
|
|
578
|
+
// src/listeners.ts
|
|
579
|
+
var import_toolkit14 = require("@reduxjs/toolkit");
|
|
580
|
+
var listenerMiddleware = (0, import_toolkit14.createListenerMiddleware)();
|
|
581
|
+
listenerMiddleware.startListening({
|
|
582
|
+
matcher: (0, import_toolkit14.isAnyOf)(npcsActions.moveNPC, npcsActions.patchNPC, npcsActions.applyNpcVerdict),
|
|
583
|
+
effect: (action, listenerApi) => {
|
|
584
|
+
const state = listenerApi.getState();
|
|
585
|
+
if (npcsActions.moveNPC.match(action)) {
|
|
586
|
+
const npc = state.npcs.entities[action.payload.id];
|
|
587
|
+
if (npc) {
|
|
588
|
+
listenerApi.dispatch(uiActions.addMessage(`${npc.name} moved to ${action.payload.position.x},${action.payload.position.y}`));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// src/store.ts
|
|
595
|
+
var createTestGameStore = () => (0, import_toolkit15.configureStore)({
|
|
596
|
+
reducer: {
|
|
597
|
+
npcs: npcsReducer,
|
|
598
|
+
player: playerReducer,
|
|
599
|
+
grid: gridReducer,
|
|
600
|
+
stealth: stealthReducer,
|
|
601
|
+
social: socialReducer,
|
|
602
|
+
bridge: bridgeReducer,
|
|
603
|
+
memory: memoryReducer,
|
|
604
|
+
inventory: inventoryReducer,
|
|
605
|
+
soul: soulReducer,
|
|
606
|
+
ui: uiReducer,
|
|
607
|
+
transcript: transcriptReducer,
|
|
608
|
+
scenario: scenarioReducer,
|
|
609
|
+
harness: harnessReducer
|
|
610
|
+
},
|
|
611
|
+
middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(listenerMiddleware.middleware)
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// src/game.ts
|
|
615
|
+
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
616
|
+
var tryParseVerdict = (output2) => {
|
|
617
|
+
try {
|
|
618
|
+
const match = output2.match(/\{[\s\S]*\}/);
|
|
619
|
+
if (match) return JSON.parse(match[0]);
|
|
620
|
+
} catch {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
};
|
|
625
|
+
var applyScenarioInitialState = (step, store) => {
|
|
626
|
+
if (step.eventType === "stealth") {
|
|
627
|
+
store.dispatch(stealthActions.setDoorOpen(true));
|
|
628
|
+
store.dispatch(stealthActions.bumpAlert(25));
|
|
629
|
+
store.dispatch(
|
|
630
|
+
npcsActions.upsertNPC({
|
|
631
|
+
id: "doomguard",
|
|
632
|
+
name: "Doomguard Patrol",
|
|
633
|
+
faction: "Doomguards",
|
|
634
|
+
hp: 100,
|
|
635
|
+
suspicion: 40,
|
|
636
|
+
inventory: [],
|
|
637
|
+
knownSecrets: [],
|
|
638
|
+
position: { x: 5, y: 10 }
|
|
639
|
+
})
|
|
640
|
+
);
|
|
641
|
+
store.dispatch(
|
|
642
|
+
memoryActions.storeMemory({
|
|
643
|
+
id: "mem-door-001",
|
|
644
|
+
npcId: "doomguard",
|
|
645
|
+
text: "Armory door found open at x:5, y:12",
|
|
646
|
+
importance: 0.9
|
|
647
|
+
})
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (step.eventType === "social") {
|
|
651
|
+
store.dispatch(
|
|
652
|
+
npcsActions.upsertNPC({
|
|
653
|
+
id: "miller",
|
|
654
|
+
name: "Miller",
|
|
655
|
+
faction: "Neutral",
|
|
656
|
+
hp: 100,
|
|
657
|
+
suspicion: 50,
|
|
658
|
+
inventory: ["medkit"],
|
|
659
|
+
knownSecrets: ["player_stole_rations"],
|
|
660
|
+
position: { x: 5, y: 12 }
|
|
661
|
+
})
|
|
662
|
+
);
|
|
663
|
+
store.dispatch(socialActions.setDialogue("I know you took those rations..."));
|
|
664
|
+
store.dispatch(socialActions.setTradeOffer({ npcId: "miller", item: "medkit", price: 100 }));
|
|
665
|
+
store.dispatch(npcsActions.patchNPC({ id: "miller", patch: { suspicion: 75 } }));
|
|
666
|
+
}
|
|
667
|
+
if (step.eventType === "escape") {
|
|
668
|
+
store.dispatch(playerActions.setHidden(false));
|
|
669
|
+
}
|
|
670
|
+
if (step.eventType === "persistence") {
|
|
671
|
+
store.dispatch(soulActions.markSoulExported({ npcId: "doomguard", txId: "tx-demo-001" }));
|
|
672
|
+
store.dispatch(soulActions.markSoulImported("tx-demo-001"));
|
|
673
|
+
store.dispatch(memoryActions.clearMemoryForNpc("doomguard"));
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
var runGame = async ({
|
|
677
|
+
mode
|
|
678
|
+
}) => {
|
|
679
|
+
console.log("\n[VOID::WATCHER] Echoes session booting...");
|
|
680
|
+
console.log(`NEURAL_LINK_ESTABLISHED :: mode=${mode}
|
|
681
|
+
`);
|
|
682
|
+
const store = createTestGameStore();
|
|
683
|
+
store.dispatch(uiActions.setMode(mode));
|
|
684
|
+
store.dispatch(
|
|
685
|
+
npcsActions.upsertNPC({
|
|
686
|
+
id: "scout",
|
|
687
|
+
name: "Scout",
|
|
688
|
+
faction: "Player",
|
|
689
|
+
hp: 100,
|
|
690
|
+
suspicion: 0,
|
|
691
|
+
inventory: ["coin-pouch"],
|
|
692
|
+
knownSecrets: [],
|
|
693
|
+
position: { x: 1, y: 1 }
|
|
694
|
+
})
|
|
695
|
+
);
|
|
696
|
+
const rl = mode === "manual" ? (0, import_promises.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout }) : null;
|
|
697
|
+
console.log(renderLegend());
|
|
698
|
+
for (const step of store.getState().scenario.steps) {
|
|
699
|
+
if (mode === "manual") {
|
|
700
|
+
console.log(`
|
|
701
|
+
\u256D\u2500 ${step.title} \u2500\u256E`);
|
|
702
|
+
console.log(step.description);
|
|
703
|
+
console.log("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F\n");
|
|
704
|
+
console.log(renderGrid(store.getState()));
|
|
705
|
+
await rl?.question("> Press Enter to execute this trace segment...");
|
|
706
|
+
} else {
|
|
707
|
+
console.log(`
|
|
708
|
+
:: ${step.title}`);
|
|
709
|
+
console.log(step.description);
|
|
710
|
+
}
|
|
711
|
+
applyScenarioInitialState(step, store);
|
|
712
|
+
for (const command of step.commands) {
|
|
713
|
+
const result = await runCommand(command);
|
|
714
|
+
store.dispatch(harnessActions.markCovered(command.group));
|
|
715
|
+
store.dispatch(
|
|
716
|
+
transcriptActions.recordTranscript({
|
|
717
|
+
scenarioId: step.id,
|
|
718
|
+
commandGroup: command.group,
|
|
719
|
+
command: command.command,
|
|
720
|
+
expectedRoutes: command.expectedRoutes,
|
|
721
|
+
status: result.status,
|
|
722
|
+
output: result.output
|
|
723
|
+
})
|
|
724
|
+
);
|
|
725
|
+
if (command.group === "npc_process_chat") {
|
|
726
|
+
const verdict = tryParseVerdict(result.output);
|
|
727
|
+
if (verdict && verdict.action) {
|
|
728
|
+
const npcIdMatch = command.command.match(/npc (?:process|chat) (\w+)/);
|
|
729
|
+
const npcId = npcIdMatch ? npcIdMatch[1] : null;
|
|
730
|
+
if (npcId) {
|
|
731
|
+
store.dispatch(npcsActions.applyNpcVerdict({ id: npcId, verdict }));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (command.group === "bridge_validate") {
|
|
736
|
+
const validation = tryParseVerdict(result.output);
|
|
737
|
+
if (validation && !validation.valid) {
|
|
738
|
+
store.dispatch(uiActions.addMessage(`System Bridge Blocked Action: ${validation.reason}`));
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
console.log(`[${result.status}] ${command.command}`);
|
|
742
|
+
if (result.status === "error") {
|
|
743
|
+
console.log("LOG_ERR_CRITICAL :: command execution fault recorded");
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (mode === "autoplay") {
|
|
747
|
+
await delay(500);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
rl?.close();
|
|
751
|
+
const state = store.getState();
|
|
752
|
+
const missingGroups = selectMissingGroups(state.harness.covered);
|
|
753
|
+
const complete = missingGroups.length === 0;
|
|
754
|
+
const summary = complete ? `Run complete: all ${Object.keys(state.harness.covered).length} command groups were exercised.` : `Run incomplete: missing command groups -> ${missingGroups.join(", ")}`;
|
|
755
|
+
console.log("\n=== Transcript Summary // chrome rain ledger ===");
|
|
756
|
+
for (const entry of state.transcript.entries) {
|
|
757
|
+
console.log(
|
|
758
|
+
`${entry.at} | ${entry.commandGroup} | ${entry.status} | ${entry.command} | routes: ${entry.expectedRoutes.join(", ")}`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
console.log("\n\u16A0 \u16A2 \u16A6 \u16A8 \u16B1 \u16B2 \u16B7 \u16B9");
|
|
762
|
+
console.log(summary);
|
|
763
|
+
return {
|
|
764
|
+
complete,
|
|
765
|
+
missingGroups,
|
|
766
|
+
transcript: state.transcript.entries,
|
|
767
|
+
summary
|
|
768
|
+
};
|
|
769
|
+
};
|
|
770
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
771
|
+
0 && (module.exports = {
|
|
772
|
+
REQUIRED_GROUPS,
|
|
773
|
+
runGame
|
|
774
|
+
});
|