@sharpee/engine 3.0.0 → 3.3.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 (130) 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/combatant-health-validation.d.ts +38 -0
  6. package/combatant-health-validation.d.ts.map +1 -0
  7. package/combatant-health-validation.js +54 -0
  8. package/combatant-health-validation.js.map +1 -0
  9. package/command-executor.d.ts +10 -4
  10. package/command-executor.d.ts.map +1 -1
  11. package/command-executor.js +27 -20
  12. package/command-executor.js.map +1 -1
  13. package/game-engine.d.ts +65 -11
  14. package/game-engine.d.ts.map +1 -1
  15. package/game-engine.js +185 -85
  16. package/game-engine.js.map +1 -1
  17. package/index.d.ts +20 -17
  18. package/index.d.ts.map +1 -1
  19. package/index.js +26 -22
  20. package/index.js.map +1 -1
  21. package/language-provider-interface.d.ts +1 -1
  22. package/language-provider-interface.d.ts.map +1 -1
  23. package/narrative/index.d.ts +1 -1
  24. package/narrative/index.d.ts.map +1 -1
  25. package/narrative/index.js +3 -3
  26. package/narrative/index.js.map +1 -1
  27. package/package.json +12 -12
  28. package/platform-operations.d.ts +2 -2
  29. package/platform-operations.d.ts.map +1 -1
  30. package/platform-operations.js +4 -4
  31. package/platform-operations.js.map +1 -1
  32. package/prose-pipeline/assemble.js +2 -2
  33. package/prose-pipeline/assemble.js.map +1 -1
  34. package/prose-pipeline/decorations/index.d.ts +4 -4
  35. package/prose-pipeline/decorations/index.d.ts.map +1 -1
  36. package/prose-pipeline/decorations/index.js +7 -7
  37. package/prose-pipeline/decorations/index.js.map +1 -1
  38. package/prose-pipeline/decorations/parser.d.ts +1 -1
  39. package/prose-pipeline/decorations/parser.d.ts.map +1 -1
  40. package/prose-pipeline/decorations/parser.js +6 -6
  41. package/prose-pipeline/decorations/parser.js.map +1 -1
  42. package/prose-pipeline/decorations/resolver.js +2 -2
  43. package/prose-pipeline/decorations/resolver.js.map +1 -1
  44. package/prose-pipeline/handlers/audibility.d.ts +1 -1
  45. package/prose-pipeline/handlers/audibility.d.ts.map +1 -1
  46. package/prose-pipeline/handlers/audibility.js +7 -7
  47. package/prose-pipeline/handlers/audibility.js.map +1 -1
  48. package/prose-pipeline/handlers/banner.d.ts +1 -1
  49. package/prose-pipeline/handlers/banner.d.ts.map +1 -1
  50. package/prose-pipeline/handlers/banner.js +8 -8
  51. package/prose-pipeline/handlers/banner.js.map +1 -1
  52. package/prose-pipeline/handlers/client-query.d.ts +1 -1
  53. package/prose-pipeline/handlers/client-query.d.ts.map +1 -1
  54. package/prose-pipeline/handlers/client-query.js +2 -2
  55. package/prose-pipeline/handlers/client-query.js.map +1 -1
  56. package/prose-pipeline/handlers/command-failed.d.ts +1 -1
  57. package/prose-pipeline/handlers/command-failed.d.ts.map +1 -1
  58. package/prose-pipeline/handlers/command-failed.js +4 -4
  59. package/prose-pipeline/handlers/command-failed.js.map +1 -1
  60. package/prose-pipeline/handlers/domain-message.d.ts +1 -1
  61. package/prose-pipeline/handlers/domain-message.d.ts.map +1 -1
  62. package/prose-pipeline/handlers/domain-message.js +15 -8
  63. package/prose-pipeline/handlers/domain-message.js.map +1 -1
  64. package/prose-pipeline/handlers/game.d.ts +1 -1
  65. package/prose-pipeline/handlers/game.d.ts.map +1 -1
  66. package/prose-pipeline/handlers/game.js +2 -2
  67. package/prose-pipeline/handlers/game.js.map +1 -1
  68. package/prose-pipeline/handlers/generic.d.ts +1 -1
  69. package/prose-pipeline/handlers/generic.d.ts.map +1 -1
  70. package/prose-pipeline/handlers/generic.js +12 -12
  71. package/prose-pipeline/handlers/generic.js.map +1 -1
  72. package/prose-pipeline/handlers/implicit-take.d.ts +1 -1
  73. package/prose-pipeline/handlers/implicit-take.d.ts.map +1 -1
  74. package/prose-pipeline/handlers/implicit-take.js +2 -2
  75. package/prose-pipeline/handlers/implicit-take.js.map +1 -1
  76. package/prose-pipeline/handlers/index.d.ts +11 -11
  77. package/prose-pipeline/handlers/index.d.ts.map +1 -1
  78. package/prose-pipeline/handlers/index.js +21 -21
  79. package/prose-pipeline/handlers/index.js.map +1 -1
  80. package/prose-pipeline/handlers/platform.d.ts +1 -1
  81. package/prose-pipeline/handlers/platform.d.ts.map +1 -1
  82. package/prose-pipeline/handlers/platform.js +5 -5
  83. package/prose-pipeline/handlers/platform.js.map +1 -1
  84. package/prose-pipeline/handlers/revealed.d.ts +1 -1
  85. package/prose-pipeline/handlers/revealed.d.ts.map +1 -1
  86. package/prose-pipeline/handlers/revealed.js +4 -4
  87. package/prose-pipeline/handlers/revealed.js.map +1 -1
  88. package/prose-pipeline/handlers/room.d.ts +1 -1
  89. package/prose-pipeline/handlers/room.d.ts.map +1 -1
  90. package/prose-pipeline/handlers/room.js +9 -9
  91. package/prose-pipeline/handlers/room.js.map +1 -1
  92. package/prose-pipeline/handlers/types.d.ts +7 -1
  93. package/prose-pipeline/handlers/types.d.ts.map +1 -1
  94. package/prose-pipeline/index.d.ts +11 -9
  95. package/prose-pipeline/index.d.ts.map +1 -1
  96. package/prose-pipeline/index.js +36 -32
  97. package/prose-pipeline/index.js.map +1 -1
  98. package/prose-pipeline/phrase-render.d.ts +28 -1
  99. package/prose-pipeline/phrase-render.d.ts.map +1 -1
  100. package/prose-pipeline/phrase-render.js +29 -2
  101. package/prose-pipeline/phrase-render.js.map +1 -1
  102. package/prose-pipeline/pipeline.d.ts +4 -5
  103. package/prose-pipeline/pipeline.d.ts.map +1 -1
  104. package/prose-pipeline/pipeline.js +31 -31
  105. package/prose-pipeline/pipeline.js.map +1 -1
  106. package/prose-pipeline/render-context.d.ts +2 -0
  107. package/prose-pipeline/render-context.d.ts.map +1 -1
  108. package/prose-pipeline/render-context.js.map +1 -1
  109. package/prose-pipeline/stages/index.d.ts +2 -2
  110. package/prose-pipeline/stages/index.d.ts.map +1 -1
  111. package/prose-pipeline/stages/index.js +5 -5
  112. package/prose-pipeline/stages/index.js.map +1 -1
  113. package/save-restore-service.d.ts +9 -3
  114. package/save-restore-service.d.ts.map +1 -1
  115. package/save-restore-service.js +15 -1
  116. package/save-restore-service.js.map +1 -1
  117. package/sound/dispatcher.js +2 -2
  118. package/sound/dispatcher.js.map +1 -1
  119. package/sound/index.d.ts +2 -2
  120. package/sound/index.d.ts.map +1 -1
  121. package/sound/index.js +6 -6
  122. package/sound/index.js.map +1 -1
  123. package/story.d.ts +10 -3
  124. package/story.d.ts.map +1 -1
  125. package/story.js +2 -2
  126. package/story.js.map +1 -1
  127. package/test-helpers/mock-prose-pipeline.d.ts +1 -1
  128. package/test-helpers/mock-prose-pipeline.d.ts.map +1 -1
  129. package/turn-event-processor.d.ts +1 -1
  130. package/turn-event-processor.d.ts.map +1 -1
package/game-engine.js CHANGED
@@ -10,24 +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 command_executor_1 = require("./command-executor");
23
- const action_context_factory_1 = require("./action-context-factory");
24
- const sound_1 = require("./sound");
25
- const turn_event_processor_1 = require("./turn-event-processor");
26
- const parser_interface_1 = require("./parser-interface");
27
- const language_provider_interface_1 = require("./language-provider-interface");
28
- const vocabulary_manager_1 = require("./vocabulary-manager");
29
- const save_restore_service_1 = require("./save-restore-service");
30
- 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");
31
32
  /**
32
33
  * Conservative client-capability profile used when `start()` is called
33
34
  * without an explicit `capabilities` option. Mirrors a CLI / text-only
@@ -96,8 +97,16 @@ class GameEngine {
96
97
  * room for future extension seams (e.g., custom propagate injection
97
98
  * via `setSoundDispatcher` in tests).
98
99
  */
99
- soundDispatcher = new sound_1.SoundDispatcher();
100
+ soundDispatcher = new index_js_3.SoundDispatcher();
100
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;
101
110
  narrativeSettings;
102
111
  // Alternate input mode handlers (ADR-137)
103
112
  inputModeHandlers = new Map();
@@ -187,29 +196,37 @@ class GameEngine {
187
196
  });
188
197
  });
189
198
  this.pluginRegistry = new plugins_1.PluginRegistry();
190
- this.pluginRegistry.register(new scene_evaluation_plugin_1.SceneEvaluationPlugin());
199
+ this.pluginRegistry.register(new scene_evaluation_plugin_js_1.SceneEvaluationPlugin());
191
200
  this.random = (0, core_1.createSeededRandom)();
192
- 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
193
205
  // Initialize extracted services (Phase 4 remediation)
194
- this.vocabularyManager = (0, vocabulary_manager_1.createVocabularyManager)();
195
- 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)({
196
208
  maxSnapshots: this.config.maxUndoSnapshots ?? 10
197
209
  });
198
- this.turnEventProcessor = (0, turn_event_processor_2.createTurnEventProcessor)(this.perceptionService);
210
+ this.turnEventProcessor = (0, turn_event_processor_js_2.createTurnEventProcessor)(this.perceptionService);
199
211
  // Set provided dependencies
200
212
  this.languageProvider = options.language;
201
213
  this.parser = options.parser;
202
- this.textService = new prose_pipeline_1.ProsePipeline(this.languageProvider, this.world);
214
+ this.textService = new index_js_1.ProsePipeline(this.languageProvider, this.world);
203
215
  // Update action registry with language provider
204
216
  this.actionRegistry.setLanguageProvider(this.languageProvider);
205
217
  // Wire parser with platform events if supported
206
- if (this.parser && (0, parser_interface_1.hasPlatformEventEmitter)(this.parser)) {
218
+ if (this.parser && (0, parser_interface_js_1.hasPlatformEventEmitter)(this.parser)) {
207
219
  this.parser.setPlatformEventEmitter((event) => {
208
220
  this.platformEvents.addEvent(event);
209
221
  });
210
222
  }
211
223
  // Create command executor with dependencies
212
- 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);
225
+ // ADR-224: auto-register the deadly-room death transformer so every story
226
+ // (TS or Chord) gets the deadly-room verb-allowlist / probabilistic hazard for
227
+ // free — no story wiring needed. It early-returns when the player's room has no
228
+ // DeadlyRoomTrait, and uses the engine's seeded RNG for the `chance` variant.
229
+ this.commandExecutor.registerParsedCommandTransformer((0, stdlib_1.createDeadlyRoomTransformer)(this.random));
213
230
  // Query handling is now managed by the platform layer
214
231
  // Platform owns the QueryManager and handles all queries
215
232
  // Note: game.initialized event is emitted in start() to avoid race condition
@@ -224,7 +241,7 @@ class GameEngine {
224
241
  this.emitGameEvent(loadingEvent);
225
242
  this.story = story;
226
243
  // Build narrative settings from story config (ADR-089)
227
- this.narrativeSettings = (0, narrative_1.buildNarrativeSettings)(story.config.narrative);
244
+ this.narrativeSettings = (0, index_js_2.buildNarrativeSettings)(story.config.narrative);
228
245
  // Create player first so initializeWorld() can place them
229
246
  const newPlayer = story.createPlayer(this.world);
230
247
  this.context.player = newPlayer;
@@ -248,7 +265,9 @@ class GameEngine {
248
265
  story.initializeWorld(this.world);
249
266
  // ADR-209 AC-5: fail load synchronously (naming room and marker) if any
250
267
  // snippet-bearing room's description carries an unbound {snippet:name}.
251
- (0, snippet_validation_1.validateRoomSnippets)(this.world);
268
+ (0, snippet_validation_js_1.validateRoomSnippets)(this.world);
269
+ // ADR-226 AC-7: every combatant must carry the HealthTrait combat operates on.
270
+ (0, combatant_health_validation_js_1.validateCombatantHealth)(this.world);
252
271
  // Configure language provider with narrative settings (ADR-089)
253
272
  this.configureLanguageProviderNarrative(newPlayer);
254
273
  // Update metadata
@@ -628,6 +647,30 @@ class GameEngine {
628
647
  });
629
648
  this.emit('channel:packet', packet, turn);
630
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
+ }
631
674
  /**
632
675
  * Stop the game engine
633
676
  */
@@ -674,41 +717,22 @@ class GameEngine {
674
717
  this.emit('game:over', this.context);
675
718
  }
676
719
  /**
677
- * Restart the game from scratch.
720
+ * Build the restart acknowledgment event (ADR-248).
678
721
  *
679
- * Clears the world, resets engine state, and re-initializes the story.
680
- * 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.
681
727
  */
682
- async restartGame() {
683
- if (!this.story)
684
- return;
685
- // Stop engine if running
686
- if (this.running) {
687
- this.stop();
688
- }
689
- // Reset pronoun context
690
- if (this.parser && (0, parser_interface_1.hasPronounContext)(this.parser)) {
691
- this.parser.resetPronounContext();
692
- }
693
- // Clear world state (entities, spatial index, relationships, etc.)
694
- this.world.clear();
695
- // Reset engine context
696
- this.context.currentTurn = 1;
697
- this.context.history = [];
698
- this.context.metadata.started = new Date();
699
- this.context.metadata.lastPlayed = new Date();
700
- // Clear engine bookkeeping
701
- this.turnEvents.clear();
702
- this.pendingPlatformOps = [];
703
- this.soundBuffer.length = 0;
704
- this.saveRestoreService.clearUndoSnapshots();
705
- this.hasEmittedInitialized = false;
706
- // Clear plugin registry so story can re-register plugins
707
- this.pluginRegistry.clear();
708
- // Re-initialize the story (creates entities, player, custom actions, etc.)
709
- this.setStory(this.story);
710
- // Start the engine (emits game.initialized + game.started)
711
- 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
+ };
712
736
  }
713
737
  /**
714
738
  * Execute a turn
@@ -726,7 +750,7 @@ class GameEngine {
726
750
  // the line, matching player expectations from Inform-style interpreters.
727
751
  // Skipped while an alternate input mode is active (ADR-137) — mode
728
752
  // handlers own the raw line, punctuation and all.
729
- 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)) {
730
754
  const statements = splitChainedInput(input);
731
755
  if (statements.length > 1) {
732
756
  let result;
@@ -785,7 +809,7 @@ class GameEngine {
785
809
  }
786
810
  this.emit('turn:start', turn, input);
787
811
  // Check for alternate input mode (ADR-137)
788
- const activeModeId = this.world.getStateValue(types_1.INPUT_MODE_STATE_KEY);
812
+ const activeModeId = this.world.getStateValue(types_js_1.INPUT_MODE_STATE_KEY);
789
813
  if (activeModeId) {
790
814
  const handler = this.inputModeHandlers.get(activeModeId);
791
815
  if (handler) {
@@ -800,7 +824,7 @@ class GameEngine {
800
824
  if (this.parser) {
801
825
  // Set world context for parser
802
826
  const player = this.world.getPlayer();
803
- if (player && (0, parser_interface_1.hasWorldContext)(this.parser)) {
827
+ if (player && (0, parser_interface_js_1.hasWorldContext)(this.parser)) {
804
828
  const playerLocation = this.world.getLocation(player.id) || '';
805
829
  this.parser.setWorldContext(this.world, player.id, playerLocation);
806
830
  }
@@ -843,7 +867,7 @@ class GameEngine {
843
867
  locationId: playerLocation
844
868
  };
845
869
  // Store events for this turn (process through enrichment pipeline)
846
- 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));
847
871
  // Apply perception filtering if service is configured
848
872
  // This transforms events based on what the player can perceive
849
873
  if (this.perceptionService) {
@@ -865,7 +889,7 @@ class GameEngine {
865
889
  if (result.success) {
866
890
  this.updateCommandHistory(result, input, turn);
867
891
  // Update pronoun context for "it"/"them"/"him"/"her" resolution (ADR-089)
868
- 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) {
869
893
  this.parser.updatePronounContext(result.validatedCommand, turn);
870
894
  }
871
895
  }
@@ -900,6 +924,16 @@ class GameEngine {
900
924
  const playerLocation = this.world.getLocation(this.context.player.id);
901
925
  // Plugin tick loop (ADR-120)
902
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
+ });
903
937
  const pluginContext = {
904
938
  world: this.world,
905
939
  turn,
@@ -908,7 +942,7 @@ class GameEngine {
908
942
  random: this.random,
909
943
  actionResult: {
910
944
  actionId: result.actionId || '',
911
- success: result.success,
945
+ success: result.success && !actionRefused,
912
946
  targetId: result.validatedCommand?.directObject?.entity?.id,
913
947
  },
914
948
  actionEvents: semanticEvents,
@@ -979,6 +1013,13 @@ class GameEngine {
979
1013
  // channels still re-emit.
980
1014
  this.emitChannelPacket(turnEvents, blocks, turn);
981
1015
  }
1016
+ // Detect a canonical player-death event (ADR-224) *before* the turn's
1017
+ // events are cleared below. The death may have been emitted this turn by
1018
+ // the action, an interceptor, or a scheduler daemon — all have landed in
1019
+ // this turn's event stream by now, and story policy (event handlers in the
1020
+ // executor + state-machine plugins in the tick loop above) has already had
1021
+ // its "first crack" to veto by resetting the player's HealthTrait.
1022
+ const deathCause = this.playerDeathCauseThisTurn(turn);
982
1023
  // Clear turn events after processing to prevent accumulation on same turn (meta commands)
983
1024
  this.turnEvents.set(turn, []);
984
1025
  // Emit completion
@@ -988,6 +1029,14 @@ class GameEngine {
988
1029
  this.stop('victory', victoryDetails);
989
1030
  return result;
990
1031
  }
1032
+ // Route a still-dead player to game.lost. Only if the player's *derived*
1033
+ // life-state is still dead do we end the game — the re-check of live state,
1034
+ // not the event's `terminal` flag, is the engine's final word (ADR-224
1035
+ // Q-2, AC-3). A story reincarnation policy that cleared `dead` above wins.
1036
+ if (deathCause !== undefined && this.isPlayerDead()) {
1037
+ this.stop('defeat', { reason: 'You have died.', cause: deathCause });
1038
+ return result;
1039
+ }
991
1040
  // Check for game over via story.isComplete()
992
1041
  if (this.isGameOver()) {
993
1042
  // Check if it's a victory (story completed successfully)
@@ -1078,7 +1127,7 @@ class GameEngine {
1078
1127
  }
1079
1128
  // Create action context for meta-command execution
1080
1129
  const scopeResolver = (0, stdlib_1.createScopeResolver)(this.world);
1081
- 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);
1082
1131
  // Run action's four-phase pattern
1083
1132
  const actionValidation = action.validate(actionContext);
1084
1133
  let actionEvents;
@@ -1244,20 +1293,19 @@ class GameEngine {
1244
1293
  }
1245
1294
  case core_1.PlatformEventType.RESTART_REQUESTED: {
1246
1295
  const context = platformOp.payload.context;
1247
- if (this.saveRestoreHooks?.onRestartRequested) {
1248
- const shouldRestart = await this.saveRestoreHooks.onRestartRequested(context);
1249
- if (shouldRestart) {
1250
- await this.restartGame();
1251
- completionEvents.push((0, core_1.createRestartCompletedEvent)(true));
1252
- }
1253
- else {
1254
- completionEvents.push((0, core_1.createRestartCompletedEvent)(false));
1255
- }
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');
1256
1306
  }
1257
1307
  else {
1258
- // No restart hook — auto-confirm
1259
- await this.restartGame();
1260
- completionEvents.push((0, core_1.createRestartCompletedEvent)(true));
1308
+ completionEvents.push((0, core_1.createRestartCompletedEvent)(false));
1261
1309
  }
1262
1310
  break;
1263
1311
  }
@@ -1383,7 +1431,7 @@ class GameEngine {
1383
1431
  */
1384
1432
  configureLanguageProviderNarrative(player) {
1385
1433
  // Check if language provider supports narrative settings
1386
- if (!this.languageProvider || !(0, language_provider_interface_1.hasNarrativeSettings)(this.languageProvider)) {
1434
+ if (!this.languageProvider || !(0, language_provider_interface_js_1.hasNarrativeSettings)(this.languageProvider)) {
1387
1435
  return;
1388
1436
  }
1389
1437
  // Build narrative context for language provider
@@ -1424,11 +1472,11 @@ class GameEngine {
1424
1472
  throw new Error(`Cannot sync player state: entity '${newPlayerId}' not found`);
1425
1473
  }
1426
1474
  this.context.player = newPlayer;
1427
- if (this.parser && (0, parser_interface_1.hasWorldContext)(this.parser)) {
1475
+ if (this.parser && (0, parser_interface_js_1.hasWorldContext)(this.parser)) {
1428
1476
  const playerLocation = this.world.getLocation(newPlayerId) || '';
1429
1477
  this.parser.setWorldContext(this.world, newPlayerId, playerLocation);
1430
1478
  }
1431
- if (this.parser && (0, parser_interface_1.hasPronounContext)(this.parser)) {
1479
+ if (this.parser && (0, parser_interface_js_1.hasPronounContext)(this.parser)) {
1432
1480
  this.parser.resetPronounContext();
1433
1481
  }
1434
1482
  this.updateScopeVocabulary();
@@ -1440,6 +1488,23 @@ class GameEngine {
1440
1488
  getPluginRegistry() {
1441
1489
  return this.pluginRegistry;
1442
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
+ }
1443
1508
  /**
1444
1509
  * Get event processor for handler registration (ADR-075)
1445
1510
  */
@@ -1675,7 +1740,7 @@ class GameEngine {
1675
1740
  playerId: this.context.player.id,
1676
1741
  locationId: playerLocation ?? undefined
1677
1742
  };
1678
- 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));
1679
1744
  if (this.perceptionService) {
1680
1745
  processed = this.perceptionService.filterEvents(processed, this.context.player, this.world);
1681
1746
  }
@@ -1986,10 +2051,14 @@ class GameEngine {
1986
2051
  shouldRestart = await this.saveRestoreHooks.onRestartRequested(context);
1987
2052
  }
1988
2053
  if (shouldRestart) {
1989
- const completionEvent = (0, core_1.createRestartCompletedEvent)(true);
1990
- this.eventSource.emit(completionEvent);
1991
- this.emit('event', completionEvent);
1992
- 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');
1993
2062
  }
1994
2063
  else {
1995
2064
  const cancelEvent = (0, core_1.createRestartCompletedEvent)(false);
@@ -2118,6 +2187,37 @@ class GameEngine {
2118
2187
  // Default: game never ends
2119
2188
  return false;
2120
2189
  }
2190
+ /**
2191
+ * The `cause` of a canonical player-death event (ADR-224) emitted during the
2192
+ * given turn, or `undefined` if the player did not die this turn. Scans the
2193
+ * turn's accumulated events, so it sees deaths from the action, interceptors,
2194
+ * and scheduler daemons alike. When several fire in one turn (rare), the first
2195
+ * is authoritative — `killPlayer` is idempotent, so later calls emit nothing.
2196
+ * @param turn the turn number whose events to scan
2197
+ */
2198
+ playerDeathCauseThisTurn(turn) {
2199
+ const events = this.turnEvents.get(turn) || [];
2200
+ for (const event of events) {
2201
+ if (event.type === stdlib_1.PLAYER_DIED_EVENT) {
2202
+ const cause = event.data?.cause;
2203
+ return typeof cause === 'string' ? cause : 'unknown';
2204
+ }
2205
+ }
2206
+ return undefined;
2207
+ }
2208
+ /**
2209
+ * Whether the player is currently dead by their derived `HealthTrait` state
2210
+ * (ADR-226/ADR-224). A player with no `HealthTrait` is alive by default (the
2211
+ * opt-in rule) — `killPlayer` lazily attaches one, so a real death always has a
2212
+ * trait to read. This is the engine's "final word" after story policy has run.
2213
+ */
2214
+ isPlayerDead() {
2215
+ const player = this.context.player;
2216
+ if (!player)
2217
+ return false;
2218
+ const health = player.get(world_model_1.TraitType.HEALTH);
2219
+ return health ? !world_model_1.HealthBehavior.isAlive(health) : false;
2220
+ }
2121
2221
  /**
2122
2222
  * Add event listener
2123
2223
  */