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