@sharpee/engine 3.1.0 → 3.5.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.
Files changed (126) hide show
  1. package/action-context-factory.d.ts +9 -2
  2. package/action-context-factory.d.ts.map +1 -1
  3. package/action-context-factory.js +15 -5
  4. package/action-context-factory.js.map +1 -1
  5. package/command-executor.d.ts +10 -4
  6. package/command-executor.d.ts.map +1 -1
  7. package/command-executor.js +27 -20
  8. package/command-executor.js.map +1 -1
  9. package/game-engine.d.ts +49 -11
  10. package/game-engine.d.ts.map +1 -1
  11. package/game-engine.js +133 -87
  12. package/game-engine.js.map +1 -1
  13. package/index.d.ts +20 -18
  14. package/index.d.ts.map +1 -1
  15. package/index.js +26 -23
  16. package/index.js.map +1 -1
  17. package/language-provider-interface.d.ts +1 -1
  18. package/language-provider-interface.d.ts.map +1 -1
  19. package/narrative/index.d.ts +1 -1
  20. package/narrative/index.d.ts.map +1 -1
  21. package/narrative/index.js +3 -3
  22. package/narrative/index.js.map +1 -1
  23. package/package.json +12 -12
  24. package/platform-operations.d.ts +2 -2
  25. package/platform-operations.d.ts.map +1 -1
  26. package/platform-operations.js +4 -4
  27. package/platform-operations.js.map +1 -1
  28. package/prose-pipeline/assemble.js +2 -2
  29. package/prose-pipeline/assemble.js.map +1 -1
  30. package/prose-pipeline/decorations/index.d.ts +4 -4
  31. package/prose-pipeline/decorations/index.d.ts.map +1 -1
  32. package/prose-pipeline/decorations/index.js +7 -7
  33. package/prose-pipeline/decorations/index.js.map +1 -1
  34. package/prose-pipeline/decorations/parser.d.ts +1 -1
  35. package/prose-pipeline/decorations/parser.d.ts.map +1 -1
  36. package/prose-pipeline/decorations/parser.js +6 -6
  37. package/prose-pipeline/decorations/parser.js.map +1 -1
  38. package/prose-pipeline/decorations/resolver.js +2 -2
  39. package/prose-pipeline/decorations/resolver.js.map +1 -1
  40. package/prose-pipeline/handlers/audibility.d.ts +1 -1
  41. package/prose-pipeline/handlers/audibility.d.ts.map +1 -1
  42. package/prose-pipeline/handlers/audibility.js +7 -7
  43. package/prose-pipeline/handlers/audibility.js.map +1 -1
  44. package/prose-pipeline/handlers/banner.d.ts +1 -1
  45. package/prose-pipeline/handlers/banner.d.ts.map +1 -1
  46. package/prose-pipeline/handlers/banner.js +8 -8
  47. package/prose-pipeline/handlers/banner.js.map +1 -1
  48. package/prose-pipeline/handlers/client-query.d.ts +1 -1
  49. package/prose-pipeline/handlers/client-query.d.ts.map +1 -1
  50. package/prose-pipeline/handlers/client-query.js +2 -2
  51. package/prose-pipeline/handlers/client-query.js.map +1 -1
  52. package/prose-pipeline/handlers/command-failed.d.ts +1 -1
  53. package/prose-pipeline/handlers/command-failed.d.ts.map +1 -1
  54. package/prose-pipeline/handlers/command-failed.js +4 -4
  55. package/prose-pipeline/handlers/command-failed.js.map +1 -1
  56. package/prose-pipeline/handlers/domain-message.d.ts +1 -1
  57. package/prose-pipeline/handlers/domain-message.d.ts.map +1 -1
  58. package/prose-pipeline/handlers/domain-message.js +15 -8
  59. package/prose-pipeline/handlers/domain-message.js.map +1 -1
  60. package/prose-pipeline/handlers/game.d.ts +1 -1
  61. package/prose-pipeline/handlers/game.d.ts.map +1 -1
  62. package/prose-pipeline/handlers/game.js +2 -2
  63. package/prose-pipeline/handlers/game.js.map +1 -1
  64. package/prose-pipeline/handlers/generic.d.ts +1 -1
  65. package/prose-pipeline/handlers/generic.d.ts.map +1 -1
  66. package/prose-pipeline/handlers/generic.js +12 -12
  67. package/prose-pipeline/handlers/generic.js.map +1 -1
  68. package/prose-pipeline/handlers/implicit-take.d.ts +1 -1
  69. package/prose-pipeline/handlers/implicit-take.d.ts.map +1 -1
  70. package/prose-pipeline/handlers/implicit-take.js +2 -2
  71. package/prose-pipeline/handlers/implicit-take.js.map +1 -1
  72. package/prose-pipeline/handlers/index.d.ts +11 -11
  73. package/prose-pipeline/handlers/index.d.ts.map +1 -1
  74. package/prose-pipeline/handlers/index.js +21 -21
  75. package/prose-pipeline/handlers/index.js.map +1 -1
  76. package/prose-pipeline/handlers/platform.d.ts +1 -1
  77. package/prose-pipeline/handlers/platform.d.ts.map +1 -1
  78. package/prose-pipeline/handlers/platform.js +5 -5
  79. package/prose-pipeline/handlers/platform.js.map +1 -1
  80. package/prose-pipeline/handlers/revealed.d.ts +1 -1
  81. package/prose-pipeline/handlers/revealed.d.ts.map +1 -1
  82. package/prose-pipeline/handlers/revealed.js +4 -4
  83. package/prose-pipeline/handlers/revealed.js.map +1 -1
  84. package/prose-pipeline/handlers/room.d.ts +1 -1
  85. package/prose-pipeline/handlers/room.d.ts.map +1 -1
  86. package/prose-pipeline/handlers/room.js +9 -9
  87. package/prose-pipeline/handlers/room.js.map +1 -1
  88. package/prose-pipeline/handlers/types.d.ts +7 -1
  89. package/prose-pipeline/handlers/types.d.ts.map +1 -1
  90. package/prose-pipeline/index.d.ts +11 -9
  91. package/prose-pipeline/index.d.ts.map +1 -1
  92. package/prose-pipeline/index.js +36 -32
  93. package/prose-pipeline/index.js.map +1 -1
  94. package/prose-pipeline/phrase-render.d.ts +28 -1
  95. package/prose-pipeline/phrase-render.d.ts.map +1 -1
  96. package/prose-pipeline/phrase-render.js +29 -2
  97. package/prose-pipeline/phrase-render.js.map +1 -1
  98. package/prose-pipeline/pipeline.d.ts +4 -5
  99. package/prose-pipeline/pipeline.d.ts.map +1 -1
  100. package/prose-pipeline/pipeline.js +31 -31
  101. package/prose-pipeline/pipeline.js.map +1 -1
  102. package/prose-pipeline/render-context.d.ts +2 -0
  103. package/prose-pipeline/render-context.d.ts.map +1 -1
  104. package/prose-pipeline/render-context.js.map +1 -1
  105. package/prose-pipeline/stages/index.d.ts +2 -2
  106. package/prose-pipeline/stages/index.d.ts.map +1 -1
  107. package/prose-pipeline/stages/index.js +5 -5
  108. package/prose-pipeline/stages/index.js.map +1 -1
  109. package/save-restore-service.d.ts +9 -3
  110. package/save-restore-service.d.ts.map +1 -1
  111. package/save-restore-service.js +15 -1
  112. package/save-restore-service.js.map +1 -1
  113. package/sound/dispatcher.js +2 -2
  114. package/sound/dispatcher.js.map +1 -1
  115. package/sound/index.d.ts +2 -2
  116. package/sound/index.d.ts.map +1 -1
  117. package/sound/index.js +6 -6
  118. package/sound/index.js.map +1 -1
  119. package/story.d.ts +10 -3
  120. package/story.d.ts.map +1 -1
  121. package/story.js +2 -2
  122. package/story.js.map +1 -1
  123. package/test-helpers/mock-prose-pipeline.d.ts +1 -1
  124. package/test-helpers/mock-prose-pipeline.d.ts.map +1 -1
  125. package/turn-event-processor.d.ts +1 -1
  126. package/turn-event-processor.d.ts.map +1 -1
package/game-engine.js CHANGED
@@ -10,25 +10,25 @@ exports.splitChainedInput = splitChainedInput;
10
10
  const world_model_1 = require("@sharpee/world-model");
11
11
  const event_processor_1 = require("@sharpee/event-processor");
12
12
  const stdlib_1 = require("@sharpee/stdlib");
13
- const prose_pipeline_1 = require("./prose-pipeline");
13
+ const index_js_1 = require("./prose-pipeline/index.js");
14
14
  const text_blocks_1 = require("@sharpee/text-blocks");
15
15
  const channel_service_1 = require("@sharpee/channel-service");
16
16
  const core_1 = require("@sharpee/core");
17
17
  const plugins_1 = require("@sharpee/plugins");
18
- const scene_evaluation_plugin_1 = require("./scene-evaluation-plugin");
19
- const types_1 = require("./types");
20
- const narrative_1 = require("./narrative");
21
- const snippet_validation_1 = require("./snippet-validation");
22
- const combatant_health_validation_1 = require("./combatant-health-validation");
23
- const command_executor_1 = require("./command-executor");
24
- const action_context_factory_1 = require("./action-context-factory");
25
- const sound_1 = require("./sound");
26
- const turn_event_processor_1 = require("./turn-event-processor");
27
- const parser_interface_1 = require("./parser-interface");
28
- const language_provider_interface_1 = require("./language-provider-interface");
29
- const vocabulary_manager_1 = require("./vocabulary-manager");
30
- const save_restore_service_1 = require("./save-restore-service");
31
- const turn_event_processor_2 = require("./turn-event-processor");
18
+ const scene_evaluation_plugin_js_1 = require("./scene-evaluation-plugin.js");
19
+ const types_js_1 = require("./types.js");
20
+ const index_js_2 = require("./narrative/index.js");
21
+ const snippet_validation_js_1 = require("./snippet-validation.js");
22
+ const combatant_health_validation_js_1 = require("./combatant-health-validation.js");
23
+ const command_executor_js_1 = require("./command-executor.js");
24
+ const action_context_factory_js_1 = require("./action-context-factory.js");
25
+ const index_js_3 = require("./sound/index.js");
26
+ const turn_event_processor_js_1 = require("./turn-event-processor.js");
27
+ const parser_interface_js_1 = require("./parser-interface.js");
28
+ const language_provider_interface_js_1 = require("./language-provider-interface.js");
29
+ const vocabulary_manager_js_1 = require("./vocabulary-manager.js");
30
+ const save_restore_service_js_1 = require("./save-restore-service.js");
31
+ const turn_event_processor_js_2 = require("./turn-event-processor.js");
32
32
  /**
33
33
  * Conservative client-capability profile used when `start()` is called
34
34
  * without an explicit `capabilities` option. Mirrors a CLI / text-only
@@ -97,8 +97,16 @@ class GameEngine {
97
97
  * room for future extension seams (e.g., custom propagate injection
98
98
  * via `setSoundDispatcher` in tests).
99
99
  */
100
- soundDispatcher = new sound_1.SoundDispatcher();
100
+ soundDispatcher = new index_js_3.SoundDispatcher();
101
101
  random;
102
+ /**
103
+ * Dedicated action RNG stream (ADR-231 D6), exposed to actions as
104
+ * `ActionContext.random`. A separate instance from `random` (the
105
+ * turn-plugin stream) so plugin draws can never shift action rolls;
106
+ * its seed rides the save blob (`IEngineState.actionRngSeed`) so
107
+ * post-restore action outcomes replay deterministically.
108
+ */
109
+ actionRandom;
102
110
  narrativeSettings;
103
111
  // Alternate input mode handlers (ADR-137)
104
112
  inputModeHandlers = new Map();
@@ -188,29 +196,32 @@ class GameEngine {
188
196
  });
189
197
  });
190
198
  this.pluginRegistry = new plugins_1.PluginRegistry();
191
- this.pluginRegistry.register(new scene_evaluation_plugin_1.SceneEvaluationPlugin());
199
+ this.pluginRegistry.register(new scene_evaluation_plugin_js_1.SceneEvaluationPlugin());
192
200
  this.random = (0, core_1.createSeededRandom)();
193
- this.narrativeSettings = (0, narrative_1.buildNarrativeSettings)(); // Default: 2nd person
201
+ // ADR-231 D6: dedicated action stream unseeded construction gives a
202
+ // time-based initial seed (same pattern as `random` above)
203
+ this.actionRandom = (0, core_1.createSeededRandom)();
204
+ this.narrativeSettings = (0, index_js_2.buildNarrativeSettings)(); // Default: 2nd person
194
205
  // Initialize extracted services (Phase 4 remediation)
195
- this.vocabularyManager = (0, vocabulary_manager_1.createVocabularyManager)();
196
- this.saveRestoreService = (0, save_restore_service_1.createSaveRestoreService)({
206
+ this.vocabularyManager = (0, vocabulary_manager_js_1.createVocabularyManager)();
207
+ this.saveRestoreService = (0, save_restore_service_js_1.createSaveRestoreService)({
197
208
  maxSnapshots: this.config.maxUndoSnapshots ?? 10
198
209
  });
199
- this.turnEventProcessor = (0, turn_event_processor_2.createTurnEventProcessor)(this.perceptionService);
210
+ this.turnEventProcessor = (0, turn_event_processor_js_2.createTurnEventProcessor)(this.perceptionService);
200
211
  // Set provided dependencies
201
212
  this.languageProvider = options.language;
202
213
  this.parser = options.parser;
203
- this.textService = new prose_pipeline_1.ProsePipeline(this.languageProvider, this.world);
214
+ this.textService = new index_js_1.ProsePipeline(this.languageProvider, this.world);
204
215
  // Update action registry with language provider
205
216
  this.actionRegistry.setLanguageProvider(this.languageProvider);
206
217
  // Wire parser with platform events if supported
207
- if (this.parser && (0, parser_interface_1.hasPlatformEventEmitter)(this.parser)) {
218
+ if (this.parser && (0, parser_interface_js_1.hasPlatformEventEmitter)(this.parser)) {
208
219
  this.parser.setPlatformEventEmitter((event) => {
209
220
  this.platformEvents.addEvent(event);
210
221
  });
211
222
  }
212
223
  // Create command executor with dependencies
213
- this.commandExecutor = (0, command_executor_1.createCommandExecutor)(this.world, this.actionRegistry, this.eventProcessor, this.parser, this.systemEventSource);
224
+ this.commandExecutor = (0, command_executor_js_1.createCommandExecutor)(this.world, this.actionRegistry, this.eventProcessor, this.parser, this.systemEventSource, this.actionRandom);
214
225
  // ADR-224: auto-register the deadly-room death transformer so every story
215
226
  // (TS or Chord) gets the deadly-room verb-allowlist / probabilistic hazard for
216
227
  // free — no story wiring needed. It early-returns when the player's room has no
@@ -230,7 +241,7 @@ class GameEngine {
230
241
  this.emitGameEvent(loadingEvent);
231
242
  this.story = story;
232
243
  // Build narrative settings from story config (ADR-089)
233
- this.narrativeSettings = (0, narrative_1.buildNarrativeSettings)(story.config.narrative);
244
+ this.narrativeSettings = (0, index_js_2.buildNarrativeSettings)(story.config.narrative);
234
245
  // Create player first so initializeWorld() can place them
235
246
  const newPlayer = story.createPlayer(this.world);
236
247
  this.context.player = newPlayer;
@@ -254,9 +265,9 @@ class GameEngine {
254
265
  story.initializeWorld(this.world);
255
266
  // ADR-209 AC-5: fail load synchronously (naming room and marker) if any
256
267
  // snippet-bearing room's description carries an unbound {snippet:name}.
257
- (0, snippet_validation_1.validateRoomSnippets)(this.world);
268
+ (0, snippet_validation_js_1.validateRoomSnippets)(this.world);
258
269
  // ADR-226 AC-7: every combatant must carry the HealthTrait combat operates on.
259
- (0, combatant_health_validation_1.validateCombatantHealth)(this.world);
270
+ (0, combatant_health_validation_js_1.validateCombatantHealth)(this.world);
260
271
  // Configure language provider with narrative settings (ADR-089)
261
272
  this.configureLanguageProviderNarrative(newPlayer);
262
273
  // Update metadata
@@ -636,6 +647,30 @@ class GameEngine {
636
647
  });
637
648
  this.emit('channel:packet', packet, turn);
638
649
  }
650
+ /**
651
+ * Resume a stopped engine without touching world state.
652
+ *
653
+ * The post-mortem revival seam: after `stop('defeat')`, a harness (or a
654
+ * story resurrection policy) that has restored the world to a live-player
655
+ * snapshot — e.g. the transcript-tester's RETRY block via
656
+ * `world.loadJSON()` — needs turn execution back without any world
657
+ * teardown (a full reboot would clear the world it just restored).
658
+ * Flips `running` back on; emits nothing, rebuilds nothing.
659
+ *
660
+ * No-op when already running. Throws if the engine was never started
661
+ * (no command executor) — resuming presumes a completed `start()`.
662
+ */
663
+ resume() {
664
+ if (this.running) {
665
+ return;
666
+ }
667
+ // The channel service exists only once start() has completed — resuming
668
+ // an engine that never started would bypass the whole bootstrap order.
669
+ if (!this.channelService) {
670
+ throw new Error('Engine must have been started before it can resume');
671
+ }
672
+ this.running = true;
673
+ }
639
674
  /**
640
675
  * Stop the game engine
641
676
  */
@@ -682,41 +717,22 @@ class GameEngine {
682
717
  this.emit('game:over', this.context);
683
718
  }
684
719
  /**
685
- * Restart the game from scratch.
720
+ * Build the restart acknowledgment event (ADR-248).
686
721
  *
687
- * Clears the world, resets engine state, and re-initializes the story.
688
- * Called from both processMetaPlatformOperation and processPlatformOperations.
722
+ * On confirmed restart the engine does NOT rebuild in place — it renders
723
+ * this acknowledgment ("The story restarts.") in the final packet, then
724
+ * stops with reason 'restart'; the client owns the reboot via its own
725
+ * boot path. No pre-emptive restart_completed(true) is emitted: the new
726
+ * boot's opening banner is the success signal.
689
727
  */
690
- async restartGame() {
691
- if (!this.story)
692
- return;
693
- // Stop engine if running
694
- if (this.running) {
695
- this.stop();
696
- }
697
- // Reset pronoun context
698
- if (this.parser && (0, parser_interface_1.hasPronounContext)(this.parser)) {
699
- this.parser.resetPronounContext();
700
- }
701
- // Clear world state (entities, spatial index, relationships, etc.)
702
- this.world.clear();
703
- // Reset engine context
704
- this.context.currentTurn = 1;
705
- this.context.history = [];
706
- this.context.metadata.started = new Date();
707
- this.context.metadata.lastPlayed = new Date();
708
- // Clear engine bookkeeping
709
- this.turnEvents.clear();
710
- this.pendingPlatformOps = [];
711
- this.soundBuffer.length = 0;
712
- this.saveRestoreService.clearUndoSnapshots();
713
- this.hasEmittedInitialized = false;
714
- // Clear plugin registry so story can re-register plugins
715
- this.pluginRegistry.clear();
716
- // Re-initialize the story (creates entities, player, custom actions, etc.)
717
- this.setStory(this.story);
718
- // Start the engine (emits game.initialized + game.started)
719
- this.start();
728
+ createRestartAckEvent() {
729
+ return {
730
+ id: `restart_ack_${Date.now()}`,
731
+ type: 'game.message',
732
+ timestamp: Date.now(),
733
+ data: { messageId: 'if.action.restarting.game_restarting' },
734
+ entities: {}
735
+ };
720
736
  }
721
737
  /**
722
738
  * Execute a turn
@@ -734,7 +750,7 @@ class GameEngine {
734
750
  // the line, matching player expectations from Inform-style interpreters.
735
751
  // Skipped while an alternate input mode is active (ADR-137) — mode
736
752
  // handlers own the raw line, punctuation and all.
737
- if (typeof input === 'string' && !this.world.getStateValue(types_1.INPUT_MODE_STATE_KEY)) {
753
+ if (typeof input === 'string' && !this.world.getStateValue(types_js_1.INPUT_MODE_STATE_KEY)) {
738
754
  const statements = splitChainedInput(input);
739
755
  if (statements.length > 1) {
740
756
  let result;
@@ -793,7 +809,7 @@ class GameEngine {
793
809
  }
794
810
  this.emit('turn:start', turn, input);
795
811
  // Check for alternate input mode (ADR-137)
796
- const activeModeId = this.world.getStateValue(types_1.INPUT_MODE_STATE_KEY);
812
+ const activeModeId = this.world.getStateValue(types_js_1.INPUT_MODE_STATE_KEY);
797
813
  if (activeModeId) {
798
814
  const handler = this.inputModeHandlers.get(activeModeId);
799
815
  if (handler) {
@@ -808,7 +824,7 @@ class GameEngine {
808
824
  if (this.parser) {
809
825
  // Set world context for parser
810
826
  const player = this.world.getPlayer();
811
- if (player && (0, parser_interface_1.hasWorldContext)(this.parser)) {
827
+ if (player && (0, parser_interface_js_1.hasWorldContext)(this.parser)) {
812
828
  const playerLocation = this.world.getLocation(player.id) || '';
813
829
  this.parser.setWorldContext(this.world, player.id, playerLocation);
814
830
  }
@@ -851,7 +867,7 @@ class GameEngine {
851
867
  locationId: playerLocation
852
868
  };
853
869
  // Store events for this turn (process through enrichment pipeline)
854
- let semanticEvents = result.events.map(e => (0, turn_event_processor_1.processEvent)(e, enrichmentContext));
870
+ let semanticEvents = result.events.map(e => (0, turn_event_processor_js_1.processEvent)(e, enrichmentContext));
855
871
  // Apply perception filtering if service is configured
856
872
  // This transforms events based on what the player can perceive
857
873
  if (this.perceptionService) {
@@ -873,7 +889,7 @@ class GameEngine {
873
889
  if (result.success) {
874
890
  this.updateCommandHistory(result, input, turn);
875
891
  // Update pronoun context for "it"/"them"/"him"/"her" resolution (ADR-089)
876
- if (this.parser && (0, parser_interface_1.hasPronounContext)(this.parser) && result.validatedCommand) {
892
+ if (this.parser && (0, parser_interface_js_1.hasPronounContext)(this.parser) && result.validatedCommand) {
877
893
  this.parser.updatePronounContext(result.validatedCommand, turn);
878
894
  }
879
895
  }
@@ -908,6 +924,16 @@ class GameEngine {
908
924
  const playerLocation = this.world.getLocation(this.context.player.id);
909
925
  // Plugin tick loop (ADR-120)
910
926
  // Plugins run in priority order (NPC at 100, state machines at 75, scheduler at 50)
927
+ //
928
+ // actionResult.success must reflect GENUINE action success (Phase 7):
929
+ // result.success only checks for action.error events, but modern
930
+ // blocked() paths reuse the primary event type with blocked:true /
931
+ // failed:true — a refused action would otherwise report success and
932
+ // (e.g.) advance state-machine transitions it never earned.
933
+ const actionRefused = semanticEvents.some(e => {
934
+ const data = e.data;
935
+ return data?.blocked === true || data?.failed === true;
936
+ });
911
937
  const pluginContext = {
912
938
  world: this.world,
913
939
  turn,
@@ -916,7 +942,7 @@ class GameEngine {
916
942
  random: this.random,
917
943
  actionResult: {
918
944
  actionId: result.actionId || '',
919
- success: result.success,
945
+ success: result.success && !actionRefused,
920
946
  targetId: result.validatedCommand?.directObject?.entity?.id,
921
947
  },
922
948
  actionEvents: semanticEvents,
@@ -1101,7 +1127,7 @@ class GameEngine {
1101
1127
  }
1102
1128
  // Create action context for meta-command execution
1103
1129
  const scopeResolver = (0, stdlib_1.createScopeResolver)(this.world);
1104
- const actionContext = (0, action_context_factory_1.createActionContext)(this.world, this.context, command, action, scopeResolver);
1130
+ const actionContext = (0, action_context_factory_js_1.createActionContext)(this.world, this.context, command, action, scopeResolver, undefined, this.actionRandom);
1105
1131
  // Run action's four-phase pattern
1106
1132
  const actionValidation = action.validate(actionContext);
1107
1133
  let actionEvents;
@@ -1267,20 +1293,19 @@ class GameEngine {
1267
1293
  }
1268
1294
  case core_1.PlatformEventType.RESTART_REQUESTED: {
1269
1295
  const context = platformOp.payload.context;
1270
- if (this.saveRestoreHooks?.onRestartRequested) {
1271
- const shouldRestart = await this.saveRestoreHooks.onRestartRequested(context);
1272
- if (shouldRestart) {
1273
- await this.restartGame();
1274
- completionEvents.push((0, core_1.createRestartCompletedEvent)(true));
1275
- }
1276
- else {
1277
- completionEvents.push((0, core_1.createRestartCompletedEvent)(false));
1278
- }
1296
+ const shouldRestart = this.saveRestoreHooks?.onRestartRequested
1297
+ ? await this.saveRestoreHooks.onRestartRequested(context)
1298
+ : true; // No restart hook — auto-confirm
1299
+ if (shouldRestart) {
1300
+ // ADR-248: ack in the final packet, then stop('restart'). The
1301
+ // client's hook is the reboot trigger; the stop reason is
1302
+ // bookkeeping. No restart_completed(true) — the reboot's opening
1303
+ // banner is the success signal.
1304
+ completionEvents.push(this.createRestartAckEvent());
1305
+ this.stop('restart');
1279
1306
  }
1280
1307
  else {
1281
- // No restart hook — auto-confirm
1282
- await this.restartGame();
1283
- completionEvents.push((0, core_1.createRestartCompletedEvent)(true));
1308
+ completionEvents.push((0, core_1.createRestartCompletedEvent)(false));
1284
1309
  }
1285
1310
  break;
1286
1311
  }
@@ -1406,7 +1431,7 @@ class GameEngine {
1406
1431
  */
1407
1432
  configureLanguageProviderNarrative(player) {
1408
1433
  // Check if language provider supports narrative settings
1409
- if (!this.languageProvider || !(0, language_provider_interface_1.hasNarrativeSettings)(this.languageProvider)) {
1434
+ if (!this.languageProvider || !(0, language_provider_interface_js_1.hasNarrativeSettings)(this.languageProvider)) {
1410
1435
  return;
1411
1436
  }
1412
1437
  // Build narrative context for language provider
@@ -1447,11 +1472,11 @@ class GameEngine {
1447
1472
  throw new Error(`Cannot sync player state: entity '${newPlayerId}' not found`);
1448
1473
  }
1449
1474
  this.context.player = newPlayer;
1450
- if (this.parser && (0, parser_interface_1.hasWorldContext)(this.parser)) {
1475
+ if (this.parser && (0, parser_interface_js_1.hasWorldContext)(this.parser)) {
1451
1476
  const playerLocation = this.world.getLocation(newPlayerId) || '';
1452
1477
  this.parser.setWorldContext(this.world, newPlayerId, playerLocation);
1453
1478
  }
1454
- if (this.parser && (0, parser_interface_1.hasPronounContext)(this.parser)) {
1479
+ if (this.parser && (0, parser_interface_js_1.hasPronounContext)(this.parser)) {
1455
1480
  this.parser.resetPronounContext();
1456
1481
  }
1457
1482
  this.updateScopeVocabulary();
@@ -1463,6 +1488,23 @@ class GameEngine {
1463
1488
  getPluginRegistry() {
1464
1489
  return this.pluginRegistry;
1465
1490
  }
1491
+ /**
1492
+ * The negotiated client capabilities for this session (ADR-216): the
1493
+ * `client has <capability>` predicate reads these live, and channel
1494
+ * gating uses the same flags at manifest time. Text-only before
1495
+ * `start({ capabilities })` runs or when none were negotiated.
1496
+ */
1497
+ getClientCapabilities() {
1498
+ return this.clientCapabilities ?? exports.DEFAULT_TEXT_CAPABILITIES;
1499
+ }
1500
+ /**
1501
+ * Get the dedicated action RNG stream (ADR-231 D6). Part of the
1502
+ * ISaveRestoreStateProvider contract — the save service persists this
1503
+ * stream's seed and the restore path re-seeds it.
1504
+ */
1505
+ getActionRandom() {
1506
+ return this.actionRandom;
1507
+ }
1466
1508
  /**
1467
1509
  * Get event processor for handler registration (ADR-075)
1468
1510
  */
@@ -1698,7 +1740,7 @@ class GameEngine {
1698
1740
  playerId: this.context.player.id,
1699
1741
  locationId: playerLocation ?? undefined
1700
1742
  };
1701
- let processed = events.map(e => (0, turn_event_processor_1.processEvent)(e, enrichmentContext));
1743
+ let processed = events.map(e => (0, turn_event_processor_js_1.processEvent)(e, enrichmentContext));
1702
1744
  if (this.perceptionService) {
1703
1745
  processed = this.perceptionService.filterEvents(processed, this.context.player, this.world);
1704
1746
  }
@@ -2009,10 +2051,14 @@ class GameEngine {
2009
2051
  shouldRestart = await this.saveRestoreHooks.onRestartRequested(context);
2010
2052
  }
2011
2053
  if (shouldRestart) {
2012
- const completionEvent = (0, core_1.createRestartCompletedEvent)(true);
2013
- this.eventSource.emit(completionEvent);
2014
- this.emit('event', completionEvent);
2015
- await this.restartGame();
2054
+ // ADR-248: ack in the final packet, then stop('restart').
2055
+ // No restart_completed(true) — the client reboots and its
2056
+ // opening banner is the success signal.
2057
+ const ackEvent = this.createRestartAckEvent();
2058
+ this.eventSource.emit(ackEvent);
2059
+ this.turnEvents.get(currentTurn)?.push(ackEvent);
2060
+ this.emit('event', ackEvent);
2061
+ this.stop('restart');
2016
2062
  }
2017
2063
  else {
2018
2064
  const cancelEvent = (0, core_1.createRestartCompletedEvent)(false);