Skip to main content

dotzuki_engine/battle/
driver.rs

1//! Generic, game-agnostic battle **turn-execution driver** (P0b).
2//!
3//! [`BattleDriver::execute_turn`] sits above the existing
4//! [`BattleProvider`]/[`BattlerState`](super::BattlerState)/[`BattleState`]
5//! abstraction and owns the *control flow* of a single battle turn:
6//!
7//! 1. Ask the provider for each actor's [`OrderKey`] and **stable-sort** them
8//!    ascending (turn order — C4).
9//! 2. For each actor in order: run the [`before_move`](BattleProvider::before_move)
10//!    pre-move status gate, then execute its action — for `Fight`, an
11//!    [`accuracy_check`](BattleProvider::accuracy_check) followed by
12//!    [`calculate_damage`](BattleProvider::calculate_damage); for `Switch`,
13//!    [`apply_switch`](BattleDriver::apply_switch). Faints are detected after
14//!    each hit.
15//! 3. Run the [`end_of_turn`](BattleProvider::end_of_turn) residual hook.
16//! 4. Fold everything into a [`TurnOutcome`] and detect a [`BattleEnd`].
17//!
18//! Every *number* and *rule decision* (the damage formula, accuracy/crit math,
19//! status semantics, residual ordering, turn-order tie-breaks) lives in the
20//! provider; the driver only sequences. All randomness is injected through
21//! [`BattleRng`] so the engine never links `rand` and the game controls the
22//! exact draw sequence (C2).
23//!
24//! The driver is **event-sourced**: it returns a `Vec<`[`TurnEvent`]`>` that the
25//! game maps onto its own UI/animation state machine, keeping rendering out of
26//! the engine and making the driver unit-testable without a screen.
27
28use super::{
29    BattleAction, BattleProvider, BattleRng, BattleState, BattlerRef, BattlerState as Battler,
30    EffectResult, MoveGate, OrderKey,
31};
32use std::fmt;
33
34/// A single observable event produced during turn execution.
35///
36/// Generic over the provider so move identifiers stay game-defined. The game
37/// translates these into its own UI / animation steps.
38pub enum TurnEvent<P: BattleProvider + ?Sized> {
39    /// A battler began its action with the given chosen move.
40    MoveUsed {
41        /// The acting battler.
42        who: BattlerRef,
43        /// The move used.
44        move_: P::Move,
45    },
46    /// A battler's action was prevented by the pre-move gate (sleep, freeze,
47    /// flinch, full paralysis, recharge, …). Carries the gate's reason.
48    ActionPrevented {
49        /// The prevented battler.
50        who: BattlerRef,
51        /// Why it could not act.
52        reason: EffectResult,
53    },
54    /// A move missed (failed [`accuracy_check`](BattleProvider::accuracy_check),
55    /// including the Gen-1 1/256 miss).
56    Missed {
57        /// The attacker.
58        who: BattlerRef,
59        /// The intended target.
60        target: BattlerRef,
61    },
62    /// Damage was dealt to `target`.
63    Damage {
64        /// The attacker.
65        who: BattlerRef,
66        /// The battler that took the damage.
67        target: BattlerRef,
68        /// Amount of HP lost.
69        amount: u16,
70        /// Whether this was a critical hit.
71        critical: bool,
72        /// Type-effectiveness multiplier from the damage calculation.
73        effectiveness: f32,
74    },
75    /// A battler fainted (HP reached 0).
76    Faint {
77        /// The fainted battler.
78        who: BattlerRef,
79    },
80    /// A battler switched to another party slot.
81    Switched {
82        /// The acting side.
83        side: u8,
84        /// Destination slot.
85        to_slot: usize,
86    },
87    /// An end-of-turn residual effect resolved (poison/burn tick, leech, …).
88    Residual {
89        /// The residual result.
90        result: EffectResult,
91    },
92    /// A generic effect result the game wishes to surface (catch/run handling,
93    /// item use, forced actions, …).
94    Effect {
95        /// The effect result.
96        result: EffectResult,
97    },
98}
99
100impl<P: BattleProvider + ?Sized> fmt::Debug for TurnEvent<P> {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        match self {
103            TurnEvent::MoveUsed { who, move_ } => f
104                .debug_struct("MoveUsed")
105                .field("who", who)
106                .field("move_", move_)
107                .finish(),
108            TurnEvent::ActionPrevented { who, reason } => f
109                .debug_struct("ActionPrevented")
110                .field("who", who)
111                .field("reason", reason)
112                .finish(),
113            TurnEvent::Missed { who, target } => f
114                .debug_struct("Missed")
115                .field("who", who)
116                .field("target", target)
117                .finish(),
118            TurnEvent::Damage {
119                who,
120                target,
121                amount,
122                critical,
123                effectiveness,
124            } => f
125                .debug_struct("Damage")
126                .field("who", who)
127                .field("target", target)
128                .field("amount", amount)
129                .field("critical", critical)
130                .field("effectiveness", effectiveness)
131                .finish(),
132            TurnEvent::Faint { who } => f.debug_struct("Faint").field("who", who).finish(),
133            TurnEvent::Switched { side, to_slot } => f
134                .debug_struct("Switched")
135                .field("side", side)
136                .field("to_slot", to_slot)
137                .finish(),
138            TurnEvent::Residual { result } => {
139                f.debug_struct("Residual").field("result", result).finish()
140            }
141            TurnEvent::Effect { result } => {
142                f.debug_struct("Effect").field("result", result).finish()
143            }
144        }
145    }
146}
147
148impl<P: BattleProvider + ?Sized> PartialEq for TurnEvent<P>
149where
150    P::Move: PartialEq,
151{
152    fn eq(&self, other: &Self) -> bool {
153        use TurnEvent::*;
154        match (self, other) {
155            (MoveUsed { who: a, move_: ma }, MoveUsed { who: b, move_: mb }) => a == b && ma == mb,
156            (ActionPrevented { who: a, reason: ra }, ActionPrevented { who: b, reason: rb }) => {
157                a == b && ra == rb
158            }
159            (Missed { who: a, target: ta }, Missed { who: b, target: tb }) => a == b && ta == tb,
160            (
161                Damage {
162                    who: a,
163                    target: ta,
164                    amount: am,
165                    critical: ca,
166                    effectiveness: ea,
167                },
168                Damage {
169                    who: b,
170                    target: tb,
171                    amount: bm,
172                    critical: cb,
173                    effectiveness: eb,
174                },
175            ) => a == b && ta == tb && am == bm && ca == cb && ea == eb,
176            (Faint { who: a }, Faint { who: b }) => a == b,
177            (
178                Switched {
179                    side: a,
180                    to_slot: ta,
181                },
182                Switched {
183                    side: b,
184                    to_slot: tb,
185                },
186            ) => a == b && ta == tb,
187            (Residual { result: a }, Residual { result: b }) => a == b,
188            (Effect { result: a }, Effect { result: b }) => a == b,
189            _ => false,
190        }
191    }
192}
193
194/// How a battle ended, if it ended this turn.
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub enum BattleEnd {
197    /// All opponent battlers fainted.
198    PlayerWin,
199    /// All player battlers fainted.
200    PlayerLoss,
201    /// The player (or opponent) fled.
202    Fled,
203    /// A wild monster was caught.
204    Caught,
205}
206
207/// The result of executing one turn.
208pub struct TurnOutcome<P: BattleProvider + ?Sized> {
209    /// Ordered events produced this turn, ready to drive the game's UI.
210    pub events: Vec<TurnEvent<P>>,
211    /// `Some` if the battle ended this turn.
212    pub battle_over: Option<BattleEnd>,
213}
214
215impl<P: BattleProvider + ?Sized> fmt::Debug for TurnOutcome<P> {
216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217        f.debug_struct("TurnOutcome")
218            .field("events", &self.events)
219            .field("battle_over", &self.battle_over)
220            .finish()
221    }
222}
223
224/// The generic turn-execution driver. Stateless: state goes in, events come out.
225pub struct BattleDriver;
226
227impl BattleDriver {
228    /// Execute exactly one full battle turn given each side's chosen action
229    /// (`actions[0]` = player, `actions[1]` = opponent).
230    ///
231    /// See the [module docs](self) for the sequencing. The provider supplies all
232    /// numbers/rules via its hooks; `rng` supplies all randomness.
233    pub fn execute_turn<P: BattleProvider>(
234        provider: &P,
235        state: &mut BattleState<P>,
236        actions: [BattleAction<P>; 2],
237        rng: &mut dyn BattleRng,
238    ) -> TurnOutcome<P> {
239        let mut events: Vec<TurnEvent<P>> = Vec::new();
240
241        // Each actor: (BattlerRef, action). Player first, then opponent — this
242        // is also the stable fallback order on a tie.
243        let [player_action, opponent_action] = actions;
244        let actors = [
245            (BattlerRef::PLAYER, player_action),
246            (BattlerRef::OPPONENT, opponent_action),
247        ];
248
249        // ── 1. Turn order: provider key → engine stable-sort (C4). ──
250        // RNG is drawn here (e.g. speed-tie flip) in submission order so the
251        // draw sequence is deterministic and game-controlled.
252        let mut keyed: Vec<(OrderKey, usize)> = actors
253            .iter()
254            .enumerate()
255            .map(|(idx, (who, action))| (provider.turn_order_key(state, *who, action, rng), idx))
256            .collect();
257        // Stable sort: equal keys keep submission (player-before-opponent) order.
258        keyed.sort_by(|a, b| a.0.cmp(&b.0));
259
260        // ── 2. Resolve each actor in order. ──
261        for (_key, idx) in keyed {
262            let (who, action) = &actors[idx];
263            // Skip actors whose side is already wiped (lead fainted earlier
264            // this turn) — they cannot act.
265            if Self::side_all_fainted(state, who.side) {
266                continue;
267            }
268            Self::resolve_action(provider, state, *who, action, rng, &mut events);
269
270            // Short-circuit if the battle is already decided mid-turn so we
271            // don't run residuals on a finished battle.
272            if let Some(battle_over) = Self::detect_end(state) {
273                return TurnOutcome {
274                    events,
275                    battle_over: Some(battle_over),
276                };
277            }
278        }
279
280        // ── 3. End-of-turn residuals (poison/burn/leech/wrap/weather). ──
281        for result in provider.end_of_turn(state, rng) {
282            events.push(TurnEvent::Residual { result });
283        }
284        // Surface faints triggered by residual damage.
285        Self::push_faints(state, &mut events);
286
287        // ── 4. Battle-end detection. ──
288        state.turn_count = state.turn_count.saturating_add(1);
289        let battle_over = Self::detect_end(state);
290
291        TurnOutcome {
292            events,
293            battle_over,
294        }
295    }
296
297    /// Resolve a single actor's action, appending events.
298    fn resolve_action<P: BattleProvider>(
299        provider: &P,
300        state: &mut BattleState<P>,
301        who: BattlerRef,
302        action: &BattleAction<P>,
303        rng: &mut dyn BattleRng,
304        events: &mut Vec<TurnEvent<P>>,
305    ) {
306        // Pre-move status gate (sleep/freeze/flinch/para/recharge/confusion…).
307        let gate = provider.before_move(state, who, action, rng);
308        let effective_action: BattleAction<P> = match gate {
309            MoveGate::Acts => action.clone(),
310            MoveGate::Prevented(reason) => {
311                events.push(TurnEvent::ActionPrevented { who, reason });
312                return;
313            }
314            MoveGate::ForcedAction(forced) => forced,
315        };
316
317        match effective_action {
318            BattleAction::Fight { move_ } => {
319                Self::resolve_fight(provider, state, who, move_, rng, events);
320            }
321            BattleAction::Switch { to_slot } => {
322                if Self::apply_switch(state, who.side, to_slot) {
323                    events.push(TurnEvent::Switched {
324                        side: who.side,
325                        to_slot,
326                    });
327                }
328            }
329            // Item/Run/Catch resolution is game-specific: the game models it via
330            // `before_move`/`end_of_turn` or surfaces the result through the
331            // action; the engine has nothing generic to do beyond noting it.
332            BattleAction::UseItem { .. } | BattleAction::Run | BattleAction::Nothing => {}
333        }
334    }
335
336    /// Resolve a `Fight` action: accuracy → damage → faint.
337    fn resolve_fight<P: BattleProvider>(
338        provider: &P,
339        state: &mut BattleState<P>,
340        who: BattlerRef,
341        move_: P::Move,
342        rng: &mut dyn BattleRng,
343        events: &mut Vec<TurnEvent<P>>,
344    ) {
345        let target = Self::opposing(who);
346
347        events.push(TurnEvent::MoveUsed {
348            who,
349            move_: move_.clone(),
350        });
351
352        // Accuracy (incl. the 1/256 miss) — game-owned.
353        if !provider.accuracy_check(state, who, target, &move_, rng) {
354            events.push(TurnEvent::Missed { who, target });
355            return;
356        }
357
358        // Critical-hit roll — game-owned (crit rate, Focus Energy, high-crit
359        // moves, every Gen-1 quirk). Rolled *before* damage so the result both
360        // feeds the formula and surfaces into the `Damage` event below. The
361        // default `roll_critical` never crits and draws no rng, so providers
362        // that don't override it keep their exact draw sequence.
363        let critical = provider.roll_critical(state, who, target, &move_, rng);
364
365        // Damage — game-owned formula (C3). The driver supplies a `random` byte
366        // from the injected rng and consumes the returned result; STAB/type and
367        // every quirk live inside the provider's `calculate_damage`, which is
368        // told whether this is a critical hit.
369        let random = rng.next_u8();
370        let (Some(attacker), Some(defender)) = (
371            Self::battler(state, who).cloned(),
372            Self::battler(state, target).cloned(),
373        ) else {
374            return;
375        };
376        let dmg = provider.calculate_damage(&move_, &attacker, &defender, random, critical);
377
378        if dmg.is_miss {
379            events.push(TurnEvent::Missed { who, target });
380            return;
381        }
382
383        if let Some(def_mut) = Self::battler_mut(state, target) {
384            def_mut.take_damage(dmg.damage);
385        }
386        events.push(TurnEvent::Damage {
387            who,
388            target,
389            amount: dmg.damage,
390            critical,
391            effectiveness: dmg.effectiveness,
392        });
393
394        if let Some(def) = Self::battler(state, target) {
395            if def.hp == 0 {
396                events.push(TurnEvent::Faint { who: target });
397            }
398        }
399    }
400
401    /// Switch the active battler on `side` to `to_slot`. Returns `true` on a
402    /// successful, in-bounds switch to a non-fainted member.
403    pub fn apply_switch<P: BattleProvider>(
404        state: &mut BattleState<P>,
405        side: u8,
406        to_slot: usize,
407    ) -> bool {
408        let party = match side {
409            0 => &mut state.player_battlers,
410            _ => &mut state.opponent_battlers,
411        };
412        if to_slot == 0 || to_slot >= party.len() || party[to_slot].hp == 0 {
413            return false;
414        }
415        party.swap(0, to_slot);
416        true
417    }
418
419    // ── helpers ──────────────────────────────────────────────────────
420
421    fn opposing(who: BattlerRef) -> BattlerRef {
422        BattlerRef::new(if who.side == 0 { 1 } else { 0 }, who.slot)
423    }
424
425    fn battler<P: BattleProvider>(state: &BattleState<P>, who: BattlerRef) -> Option<&Battler<P>> {
426        let party = if who.side == 0 {
427            &state.player_battlers
428        } else {
429            &state.opponent_battlers
430        };
431        party.get(who.slot as usize)
432    }
433
434    fn battler_mut<P: BattleProvider>(
435        state: &mut BattleState<P>,
436        who: BattlerRef,
437    ) -> Option<&mut Battler<P>> {
438        let party = if who.side == 0 {
439            &mut state.player_battlers
440        } else {
441            &mut state.opponent_battlers
442        };
443        party.get_mut(who.slot as usize)
444    }
445
446    /// Is every battler of `side` fainted (or the side empty)?
447    fn side_all_fainted<P: BattleProvider>(state: &BattleState<P>, side: u8) -> bool {
448        let party = if side == 0 {
449            &state.player_battlers
450        } else {
451            &state.opponent_battlers
452        };
453        party.is_empty() || party.iter().all(|b| b.hp == 0)
454    }
455
456    /// Emit `Faint` events for any lead battler at 0 HP not yet reported.
457    fn push_faints<P: BattleProvider>(state: &BattleState<P>, events: &mut Vec<TurnEvent<P>>) {
458        for who in [BattlerRef::PLAYER, BattlerRef::OPPONENT] {
459            if let Some(b) = Self::battler(state, who) {
460                if b.hp == 0 {
461                    let already = events
462                        .iter()
463                        .any(|e| matches!(e, TurnEvent::Faint { who: w } if *w == who));
464                    if !already {
465                        events.push(TurnEvent::Faint { who });
466                    }
467                }
468            }
469        }
470    }
471
472    /// Detect battle end from current HP totals.
473    fn detect_end<P: BattleProvider>(state: &BattleState<P>) -> Option<BattleEnd> {
474        let player_wiped =
475            !state.player_battlers.is_empty() && state.player_battlers.iter().all(|b| b.hp == 0);
476        let opponent_wiped = !state.opponent_battlers.is_empty()
477            && state.opponent_battlers.iter().all(|b| b.hp == 0);
478        match (player_wiped, opponent_wiped) {
479            (_, true) => Some(BattleEnd::PlayerWin),
480            (true, false) => Some(BattleEnd::PlayerLoss),
481            (false, false) => None,
482        }
483    }
484}