Advertisement
Guest User

Untitled

a guest
May 2nd, 2025
9
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 3.91 KB | None | 0 0
  1. ---
  2.  
  3. **Title:**
  4. How to handle intermediate state transitions in command handlers in an event-driven architecture?
  5.  
  6. ---
  7.  
  8. **Body:**
  9.  
  10. I'm building a game-style application using an event-sourced-ish architecture. Here's the basic structure:
  11.  
  12. - A **dispatcher** on the game engine receives commands and routes them to command handlers.
  13. - Each handler returns a list of **events** based on current state and command input. **Handlers never update state directly** — they return events only.
  14. - The dispatcher passes all events to a pure reducer which mutates the state.
  15. - The dispatcher emits the event.
  16. - Client uses the same **reducer** to apply events to state.
  17.  
  18. Here's what the core dispatcher looks like:
  19.  
  20. ```ts
  21. public executeCommand(command: Command) {
  22. try {
  23. const events = this.handleCommand(command);
  24. events.forEach((event) => {
  25. this.state = applyEvent(this.state, event);
  26. this.emitEvent(event);
  27. });
  28. } catch (error) {
  29. this.emitError(error);
  30. }
  31. }
  32.  
  33. private handleCommand(command: Command): GameEvent[] {
  34. const handler = this.commandHandlers[command.type];
  35. if (!handler) {
  36. throw new Error(`Unknown command: ${command.type}`);
  37. }
  38.  
  39. const ctx = new GameContext(this.state);
  40.  
  41. return handler(ctx, command as any);
  42. }
  43. ```
  44.  
  45. This setup has been nice so far. Until...
  46.  
  47. ---
  48.  
  49. ### 🧩 The Problem: Logic that depends on intermediate state
  50.  
  51. Here's where the design gets tricky. Some commands involve logic that depends on how **earlier events** from the same command.
  52.  
  53. #### Example: Chain Shot
  54.  
  55. ```txt
  56. Command: Player uses "Chain Shot" on Enemy A
  57. → Emit DamageDealt to Enemy A
  58. → If A dies, retarget to a random living enemy
  59. → Emit DamageDealt to Enemy B
  60. → If B dies, repeat again (up to 3 times)
  61. ```
  62.  
  63. The problem:
  64. Whether to retarget depends on which enemies are still alive — which depends on **previous events in the same handler**. But the reducer hasn’t been called yet, so the handler can’t observe that state directly.
  65.  
  66. ---
  67.  
  68. ### 🤔 Architectural Tension
  69.  
  70. To make this work, I’m weighing two approaches:
  71.  
  72. ---
  73.  
  74. #### ✅ Option 1: Apply events to a draft state inside the handler
  75.  
  76. The handler simulates the reducer locally to compute intermediate results.
  77.  
  78. ```ts
  79. const draft = clone(state);
  80. const events: Event[] = [];
  81.  
  82. const e1 = DamageDealt(enemyA.id, 30);
  83. applyEvent(draft, e1);
  84. events.push(e1);
  85.  
  86. if (draft.enemies[enemyA.id].hp <= 0) {
  87. const target = chooseRandomAliveEnemy(draft);
  88. const e2 = DamageDealt(target.id, 30);
  89. applyEvent(draft, e2);
  90. events.push(e2);
  91. }
  92. ```
  93.  
  94. 🟢 Pros:
  95.  
  96. - Leverages reducer logic, so behavior is consistent
  97. - Keeps events granular and observable
  98.  
  99. 🔴 Cons:
  100.  
  101. - Feels like im kind of undermining the point of my architecture?
  102.  
  103. ---
  104.  
  105. #### ✅ Option 2: Predict outcome manually in handler
  106.  
  107. Instead of simulating state changes, the handler infers state by redoing the logic manually.
  108.  
  109. ```ts
  110. const events: Event[] = [];
  111.  
  112. const dmg = 30;
  113. const remainingHp = enemyA.hp - dmg;
  114. events.push(DamageDealt(enemyA.id, dmg));
  115.  
  116. if (remainingHp <= 0) {
  117. const living = enemies.filter((e) => e.id !== enemyA.id && e.hp > 0);
  118. const target = chooseRandom(living);
  119. events.push(DamageDealt(target.id, dmg));
  120. }
  121. ```
  122.  
  123. 🟢 Pros:
  124.  
  125. - Keeps handler and reducer fully decoupled
  126. - No internal state simulation
  127.  
  128. 🔴 Cons:
  129.  
  130. - Duplicates reducer logic
  131. - Risk of logic drift or inconsistency.
  132.  
  133. ---
  134.  
  135. ### ❓What I'm asking
  136.  
  137. - Is using the reducer on a draft state in the handler an acceptable and clean approach?
  138. - Is this a red flag for my architecture? Is there a better pattern I’m missing?
  139. - Have others run into this when designing game systems or simulations with event-driven patterns?
  140. - Any feedback on the design or long-term risks I should be aware of?
  141.  
  142. I’m trying to balance clean separation of concerns with practical game logic. Would love to hear how others have tackled this.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement