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            (Switched { side: a, to_slot: ta }, Switched { side: b, to_slot: tb }) => {
178                a == b && ta == tb
179            }
180            (Residual { result: a }, Residual { result: b }) => a == b,
181            (Effect { result: a }, Effect { result: b }) => a == b,
182            _ => false,
183        }
184    }
185}
186
187/// How a battle ended, if it ended this turn.
188#[derive(Clone, Copy, Debug, PartialEq, Eq)]
189pub enum BattleEnd {
190    /// All opponent battlers fainted.
191    PlayerWin,
192    /// All player battlers fainted.
193    PlayerLoss,
194    /// The player (or opponent) fled.
195    Fled,
196    /// A wild monster was caught.
197    Caught,
198}
199
200/// The result of executing one turn.
201pub struct TurnOutcome<P: BattleProvider + ?Sized> {
202    /// Ordered events produced this turn, ready to drive the game's UI.
203    pub events: Vec<TurnEvent<P>>,
204    /// `Some` if the battle ended this turn.
205    pub battle_over: Option<BattleEnd>,
206}
207
208impl<P: BattleProvider + ?Sized> fmt::Debug for TurnOutcome<P> {
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210        f.debug_struct("TurnOutcome")
211            .field("events", &self.events)
212            .field("battle_over", &self.battle_over)
213            .finish()
214    }
215}
216
217/// The generic turn-execution driver. Stateless: state goes in, events come out.
218pub struct BattleDriver;
219
220impl BattleDriver {
221    /// Execute exactly one full battle turn given each side's chosen action
222    /// (`actions[0]` = player, `actions[1]` = opponent).
223    ///
224    /// See the [module docs](self) for the sequencing. The provider supplies all
225    /// numbers/rules via its hooks; `rng` supplies all randomness.
226    pub fn execute_turn<P: BattleProvider>(
227        provider: &P,
228        state: &mut BattleState<P>,
229        actions: [BattleAction<P>; 2],
230        rng: &mut dyn BattleRng,
231    ) -> TurnOutcome<P> {
232        let mut events: Vec<TurnEvent<P>> = Vec::new();
233
234        // Each actor: (BattlerRef, action). Player first, then opponent — this
235        // is also the stable fallback order on a tie.
236        let [player_action, opponent_action] = actions;
237        let actors = [
238            (BattlerRef::PLAYER, player_action),
239            (BattlerRef::OPPONENT, opponent_action),
240        ];
241
242        // ── 1. Turn order: provider key → engine stable-sort (C4). ──
243        // RNG is drawn here (e.g. speed-tie flip) in submission order so the
244        // draw sequence is deterministic and game-controlled.
245        let mut keyed: Vec<(OrderKey, usize)> = actors
246            .iter()
247            .enumerate()
248            .map(|(idx, (who, action))| (provider.turn_order_key(state, *who, action, rng), idx))
249            .collect();
250        // Stable sort: equal keys keep submission (player-before-opponent) order.
251        keyed.sort_by(|a, b| a.0.cmp(&b.0));
252
253        // ── 2. Resolve each actor in order. ──
254        for (_key, idx) in keyed {
255            let (who, action) = &actors[idx];
256            // Skip actors whose side is already wiped (lead fainted earlier
257            // this turn) — they cannot act.
258            if Self::side_all_fainted(state, who.side) {
259                continue;
260            }
261            Self::resolve_action(provider, state, *who, action, rng, &mut events);
262
263            // Short-circuit if the battle is already decided mid-turn so we
264            // don't run residuals on a finished battle.
265            if let Some(battle_over) = Self::detect_end(state) {
266                return TurnOutcome {
267                    events,
268                    battle_over: Some(battle_over),
269                };
270            }
271        }
272
273        // ── 3. End-of-turn residuals (poison/burn/leech/wrap/weather). ──
274        for result in provider.end_of_turn(state, rng) {
275            events.push(TurnEvent::Residual { result });
276        }
277        // Surface faints triggered by residual damage.
278        Self::push_faints(state, &mut events);
279
280        // ── 4. Battle-end detection. ──
281        state.turn_count = state.turn_count.saturating_add(1);
282        let battle_over = Self::detect_end(state);
283
284        TurnOutcome {
285            events,
286            battle_over,
287        }
288    }
289
290    /// Resolve a single actor's action, appending events.
291    fn resolve_action<P: BattleProvider>(
292        provider: &P,
293        state: &mut BattleState<P>,
294        who: BattlerRef,
295        action: &BattleAction<P>,
296        rng: &mut dyn BattleRng,
297        events: &mut Vec<TurnEvent<P>>,
298    ) {
299        // Pre-move status gate (sleep/freeze/flinch/para/recharge/confusion…).
300        let gate = provider.before_move(state, who, action, rng);
301        let effective_action: BattleAction<P> = match gate {
302            MoveGate::Acts => action.clone(),
303            MoveGate::Prevented(reason) => {
304                events.push(TurnEvent::ActionPrevented { who, reason });
305                return;
306            }
307            MoveGate::ForcedAction(forced) => forced,
308        };
309
310        match effective_action {
311            BattleAction::Fight { move_ } => {
312                Self::resolve_fight(provider, state, who, move_, rng, events);
313            }
314            BattleAction::Switch { to_slot } => {
315                if Self::apply_switch(state, who.side, to_slot) {
316                    events.push(TurnEvent::Switched {
317                        side: who.side,
318                        to_slot,
319                    });
320                }
321            }
322            // Item/Run/Catch resolution is game-specific: the game models it via
323            // `before_move`/`end_of_turn` or surfaces the result through the
324            // action; the engine has nothing generic to do beyond noting it.
325            BattleAction::UseItem { .. } | BattleAction::Run | BattleAction::Nothing => {}
326        }
327    }
328
329    /// Resolve a `Fight` action: accuracy → damage → faint.
330    fn resolve_fight<P: BattleProvider>(
331        provider: &P,
332        state: &mut BattleState<P>,
333        who: BattlerRef,
334        move_: P::Move,
335        rng: &mut dyn BattleRng,
336        events: &mut Vec<TurnEvent<P>>,
337    ) {
338        let target = Self::opposing(who);
339
340        events.push(TurnEvent::MoveUsed {
341            who,
342            move_: move_.clone(),
343        });
344
345        // Accuracy (incl. the 1/256 miss) — game-owned.
346        if !provider.accuracy_check(state, who, target, &move_, rng) {
347            events.push(TurnEvent::Missed { who, target });
348            return;
349        }
350
351        // Critical-hit roll — game-owned (crit rate, Focus Energy, high-crit
352        // moves, every Gen-1 quirk). Rolled *before* damage so the result both
353        // feeds the formula and surfaces into the `Damage` event below. The
354        // default `roll_critical` never crits and draws no rng, so providers
355        // that don't override it keep their exact draw sequence.
356        let critical = provider.roll_critical(state, who, target, &move_, rng);
357
358        // Damage — game-owned formula (C3). The driver supplies a `random` byte
359        // from the injected rng and consumes the returned result; STAB/type and
360        // every quirk live inside the provider's `calculate_damage`, which is
361        // told whether this is a critical hit.
362        let random = rng.next_u8();
363        let (Some(attacker), Some(defender)) = (
364            Self::battler(state, who).cloned(),
365            Self::battler(state, target).cloned(),
366        ) else {
367            return;
368        };
369        let dmg = provider.calculate_damage(&move_, &attacker, &defender, random, critical);
370
371        if dmg.is_miss {
372            events.push(TurnEvent::Missed { who, target });
373            return;
374        }
375
376        if let Some(def_mut) = Self::battler_mut(state, target) {
377            def_mut.take_damage(dmg.damage);
378        }
379        events.push(TurnEvent::Damage {
380            who,
381            target,
382            amount: dmg.damage,
383            critical,
384            effectiveness: dmg.effectiveness,
385        });
386
387        if let Some(def) = Self::battler(state, target) {
388            if def.hp == 0 {
389                events.push(TurnEvent::Faint { who: target });
390            }
391        }
392    }
393
394    /// Switch the active battler on `side` to `to_slot`. Returns `true` on a
395    /// successful, in-bounds switch to a non-fainted member.
396    pub fn apply_switch<P: BattleProvider>(
397        state: &mut BattleState<P>,
398        side: u8,
399        to_slot: usize,
400    ) -> bool {
401        let party = match side {
402            0 => &mut state.player_battlers,
403            _ => &mut state.opponent_battlers,
404        };
405        if to_slot == 0 || to_slot >= party.len() || party[to_slot].hp == 0 {
406            return false;
407        }
408        party.swap(0, to_slot);
409        true
410    }
411
412    // ── helpers ──────────────────────────────────────────────────────
413
414    fn opposing(who: BattlerRef) -> BattlerRef {
415        BattlerRef::new(if who.side == 0 { 1 } else { 0 }, who.slot)
416    }
417
418    fn battler<P: BattleProvider>(
419        state: &BattleState<P>,
420        who: BattlerRef,
421    ) -> Option<&Battler<P>> {
422        let party = if who.side == 0 {
423            &state.player_battlers
424        } else {
425            &state.opponent_battlers
426        };
427        party.get(who.slot as usize)
428    }
429
430    fn battler_mut<P: BattleProvider>(
431        state: &mut BattleState<P>,
432        who: BattlerRef,
433    ) -> Option<&mut Battler<P>> {
434        let party = if who.side == 0 {
435            &mut state.player_battlers
436        } else {
437            &mut state.opponent_battlers
438        };
439        party.get_mut(who.slot as usize)
440    }
441
442    /// Is every battler of `side` fainted (or the side empty)?
443    fn side_all_fainted<P: BattleProvider>(state: &BattleState<P>, side: u8) -> bool {
444        let party = if side == 0 {
445            &state.player_battlers
446        } else {
447            &state.opponent_battlers
448        };
449        party.is_empty() || party.iter().all(|b| b.hp == 0)
450    }
451
452    /// Emit `Faint` events for any lead battler at 0 HP not yet reported.
453    fn push_faints<P: BattleProvider>(state: &BattleState<P>, events: &mut Vec<TurnEvent<P>>) {
454        for who in [BattlerRef::PLAYER, BattlerRef::OPPONENT] {
455            if let Some(b) = Self::battler(state, who) {
456                if b.hp == 0 {
457                    let already = events
458                        .iter()
459                        .any(|e| matches!(e, TurnEvent::Faint { who: w } if *w == who));
460                    if !already {
461                        events.push(TurnEvent::Faint { who });
462                    }
463                }
464            }
465        }
466    }
467
468    /// Detect battle end from current HP totals.
469    fn detect_end<P: BattleProvider>(state: &BattleState<P>) -> Option<BattleEnd> {
470        let player_wiped =
471            !state.player_battlers.is_empty() && state.player_battlers.iter().all(|b| b.hp == 0);
472        let opponent_wiped = !state.opponent_battlers.is_empty()
473            && state.opponent_battlers.iter().all(|b| b.hp == 0);
474        match (player_wiped, opponent_wiped) {
475            (_, true) => Some(BattleEnd::PlayerWin),
476            (true, false) => Some(BattleEnd::PlayerLoss),
477            (false, false) => None,
478        }
479    }
480}