Skip to main content

dotzuki_engine/battle/
mod.rs

1//! Battle system trait definitions for a JRPG engine framework.
2//!
3//! This module defines the core abstractions for turn-based battle systems:
4//! battle state, battler state, type charts, AI decision-making, and
5//! move effect handling. All types are generic over a [`BattleProvider`]
6//! implementation — the engine is game-agnostic.
7//!
8//! ## Architecture
9//!
10//! * **`BattleProvider`** — Central trait supplying battle data, damage formula,
11//!   and factory methods. All methods take `&self` (read-only provider).
12//! * **`TypeChart`** — N×N type effectiveness matrix, parameterized by game types.
13//! * **`BattleAI`** — Move selection, switching, and item-use decisions.
14//! * **`EffectHandler`** — Dispatches move effects (damage, healing, status, stat
15//!   changes) on battler state.
16//!
17//! ## Design Principles
18//!
19//! * **Generic over game data** — No concrete game-specific, monster, or move types.
20//!   All identifiers are associated types on `BattleProvider`.
21//! * **Provider pattern** — Battle systems query the provider, which delegates
22//!   to type charts, AI, and effect handlers internally.
23//! * **No I/O, no platform** — Pure data and trait definitions only.
24
25use core::fmt;
26
27pub mod ai;
28pub mod driver;
29pub mod rng;
30pub mod stack;
31
32pub use ai::{BattleAi, BattleAiProvider};
33pub use driver::{BattleDriver, BattleEnd, TurnEvent, TurnOutcome};
34pub use rng::BattleRng;
35
36// ─── EnumMap ───────────────────────────────────────────────────────────
37
38/// A map from enum-like keys to values, backed by a `Vec` of `(K, V)` pairs.
39///
40/// Designed for use with small, game-specific enums (e.g. stat IDs, type IDs).
41/// Lookups are O(n) linear scan — acceptable for N ≤ 15.
42///
43/// # Type Parameters
44///
45/// * `K` — Key type, typically a copyable game-enum variant (`Copy + PartialEq`).
46/// * `V` — Value type.
47pub struct EnumMap<K, V> {
48    entries: Vec<(K, V)>,
49}
50
51impl<K: PartialEq, V> EnumMap<K, V> {
52    /// Create an empty map.
53    pub fn new() -> Self {
54        Self {
55            entries: Vec::new(),
56        }
57    }
58
59    /// Insert or update a key-value pair.
60    pub fn set(&mut self, key: K, value: V) {
61        if let Some(entry) = self.entries.iter_mut().find(|(k, _)| *k == key) {
62            entry.1 = value;
63        } else {
64            self.entries.push((key, value));
65        }
66    }
67
68    /// Look up a value by key. Returns `None` if the key is not present.
69    pub fn get(&self, key: K) -> Option<&V> {
70        self.entries.iter().find(|(k, _)| *k == key).map(|(_, v)| v)
71    }
72
73    /// Look up a value by key. Returns `None` if the key is not present.
74    pub fn get_mut(&mut self, key: K) -> Option<&mut V> {
75        self.entries
76            .iter_mut()
77            .find(|(k, _)| *k == key)
78            .map(|(_, v)| v)
79    }
80
81    /// Number of entries in the map.
82    pub fn len(&self) -> usize {
83        self.entries.len()
84    }
85
86    /// Returns `true` if the map contains no entries.
87    pub fn is_empty(&self) -> bool {
88        self.entries.is_empty()
89    }
90
91    /// Iterate over `(key, value)` pairs in insertion order. Used by the turn-event
92    /// log to diff stat-stage maps before/after an action.
93    pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
94        self.entries.iter().map(|(k, v)| (k, v))
95    }
96}
97
98impl<K: PartialEq, V> Default for EnumMap<K, V> {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104impl<K: Clone + PartialEq, V: Clone> Clone for EnumMap<K, V> {
105    fn clone(&self) -> Self {
106        Self {
107            entries: self.entries.clone(),
108        }
109    }
110}
111
112impl<K: fmt::Debug + PartialEq, V: fmt::Debug> fmt::Debug for EnumMap<K, V> {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        f.debug_map()
115            .entries(self.entries.iter().map(|(k, v)| (k, v)))
116            .finish()
117    }
118}
119
120// ─── ResourcePool ──────────────────────────────────────────────────────
121
122/// A per-battler pool of generic, game-defined **consumable resources** (MP / SP
123/// / TP / mana / charge — the engine assigns the *concept* no name; doc 13 §4,
124/// the highest-value STATE-SEAM).
125///
126/// ## Why a `u16`-keyed container, not `EnumMap<P::Resource>`
127///
128/// The doc's sketch suggested `EnumMap<P::Resource, u16>` on `BattlerState`. That
129/// would require a new associated type `Resource` on [`BattleProvider`]. A
130/// *non-defaulted* assoc type breaks all 16 existing `impl BattleProvider` blocks
131/// (an unacceptable, non-additive change), and a *defaulted* assoc type
132/// (`type Resource: … = …;`) is **unstable on stable Rust** (`E0658`,
133/// `associated_type_defaults`, issue #29661) — so it cannot ship on the
134/// workspace's stable toolchain either. The fully-additive choice is therefore a
135/// **`P`-independent** container keyed by a small **opaque integer resource id**
136/// (the game assigns ids; the engine never interprets them). This adds a single
137/// field to `BattlerState` and **zero** changes to the `BattleProvider` trait or
138/// any of its 16 impls.
139///
140/// Each entry tracks `(current, max)`. The pool **defaults to EMPTY**, so a
141/// battler that declares no resource behaves byte-for-byte as before. Lookups are
142/// an O(n) linear scan, like [`EnumMap`] — pools are tiny.
143#[derive(Default)]
144pub struct ResourcePool {
145    /// `(resource_id, current, max)` triples. Game-assigned ids; opaque to the engine.
146    entries: Vec<(u16, u16, u16)>,
147}
148
149impl ResourcePool {
150    /// An empty pool (the default — a battler with no declared resources).
151    pub fn new() -> Self {
152        Self {
153            entries: Vec::new(),
154        }
155    }
156
157    /// Set `id`'s current value and max (inserting the entry if absent). The
158    /// current value is clamped to `max`.
159    pub fn set(&mut self, id: u16, current: u16, max: u16) {
160        let current = current.min(max);
161        if let Some(e) = self.entries.iter_mut().find(|(k, _, _)| *k == id) {
162            e.1 = current;
163            e.2 = max;
164        } else {
165            self.entries.push((id, current, max));
166        }
167    }
168
169    /// The current value of resource `id`, or `None` if the battler has no such
170    /// resource. A `None` resource is treated as "cannot pay any positive cost".
171    pub fn current(&self, id: u16) -> Option<u16> {
172        self.entries
173            .iter()
174            .find(|(k, _, _)| *k == id)
175            .map(|(_, cur, _)| *cur)
176    }
177
178    /// The max value of resource `id`, or `None` if absent.
179    pub fn max(&self, id: u16) -> Option<u16> {
180        self.entries
181            .iter()
182            .find(|(k, _, _)| *k == id)
183            .map(|(_, _, m)| *m)
184    }
185
186    /// Whether the battler can pay `amount` of resource `id`. A `0` cost is always
187    /// payable (even with no such resource — it is inert). A positive cost on a
188    /// resource the battler does not have is **not** payable.
189    pub fn can_pay(&self, id: u16, amount: u16) -> bool {
190        if amount == 0 {
191            return true;
192        }
193        self.current(id).map(|cur| cur >= amount).unwrap_or(false)
194    }
195
196    /// Deduct `amount` of resource `id` (saturating at 0). No-op for a `0` amount
197    /// or an absent resource. Returns `true` if a deduction was applied. **Pure
198    /// arithmetic — consumes no randomness.**
199    pub fn pay(&mut self, id: u16, amount: u16) -> bool {
200        if amount == 0 {
201            return false;
202        }
203        if let Some(e) = self.entries.iter_mut().find(|(k, _, _)| *k == id) {
204            e.1 = e.1.saturating_sub(amount);
205            true
206        } else {
207            false
208        }
209    }
210
211    /// Restore `amount` of resource `id` (clamped to its max). No-op for an absent
212    /// resource.
213    pub fn restore(&mut self, id: u16, amount: u16) {
214        if let Some(e) = self.entries.iter_mut().find(|(k, _, _)| *k == id) {
215            e.1 = e.1.saturating_add(amount).min(e.2);
216        }
217    }
218
219    /// Number of distinct resources in the pool.
220    pub fn len(&self) -> usize {
221        self.entries.len()
222    }
223
224    /// Whether the pool declares no resources (the default — fully inert).
225    pub fn is_empty(&self) -> bool {
226        self.entries.is_empty()
227    }
228}
229
230impl Clone for ResourcePool {
231    fn clone(&self) -> Self {
232        Self {
233            entries: self.entries.clone(),
234        }
235    }
236}
237
238impl fmt::Debug for ResourcePool {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        f.debug_map()
241            .entries(self.entries.iter().map(|(k, cur, max)| (k, (cur, max))))
242            .finish()
243    }
244}
245
246// ─── DamageResult ──────────────────────────────────────────────────────
247
248/// Result of a damage calculation.
249#[derive(Debug, Clone, Copy, PartialEq)]
250pub struct DamageResult {
251    /// Final damage after all modifiers.
252    pub damage: u16,
253    /// Type effectiveness multiplier (1.0 = neutral, 2.0 = super effective).
254    pub effectiveness: f32,
255    /// Whether the move missed (effectiveness = 0 or accuracy failed).
256    pub is_miss: bool,
257}
258
259// ─── MoveEffect ────────────────────────────────────────────────────────
260
261/// Categories of additional effects a move can have.
262///
263/// This enum classifies post-damage or primary effects so that effect
264/// handlers can dispatch to the appropriate logic. The concrete parameters
265/// (power, type, status details) are obtained from the provider.
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum MoveEffect {
268    /// Pure damage, no additional effect.
269    Damage,
270    /// Heals the user.
271    Heal,
272    /// Inflicts a status condition on the target.
273    StatusCondition,
274    /// Raises or lowers a stat stage.
275    StatChange,
276    /// Hits multiple times in one turn.
277    MultiHit,
278    /// User must recharge next turn.
279    Recharge,
280    /// Drains HP from the target.
281    DrainHp,
282    /// User takes recoil damage.
283    Recoil,
284    /// Has a chance to flinch the target.
285    Flinch,
286    /// Sets or changes field conditions (weather, terrain, screens).
287    FieldEffect,
288    /// Fixed-damage special move (ignores normal formula).
289    SpecialDamage,
290    /// One-hit KO attempt.
291    Ohko,
292    /// Multi-turn move (charge turn, trapping, etc.).
293    MultiTurn,
294}
295
296// ─── EffectResult ──────────────────────────────────────────────────────
297
298/// Outcome of applying a move effect.
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub enum EffectResult {
301    /// No effect triggered.
302    NoEffect,
303    /// Damage was dealt.
304    DamageDealt { amount: u16 },
305    /// HP was healed.
306    Healed { amount: u16 },
307    /// A status condition was inflicted.
308    StatusInflicted,
309    /// Status infliction failed (immune, already afflicted, etc.).
310    StatusFailed,
311    /// A stat stage was modified.
312    StatModified { stages: i8 },
313    /// Stat modification was blocked (e.g. by a protective effect).
314    StatBlocked,
315    /// HP was drained from the target and healed to the user.
316    HpDrained { drained: u16 },
317    /// The user took recoil damage.
318    RecoilDamage { recoil: u16 },
319    /// The target fainted.
320    Fainted,
321    /// The move missed.
322    Miss,
323    /// The move landed as a critical hit.
324    CriticalHit,
325    /// The move hit multiple times.
326    MultiHit { hits: u8 },
327    /// The user must recharge next turn.
328    MustRecharge,
329    /// A field effect was set up.
330    FieldEffectSet,
331}
332
333// ─── Weather / Terrain ─────────────────────────────────────────────────
334
335/// Field weather condition.
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
337pub enum Weather {
338    /// No special weather.
339    #[default]
340    Clear,
341    /// Rain — may boost water moves, weaken fire moves.
342    Rain,
343    /// Intense sunlight — may boost fire moves, weaken water moves.
344    Sun,
345    /// Sandstorm — deals residual damage to non-rock/ground/steel types.
346    Sandstorm,
347    /// Hail/Snow — deals residual damage to non-ice types.
348    Snow,
349}
350
351/// Field terrain condition.
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
353pub enum Terrain {
354    /// Normal ground.
355    #[default]
356    Normal,
357    /// Electrified terrain.
358    Electric,
359    /// Overgrown terrain.
360    Grassy,
361    /// Mist-covered terrain.
362    Misty,
363    /// Psychic-charged terrain.
364    Psychic,
365}
366
367// ─── Turn-driver support types (P0b) ───────────────────────────────────
368
369/// A reference to one battler position on the battlefield.
370///
371/// In a 1v1 Gen-1-style battle `slot` is always `0`; the field is present so
372/// the same type serves multi-slot formats. `side` is `0` for the player and
373/// `1` for the opponent, matching `BattleState::player_battlers` /
374/// `opponent_battlers`.
375#[derive(Clone, Copy, Debug, PartialEq, Eq)]
376pub struct BattlerRef {
377    /// `0` = player side, `1` = opponent side.
378    pub side: u8,
379    /// Slot within the side (0-based). `0` for 1v1.
380    pub slot: u8,
381}
382
383impl BattlerRef {
384    /// Construct a battler reference.
385    pub const fn new(side: u8, slot: u8) -> Self {
386        Self { side, slot }
387    }
388
389    /// The player's lead battler (`side 0, slot 0`).
390    pub const PLAYER: Self = Self { side: 0, slot: 0 };
391    /// The opponent's lead battler (`side 1, slot 0`).
392    pub const OPPONENT: Self = Self { side: 1, slot: 0 };
393}
394
395/// A turn-ordering sort key produced by [`BattleProvider::turn_order_key`].
396///
397/// The engine **stable-sorts actors ascending** by this key, so the game must
398/// encode "acts earlier" as a *smaller* key. The tuple is ordered
399/// `(priority, speed, tiebreak)`:
400///
401/// * `priority` — move/action priority bracket. Higher-priority actions act
402///   first, so the game should store the *negated* bracket here (e.g. `-1` for
403///   a +1 priority move) to make ascending order put them first.
404/// * `speed` — likewise the *negated* effective speed, so the faster battler
405///   (larger speed → more-negative key) sorts first. The game applies its own
406///   speed modifiers (paralysis cut, badge boosts) before negating.
407/// * `tiebreak` — a game-supplied tie-break value (e.g. a coin-flip drawn from
408///   the injected RNG for the Gen-1 speed tie). Smaller acts first.
409///
410/// Stability of the sort guarantees that equal keys preserve submission order,
411/// so a game that wants pure submission order on ties can leave `tiebreak` at
412/// `0`.
413#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
414pub struct OrderKey(pub i32, pub i32, pub u32);
415
416/// The action a battler has chosen to take this turn.
417///
418/// Generic over the provider so the concrete move/item identifiers stay
419/// game-defined. Catch/Run resolution is game-specific: a game models them as
420/// `UseItem`/`Run` and decides the [`BattleEnd`](driver::BattleEnd) outcome
421/// inside its hooks.
422pub enum BattleAction<P: BattleProvider + ?Sized> {
423    /// Use a move against the implied target (the opposing lead in 1v1).
424    Fight {
425        /// The chosen move.
426        move_: P::Move,
427    },
428    /// Switch the active battler to another party slot on the same side.
429    Switch {
430        /// Destination slot within the acting side's party.
431        to_slot: usize,
432    },
433    /// Use an item (potions, status cures, capture devices, …).
434    UseItem {
435        /// The chosen item.
436        item: P::Item,
437    },
438    /// Attempt to flee the battle.
439    Run,
440    /// Do nothing this turn (e.g. recharging, forced inaction handled upstream).
441    Nothing,
442}
443
444impl<P: BattleProvider + ?Sized> Clone for BattleAction<P> {
445    fn clone(&self) -> Self {
446        match self {
447            BattleAction::Fight { move_ } => BattleAction::Fight {
448                move_: move_.clone(),
449            },
450            BattleAction::Switch { to_slot } => BattleAction::Switch { to_slot: *to_slot },
451            BattleAction::UseItem { item } => BattleAction::UseItem { item: item.clone() },
452            BattleAction::Run => BattleAction::Run,
453            BattleAction::Nothing => BattleAction::Nothing,
454        }
455    }
456}
457
458impl<P: BattleProvider + ?Sized> fmt::Debug for BattleAction<P> {
459    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460        match self {
461            BattleAction::Fight { move_ } => f.debug_struct("Fight").field("move_", move_).finish(),
462            BattleAction::Switch { to_slot } => {
463                f.debug_struct("Switch").field("to_slot", to_slot).finish()
464            }
465            BattleAction::UseItem { item } => {
466                f.debug_struct("UseItem").field("item", item).finish()
467            }
468            BattleAction::Run => write!(f, "Run"),
469            BattleAction::Nothing => write!(f, "Nothing"),
470        }
471    }
472}
473
474/// Result of [`BattleProvider::before_move`]: the pre-move status gate.
475///
476/// The game owns every Gen-1 quirk here — sleep counter decremented *before*
477/// the move, freeze, the full-paralysis roll, flinch, Hyper Beam recharge,
478/// confusion self-hit, and partial-trap continuation — and reports the outcome
479/// to the engine, which only sequences it.
480pub enum MoveGate<P: BattleProvider + ?Sized> {
481    /// The battler may act with its chosen action.
482    Acts,
483    /// The battler is prevented from acting; the carried [`EffectResult`]
484    /// (e.g. `Miss`, `NoEffect`) is surfaced as a [`TurnEvent`](driver::TurnEvent).
485    Prevented(EffectResult),
486    /// The chosen action is replaced by a forced one (confusion self-hit,
487    /// thrash/petal-dance continuation, …). The engine executes the forced
488    /// action instead.
489    ForcedAction(BattleAction<P>),
490}
491
492impl<P: BattleProvider + ?Sized> fmt::Debug for MoveGate<P> {
493    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494        match self {
495            MoveGate::Acts => write!(f, "Acts"),
496            MoveGate::Prevented(r) => f.debug_tuple("Prevented").field(r).finish(),
497            MoveGate::ForcedAction(a) => f.debug_tuple("ForcedAction").field(a).finish(),
498        }
499    }
500}
501
502// ─── BattleProvider ────────────────────────────────────────────────────
503
504/// Central trait for battle system data and formula implementations.
505///
506/// Implementations provide concrete types for monsters, moves, abilities,
507/// statuses, stats, species, and types, along with methods for damage
508/// calculation, monster creation, move selection, and effect application.
509///
510/// All methods take `&self` — the provider is a read-only data source.
511///
512/// # Associated Types
513///
514/// | Type       | Purpose                                              |
515/// |------------|------------------------------------------------------|
516/// | `Monster`  | Full monster/character data (species + stats + HP)   |
517/// | `Move`     | Move identifier (ID or inline data struct)           |
518/// | `Ability`  | Passive ability identifier                           |
519/// | `Status`   | Status condition identifier (burn, sleep, etc.)      |
520/// | `Stat`     | Stat identifier (HP, ATK, DEF, SPD, etc.)           |
521/// | `Species`  | Species/monster-type identifier                      |
522/// | `Type`     | Elemental type identifier                            |
523/// | `Item`     | Usable item identifier                               |
524pub trait BattleProvider {
525    /// The monster data type.
526    type Monster: Clone + fmt::Debug;
527    /// The move identifier or data type.
528    type Move: Clone + fmt::Debug;
529    /// The ability identifier type.
530    type Ability: Clone + fmt::Debug;
531    /// The status condition type.
532    type Status: Clone + PartialEq + fmt::Debug;
533    /// The stat identifier type. Must be `Copy` + `PartialEq` for [`EnumMap`] key usage.
534    type Stat: Copy + PartialEq + fmt::Debug;
535    /// The species identifier type.
536    type Species: Clone + fmt::Debug;
537    /// The elemental type identifier type.
538    type Type: Clone + PartialEq + fmt::Debug;
539    /// The item identifier type.
540    type Item: Clone + fmt::Debug;
541
542    /// Calculate damage for a move against a defender.
543    ///
544    /// The provider is responsible for implementing the damage formula,
545    /// consulting the type chart, and applying stat stage modifiers.
546    fn calculate_damage(
547        &self,
548        move_: &Self::Move,
549        attacker: &BattlerState<Self>,
550        defender: &BattlerState<Self>,
551        random: u8,
552        is_critical: bool,
553    ) -> DamageResult;
554
555    /// Select a move for the given battler. Typically delegates to a
556    /// [`BattleAI`] implementation.
557    fn select_move(&self, battler: &BattlerState<Self>, state: &BattleState<Self>) -> Self::Move;
558
559    /// Apply a move's effect after damage has been dealt. Typically
560    /// delegates to an [`EffectHandler`] implementation.
561    fn apply_move_effect(
562        &self,
563        effect: MoveEffect,
564        user: &mut BattlerState<Self>,
565        target: &mut BattlerState<Self>,
566    ) -> EffectResult;
567
568    /// Construct a battler state from species and level data.
569    ///
570    /// The provider looks up base stats, learnable moves, and type
571    /// information for the given species at the given level.
572    fn create_monster(&self, species: Self::Species, level: u8) -> BattlerState<Self>;
573
574    /// Check whether a battler has fainted (HP = 0).
575    fn check_faint(&self, battler: &BattlerState<Self>) -> bool {
576        battler.hp == 0
577    }
578
579    // ── Resource cost hook (doc 13 §4 — the MP/SP/mana cost gate) ─────
580    //
581    // DEFAULTED so all 16 existing `impl BattleProvider` blocks compile
582    // UNCHANGED. With the default empty slice (and the empty-by-default
583    // [`ResourcePool`]), the engine's cost gate is a pure NO-OP: every existing
584    // battle is byte-identical and the draw sequence is untouched (the gate is
585    // pure arithmetic — it consumes NO randomness).
586
587    /// The resource cost of `move_` as a list of `(resource_id, amount)` pairs.
588    ///
589    /// The engine assigns the resources no meaning (it is not "MP" to the engine
590    /// — the ids are game-defined and opaque). Before resolving a `Fight` action,
591    /// the [`StackDriver`](crate::battle::stack::StackDriver) checks the actor can
592    /// pay **all** of these against its [`ResourcePool`]; if it cannot, the move is
593    /// **prevented** (the existing `BeforeMove`/`Fail` prevention path), and if it
594    /// can, the engine deducts each cost. The check and deduction are **pure
595    /// arithmetic and consume no `rng`**, so a game that declares costs keeps its
596    /// exact draw order.
597    ///
598    /// The default is `&[]` ⇒ no cost ⇒ the gate is inert, so a battler with no
599    /// resources behaves exactly as today.
600    fn move_cost(&self, _move_: &Self::Move) -> &[(u16, u16)] {
601        &[]
602    }
603
604    // ── Turn-driver hooks (P0b) ──────────────────────────────────────
605    //
606    // All four are *defaulted* so pre-existing providers (and other
607    // games) keep compiling unchanged. The engine driver supplies the
608    // *sequencing*; these hooks supply the game's *numbers and rule
609    // decisions* (C3/C4). Randomness is injected via `&mut dyn BattleRng`
610    // so the engine never links `rand` (C2) and the game owns the exact
611    // draw order (critical for Gen-1 quirks).
612
613    /// Return the [`OrderKey`] used to sequence `who`'s `action` this turn.
614    ///
615    /// The engine stable-sorts actors ascending by this key (C4). The game
616    /// encodes the priority bracket, effective speed (after paralysis cut,
617    /// badge boosts, …), and any tie-break — e.g. the Gen-1 speed-tie coin
618    /// flip drawn from `rng`. The default treats every actor as equal, so the
619    /// driver falls back to stable submission order.
620    fn turn_order_key(
621        &self,
622        _state: &BattleState<Self>,
623        _who: BattlerRef,
624        _action: &BattleAction<Self>,
625        _rng: &mut dyn BattleRng,
626    ) -> OrderKey
627    where
628        Self: Sized,
629    {
630        OrderKey::default()
631    }
632
633    /// Pre-move status gate: may `who` act with `action` this turn?
634    ///
635    /// The game owns sleep (counter decremented *before* the move), freeze,
636    /// the full-paralysis roll, flinch, Hyper Beam recharge, confusion
637    /// self-hit, partial-trap continuation, etc. The default always allows
638    /// the action.
639    fn before_move(
640        &self,
641        _state: &mut BattleState<Self>,
642        _who: BattlerRef,
643        _action: &BattleAction<Self>,
644        _rng: &mut dyn BattleRng,
645    ) -> MoveGate<Self>
646    where
647        Self: Sized,
648    {
649        MoveGate::Acts
650    }
651
652    /// Accuracy check for `who`'s `move_` against `target` (incl. the Gen-1
653    /// 1/256 miss). Returns `true` if the move connects. Default: always hits.
654    fn accuracy_check(
655        &self,
656        _state: &BattleState<Self>,
657        _who: BattlerRef,
658        _target: BattlerRef,
659        _move_: &Self::Move,
660        _rng: &mut dyn BattleRng,
661    ) -> bool
662    where
663        Self: Sized,
664    {
665        true
666    }
667
668    /// End-of-turn residual hook: poison/burn tick, leech seed, wrap damage,
669    /// weather, etc. The game owns ordering and numbers; it returns the
670    /// resulting [`EffectResult`]s for the UI. Default: no residuals.
671    fn end_of_turn(
672        &self,
673        _state: &mut BattleState<Self>,
674        _rng: &mut dyn BattleRng,
675    ) -> Vec<EffectResult>
676    where
677        Self: Sized,
678    {
679        Vec::new()
680    }
681
682    /// Roll whether `who`'s `move_` against `target` lands a **critical hit**.
683    ///
684    /// The driver calls this *before* [`calculate_damage`](Self::calculate_damage)
685    /// and feeds the result back in as its `is_critical` argument, then surfaces
686    /// it into the [`TurnEvent::Damage`](crate::battle::driver::TurnEvent::Damage)
687    /// event's `critical` flag — so a provider-computed crit reaches both the
688    /// damage formula and the UI/event stream.
689    ///
690    /// The game owns the crit-rate math (base speed / Focus Energy / high-crit
691    /// moves and every Gen-1 quirk), drawing from `rng` as needed. The default
692    /// never crits and draws **no** randomness, so pre-existing providers and
693    /// other games keep their exact draw sequence unchanged.
694    fn roll_critical(
695        &self,
696        _state: &BattleState<Self>,
697        _who: BattlerRef,
698        _target: BattlerRef,
699        _move_: &Self::Move,
700        _rng: &mut dyn BattleRng,
701    ) -> bool
702    where
703        Self: Sized,
704    {
705        false
706    }
707}
708
709// ─── BattlerState ──────────────────────────────────────────────────────
710
711/// The battle state of a single monster/character.
712///
713/// Tracks HP, stats, stat-stage modifiers, status condition, and known
714/// moves. Generic over the [`BattleProvider`] that supplies the concrete
715/// type identifiers.
716pub struct BattlerState<P: BattleProvider + ?Sized> {
717    /// Species of this monster.
718    pub species: P::Species,
719    /// Current hit points.
720    pub hp: u16,
721    /// Maximum hit points.
722    pub max_hp: u16,
723    /// The battler's level. **Defaults to `50`** in [`new`](Self::new) (set it via
724    /// [`with_level`](Self::with_level) or the field directly). The engine never
725    /// interprets it; a provider's damage/effect logic reads it as needed (e.g. the
726    /// level term in a damage formula). Additive — existing callers that ignore it
727    /// keep the prior fixed-50 behaviour.
728    pub level: u8,
729    /// Base stat values, keyed by stat ID.
730    pub stats: EnumMap<P::Stat, u16>,
731    /// Stat-stage modifiers (-6 to +6), keyed by stat ID.
732    pub stat_stages: EnumMap<P::Stat, i8>,
733    /// Current status condition, if any.
734    pub status: Option<P::Status>,
735    /// Known moves.
736    pub moves: Vec<P::Move>,
737    /// Generic, game-defined consumable resources (MP / SP / mana / charge —
738    /// doc 13 §4). **Defaults to EMPTY**, so a battler that declares no resource
739    /// behaves exactly as before. Keyed by an opaque game-assigned `u16` id; the
740    /// engine never interprets a resource's *meaning* (it is not "MP" to the
741    /// engine). See [`ResourcePool`].
742    pub resources: ResourcePool,
743}
744
745impl<P: BattleProvider + ?Sized> BattlerState<P> {
746    /// Create a new battler state.
747    pub fn new(
748        species: P::Species,
749        hp: u16,
750        max_hp: u16,
751        stats: EnumMap<P::Stat, u16>,
752        moves: Vec<P::Move>,
753    ) -> Self {
754        Self {
755            species,
756            hp,
757            max_hp,
758            level: 50,
759            stats,
760            stat_stages: EnumMap::default(),
761            status: None,
762            moves,
763            resources: ResourcePool::new(),
764        }
765    }
766
767    /// Builder: set the battler's [`level`](Self::level).
768    pub fn with_level(mut self, level: u8) -> Self {
769        self.level = level;
770        self
771    }
772
773    /// Apply damage, clamping HP to zero (never negative).
774    pub fn take_damage(&mut self, amount: u16) {
775        self.hp = self.hp.saturating_sub(amount);
776    }
777
778    /// Heal HP, clamping to max_hp.
779    pub fn heal(&mut self, amount: u16) {
780        self.hp = self.hp.saturating_add(amount).min(self.max_hp);
781    }
782
783    /// Builder: declare a generic resource (id, current = max) on this battler.
784    /// Returns `self` so it chains after [`new`](Self::new) without changing the
785    /// constructor's signature (the additivity invariant).
786    pub fn with_resource(mut self, id: u16, max: u16) -> Self {
787        self.resources.set(id, max, max);
788        self
789    }
790
791    /// Whether this battler can pay `amount` of resource `id` (delegates to
792    /// [`ResourcePool::can_pay`]). A `0` cost is always payable; a positive cost
793    /// on an undeclared resource is not. **Pure — consumes no randomness.**
794    pub fn can_pay_resource(&self, id: u16, amount: u16) -> bool {
795        self.resources.can_pay(id, amount)
796    }
797
798    /// Deduct `amount` of resource `id` (saturating). **Pure arithmetic — no rng.**
799    pub fn pay_resource(&mut self, id: u16, amount: u16) -> bool {
800        self.resources.pay(id, amount)
801    }
802}
803
804impl<P: BattleProvider + ?Sized> fmt::Debug for BattlerState<P> {
805    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
806        f.debug_struct("BattlerState")
807            .field("species", &self.species)
808            .field("hp", &self.hp)
809            .field("max_hp", &self.max_hp)
810            .field("stats", &self.stats)
811            .field("stat_stages", &self.stat_stages)
812            .field("status", &self.status)
813            .field("moves", &self.moves)
814            .field("resources", &self.resources)
815            .finish()
816    }
817}
818
819impl<P: BattleProvider + ?Sized> Clone for BattlerState<P> {
820    fn clone(&self) -> Self {
821        Self {
822            species: self.species.clone(),
823            hp: self.hp,
824            max_hp: self.max_hp,
825            level: self.level,
826            stats: self.stats.clone(),
827            stat_stages: self.stat_stages.clone(),
828            status: self.status.clone(),
829            moves: self.moves.clone(),
830            resources: self.resources.clone(),
831        }
832    }
833}
834
835// ─── BattleState ───────────────────────────────────────────────────────
836
837/// The complete state of an ongoing battle.
838///
839/// Tracks both sides' parties, turn order, field conditions, and turn
840/// counter. Generic over the [`BattleProvider`] that supplies the concrete
841/// type identifiers.
842pub struct BattleState<P: BattleProvider + ?Sized> {
843    /// The player's party (one or more battlers).
844    pub player_battlers: Vec<BattlerState<P>>,
845    /// The opponent's party (one or more battlers).
846    pub opponent_battlers: Vec<BattlerState<P>>,
847    /// Turn order — indices into the combined battler list.
848    pub turn_order: Vec<usize>,
849    /// Current weather condition.
850    pub weather: Weather,
851    /// Current terrain condition.
852    pub terrain: Terrain,
853    /// Number of turns elapsed.
854    pub turn_count: u32,
855}
856
857impl<P: BattleProvider + ?Sized> BattleState<P> {
858    /// Create a new battle state.
859    pub fn new(
860        player_battlers: Vec<BattlerState<P>>,
861        opponent_battlers: Vec<BattlerState<P>>,
862    ) -> Self {
863        Self {
864            player_battlers,
865            opponent_battlers,
866            turn_order: Vec::new(),
867            weather: Weather::default(),
868            terrain: Terrain::default(),
869            turn_count: 0,
870        }
871    }
872
873    /// Get a reference to the active player battler (first in party).
874    pub fn active_player(&self) -> Option<&BattlerState<P>> {
875        self.player_battlers.first()
876    }
877
878    /// Get a mutable reference to the active player battler.
879    pub fn active_player_mut(&mut self) -> Option<&mut BattlerState<P>> {
880        self.player_battlers.first_mut()
881    }
882
883    /// Get a reference to the active opponent battler.
884    pub fn active_opponent(&self) -> Option<&BattlerState<P>> {
885        self.opponent_battlers.first()
886    }
887
888    /// Get a mutable reference to the active opponent battler.
889    pub fn active_opponent_mut(&mut self) -> Option<&mut BattlerState<P>> {
890        self.opponent_battlers.first_mut()
891    }
892}
893
894impl<P: BattleProvider + ?Sized> fmt::Debug for BattleState<P> {
895    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
896        f.debug_struct("BattleState")
897            .field("player_battlers", &self.player_battlers)
898            .field("opponent_battlers", &self.opponent_battlers)
899            .field("turn_order", &self.turn_order)
900            .field("weather", &self.weather)
901            .field("terrain", &self.terrain)
902            .field("turn_count", &self.turn_count)
903            .finish()
904    }
905}
906
907impl<P: BattleProvider + ?Sized> Clone for BattleState<P> {
908    fn clone(&self) -> Self {
909        Self {
910            player_battlers: self.player_battlers.clone(),
911            opponent_battlers: self.opponent_battlers.clone(),
912            turn_order: self.turn_order.clone(),
913            weather: self.weather,
914            terrain: self.terrain,
915            turn_count: self.turn_count,
916        }
917    }
918}
919
920// ─── TypeChart ─────────────────────────────────────────────────────────
921
922/// N×N type effectiveness matrix.
923///
924/// Returns a multiplier for an attacking type against a set of defending
925/// types. The implementation can be a lookup table, a procedural rule,
926/// or any combination thereof.
927pub trait TypeChart {
928    /// The elemental type identifier.
929    type Type: PartialEq + fmt::Debug;
930
931    /// Compute the effectiveness of an attacking type against one or more
932    /// defending types.
933    ///
934    /// The result is a multiplier: `1.0` = neutral, `2.0` = super effective,
935    /// `0.5` = not very effective, `0.0` = immune.
936    ///
937    /// For dual-type defenders, the caller passes both types in the
938    /// `defending` slice and the chart computes the combined multiplier.
939    fn effectiveness(attacking: &Self::Type, defending: &[Self::Type]) -> f32;
940}
941
942// ─── BattleAI ──────────────────────────────────────────────────────────
943
944/// AI decision-making for battle actions.
945///
946/// Implementations decide which move to use, whether to switch monsters,
947/// and whether to use items — all based on the current battle state.
948pub trait BattleAI<P: BattleProvider + ?Sized> {
949    /// Select a move for the given battler.
950    fn select_move(&self, battler: &BattlerState<P>, state: &BattleState<P>) -> P::Move;
951
952    /// Decide whether the battler should switch to a different monster.
953    fn should_switch(&self, battler: &BattlerState<P>) -> bool;
954
955    /// Decide whether the battler should use an item, and which one.
956    fn should_use_item(&self, battler: &BattlerState<P>) -> Option<P::Item>;
957}
958
959// ─── EffectHandler ─────────────────────────────────────────────────────
960
961/// Handles move effects on battler state.
962///
963/// After damage has been dealt, the effect handler applies additional
964/// effects such as status infliction, stat modification, healing, and
965/// field changes.
966pub trait EffectHandler<P: BattleProvider + ?Sized> {
967    /// Apply a move effect to the user and/or target.
968    ///
969    /// * `effect` — The category of effect to apply.
970    /// * `user` — The battler that used the move.
971    /// * `target` — The battler being targeted.
972    /// * `provider` — The battle data provider for lookups.
973    fn handle_effect(
974        &self,
975        effect: MoveEffect,
976        user: &mut BattlerState<P>,
977        target: &mut BattlerState<P>,
978        provider: &P,
979    ) -> EffectResult;
980}
981
982// ─── Tests ─────────────────────────────────────────────────────────────
983
984#[cfg(test)]
985mod tests {
986    use super::*;
987
988    // ── Mock Types ────────────────────────────────────────────────────
989
990    /// A simple three-type rock-paper-scissors system:
991    ///   TypeA beats TypeB, TypeB beats TypeC, TypeC beats TypeA.
992    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
993    enum MockType {
994        TypeA,
995        TypeB,
996        TypeC,
997    }
998
999    /// Stats for the mock battle system.
1000    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1001    enum MockStat {
1002        Hp,
1003        Attack,
1004        Defense,
1005    }
1006
1007    /// Status conditions.
1008    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1009    #[allow(dead_code)]
1010    enum MockStatus {
1011        Poison,
1012        Burn,
1013        Sleep,
1014    }
1015
1016    /// Species identifiers.
1017    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1018    #[allow(dead_code)]
1019    enum MockSpecies {
1020        Alpha,
1021        Beta,
1022        Gamma,
1023    }
1024
1025    /// A move with power, type, and accuracy.
1026    #[derive(Debug, Clone, PartialEq)]
1027    struct MockMove {
1028        name: String,
1029        power: u8,
1030        move_type: MockType,
1031        accuracy: u8,
1032    }
1033
1034    /// A monster (used as Monster associated type).
1035    #[derive(Debug, Clone, PartialEq)]
1036    struct MockMonster {
1037        name: String,
1038        monster_type: MockType,
1039        hp: u16,
1040        attack: u16,
1041        defense: u16,
1042    }
1043
1044    // ── Mock TypeChart ───────────────────────────────────────────────
1045
1046    struct MockTypeChart;
1047
1048    impl TypeChart for MockTypeChart {
1049        type Type = MockType;
1050
1051        fn effectiveness(attacking: &Self::Type, defending: &[Self::Type]) -> f32 {
1052            // Single-type check against the first defending type.
1053            let def = defending.first().copied().unwrap_or(MockType::TypeA);
1054            match (attacking, def) {
1055                // TypeA beats TypeB (super effective, 2x)
1056                (MockType::TypeA, MockType::TypeB) => 2.0,
1057                // TypeB beats TypeC (super effective, 2x)
1058                (MockType::TypeB, MockType::TypeC) => 2.0,
1059                // TypeC beats TypeA (super effective, 2x)
1060                (MockType::TypeC, MockType::TypeA) => 2.0,
1061                // Reversed: not very effective (0.5x)
1062                (MockType::TypeA, MockType::TypeC) => 0.5,
1063                (MockType::TypeB, MockType::TypeA) => 0.5,
1064                (MockType::TypeC, MockType::TypeB) => 0.5,
1065                // Same type: not very effective (0.5x)
1066                (a, d) if *a == d => 0.5,
1067                // Default: neutral
1068                _ => 1.0,
1069            }
1070        }
1071    }
1072
1073    // ── Mock Provider ─────────────────────────────────────────────────
1074
1075    struct MockProvider;
1076
1077    impl BattleProvider for MockProvider {
1078        type Monster = MockMonster;
1079        type Move = MockMove;
1080        type Ability = String;
1081        type Status = MockStatus;
1082        type Stat = MockStat;
1083        type Species = MockSpecies;
1084        type Type = MockType;
1085        type Item = String;
1086
1087        fn calculate_damage(
1088            &self,
1089            move_: &Self::Move,
1090            attacker: &BattlerState<Self>,
1091            defender: &BattlerState<Self>,
1092            _random: u8,
1093            _is_critical: bool,
1094        ) -> DamageResult {
1095            let atk = attacker.stats.get(MockStat::Attack).copied().unwrap_or(0);
1096            let def = defender
1097                .stats
1098                .get(MockStat::Defense)
1099                .copied()
1100                .unwrap_or(1)
1101                .max(1);
1102            let defender_types: Vec<MockType> = vec![]; // Simplified: uses move_type only
1103
1104            let effectiveness = MockTypeChart::effectiveness(&move_.move_type, &defender_types);
1105
1106            if effectiveness == 0.0 {
1107                return DamageResult {
1108                    damage: 0,
1109                    effectiveness,
1110                    is_miss: true,
1111                };
1112            }
1113
1114            // Simple formula: (power * attack / defense) * effectiveness
1115            let base = (move_.power as u32 * atk as u32 / def as u32) as u16;
1116            let damage = ((base as f32) * effectiveness) as u16;
1117
1118            DamageResult {
1119                damage: damage.max(1),
1120                effectiveness,
1121                is_miss: false,
1122            }
1123        }
1124
1125        fn select_move(
1126            &self,
1127            battler: &BattlerState<Self>,
1128            _state: &BattleState<Self>,
1129        ) -> Self::Move {
1130            // Always pick the first move.
1131            battler.moves.first().cloned().unwrap()
1132        }
1133
1134        fn apply_move_effect(
1135            &self,
1136            _effect: MoveEffect,
1137            user: &mut BattlerState<Self>,
1138            target: &mut BattlerState<Self>,
1139        ) -> EffectResult {
1140            // Simplified: always deal damage based on first move's power.
1141            let power = user.moves.first().map(|m| m.power).unwrap_or(0);
1142            if power == 0 {
1143                return EffectResult::NoEffect;
1144            }
1145            target.take_damage(power as u16);
1146            EffectResult::DamageDealt {
1147                amount: power as u16,
1148            }
1149        }
1150
1151        fn create_monster(&self, species: Self::Species, _level: u8) -> BattlerState<Self> {
1152            // Simplified: hardcoded stats per species.
1153            let (hp, atk, def, _monster_type) = match species {
1154                MockSpecies::Alpha => (100, 50, 30, MockType::TypeA),
1155                MockSpecies::Beta => (120, 40, 40, MockType::TypeB),
1156                MockSpecies::Gamma => (90, 60, 20, MockType::TypeC),
1157            };
1158            let mut stats = EnumMap::new();
1159            stats.set(MockStat::Hp, hp);
1160            stats.set(MockStat::Attack, atk);
1161            stats.set(MockStat::Defense, def);
1162            BattlerState::new(species, hp, hp, stats, Vec::new())
1163        }
1164    }
1165
1166    // ── Mock AI ───────────────────────────────────────────────────────
1167
1168    struct MockAI;
1169
1170    impl BattleAI<MockProvider> for MockAI {
1171        fn select_move(
1172            &self,
1173            battler: &BattlerState<MockProvider>,
1174            _state: &BattleState<MockProvider>,
1175        ) -> MockMove {
1176            battler.moves.first().cloned().unwrap()
1177        }
1178
1179        fn should_switch(&self, _battler: &BattlerState<MockProvider>) -> bool {
1180            false
1181        }
1182
1183        fn should_use_item(&self, _battler: &BattlerState<MockProvider>) -> Option<String> {
1184            None
1185        }
1186    }
1187
1188    // ── Mock EffectHandler ────────────────────────────────────────────
1189
1190    struct MockEffectHandler;
1191
1192    impl EffectHandler<MockProvider> for MockEffectHandler {
1193        fn handle_effect(
1194            &self,
1195            effect: MoveEffect,
1196            _user: &mut BattlerState<MockProvider>,
1197            target: &mut BattlerState<MockProvider>,
1198            _provider: &MockProvider,
1199        ) -> EffectResult {
1200            match effect {
1201                MoveEffect::Damage => {
1202                    target.take_damage(10);
1203                    EffectResult::DamageDealt { amount: 10 }
1204                }
1205                MoveEffect::Heal => {
1206                    target.heal(10);
1207                    EffectResult::Healed { amount: 10 }
1208                }
1209                MoveEffect::StatusCondition => EffectResult::StatusInflicted,
1210                MoveEffect::StatChange => EffectResult::StatModified { stages: 1 },
1211                MoveEffect::MultiHit => EffectResult::MultiHit { hits: 3 },
1212                MoveEffect::Recharge => EffectResult::MustRecharge,
1213                _ => EffectResult::NoEffect,
1214            }
1215        }
1216    }
1217
1218    // ── Helper ────────────────────────────────────────────────────────
1219
1220    /// Create a mock battler with given type and stats.
1221    fn make_battler(
1222        species: MockSpecies,
1223        hp: u16,
1224        atk: u16,
1225        def: u16,
1226    ) -> BattlerState<MockProvider> {
1227        let mut stats = EnumMap::new();
1228        stats.set(MockStat::Hp, hp);
1229        stats.set(MockStat::Attack, atk);
1230        stats.set(MockStat::Defense, def);
1231        BattlerState::new(species, hp, hp, stats, Vec::new())
1232    }
1233
1234    fn make_move(name: &str, power: u8, move_type: MockType) -> MockMove {
1235        MockMove {
1236            name: name.to_string(),
1237            power,
1238            move_type,
1239            accuracy: 255,
1240        }
1241    }
1242
1243    // ── Tests: TypeChart ──────────────────────────────────────────────
1244
1245    #[test]
1246    fn type_a_beats_type_b() {
1247        let eff = MockTypeChart::effectiveness(&MockType::TypeA, &[MockType::TypeB]);
1248        assert!(
1249            (eff - 2.0).abs() < f32::EPSILON,
1250            "TypeA should be 2x vs TypeB, got {eff}"
1251        );
1252    }
1253
1254    #[test]
1255    fn type_a_vs_type_a_is_half() {
1256        let eff = MockTypeChart::effectiveness(&MockType::TypeA, &[MockType::TypeA]);
1257        assert!(
1258            (eff - 0.5).abs() < f32::EPSILON,
1259            "TypeA vs TypeA should be 0.5x, got {eff}"
1260        );
1261    }
1262
1263    #[test]
1264    fn type_b_beats_type_c() {
1265        let eff = MockTypeChart::effectiveness(&MockType::TypeB, &[MockType::TypeC]);
1266        assert!(
1267            (eff - 2.0).abs() < f32::EPSILON,
1268            "TypeB should be 2x vs TypeC, got {eff}"
1269        );
1270    }
1271
1272    #[test]
1273    fn type_c_beats_type_a() {
1274        let eff = MockTypeChart::effectiveness(&MockType::TypeC, &[MockType::TypeA]);
1275        assert!(
1276            (eff - 2.0).abs() < f32::EPSILON,
1277            "TypeC should be 2x vs TypeA, got {eff}"
1278        );
1279    }
1280
1281    #[test]
1282    fn type_b_vs_type_a_is_half() {
1283        let eff = MockTypeChart::effectiveness(&MockType::TypeB, &[MockType::TypeA]);
1284        assert!(
1285            (eff - 0.5).abs() < f32::EPSILON,
1286            "TypeB vs TypeA should be 0.5x, got {eff}"
1287        );
1288    }
1289
1290    // ── Tests: BattleProvider ─────────────────────────────────────────
1291
1292    #[test]
1293    fn provider_can_be_implemented() {
1294        let provider = MockProvider;
1295        let battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1296        assert!(!provider.check_faint(&battler));
1297    }
1298
1299    #[test]
1300    fn create_monster_returns_battler() {
1301        let provider = MockProvider;
1302        let battler = provider.create_monster(MockSpecies::Alpha, 50);
1303        assert_eq!(battler.hp, 100);
1304        assert_eq!(battler.max_hp, 100);
1305    }
1306
1307    #[test]
1308    fn calculate_damage_with_type_advantage() {
1309        let provider = MockProvider;
1310        let attacker = make_battler(MockSpecies::Alpha, 100, 50, 30);
1311        let defender = make_battler(MockSpecies::Beta, 100, 40, 40);
1312        let mv = make_move("SuperPunch", 60, MockType::TypeA);
1313
1314        let result = provider.calculate_damage(&mv, &attacker, &defender, 255, false);
1315
1316        // TypeA vs defender... but our simplified formula uses move_type only.
1317        // The effectiveness for TypeA in MockTypeChart depends on defender's type.
1318        // Since we don't pass defender type to the calculator, it defaults to neutral.
1319        // The damage = (60 * 50 / 40) * 1.0 = 75
1320        assert!(result.damage > 0);
1321        assert!(!result.is_miss);
1322    }
1323
1324    #[test]
1325    fn select_move_returns_first_move() {
1326        let provider = MockProvider;
1327        let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1328        let mv = make_move("Tackle", 40, MockType::TypeA);
1329        battler.moves = vec![mv.clone()];
1330
1331        let state = BattleState::<MockProvider>::new(
1332            vec![battler.clone()],
1333            vec![make_battler(MockSpecies::Beta, 100, 40, 40)],
1334        );
1335
1336        let selected = provider.select_move(&battler, &state);
1337        assert_eq!(selected.name, "Tackle");
1338    }
1339
1340    #[test]
1341    fn apply_move_effect_deals_damage() {
1342        let provider = MockProvider;
1343        let mut user = make_battler(MockSpecies::Alpha, 100, 50, 30);
1344        let mv = make_move("Tackle", 40, MockType::TypeA);
1345        user.moves = vec![mv];
1346        let mut target = make_battler(MockSpecies::Beta, 100, 40, 40);
1347
1348        let result = provider.apply_move_effect(MoveEffect::Damage, &mut user, &mut target);
1349        assert!(matches!(result, EffectResult::DamageDealt { .. }));
1350        assert_eq!(target.hp, 60); // 100 - 40
1351    }
1352
1353    #[test]
1354    fn check_faint_detects_zero_hp() {
1355        let provider = MockProvider;
1356        let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1357        assert!(!provider.check_faint(&battler));
1358
1359        battler.hp = 0;
1360        assert!(provider.check_faint(&battler));
1361    }
1362
1363    // ── Tests: BattleAI trait ─────────────────────────────────────────
1364
1365    #[test]
1366    fn ai_trait_can_be_implemented() {
1367        let ai = MockAI;
1368        let battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1369        let _state = BattleState::<MockProvider>::new(
1370            vec![battler.clone()],
1371            vec![make_battler(MockSpecies::Beta, 100, 40, 40)],
1372        );
1373
1374        assert!(!ai.should_switch(&battler));
1375        assert!(ai.should_use_item(&battler).is_none());
1376    }
1377
1378    #[test]
1379    fn ai_select_move_works() {
1380        let ai = MockAI;
1381        let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1382        let mv = make_move("Fireball", 50, MockType::TypeA);
1383        battler.moves = vec![mv.clone()];
1384
1385        let state = BattleState::<MockProvider>::new(
1386            vec![battler.clone()],
1387            vec![make_battler(MockSpecies::Beta, 100, 40, 40)],
1388        );
1389
1390        let chosen = ai.select_move(&battler, &state);
1391        assert_eq!(chosen.name, "Fireball");
1392    }
1393
1394    // ── Tests: EffectHandler trait ────────────────────────────────────
1395
1396    #[test]
1397    fn effect_handler_trait_can_be_implemented() {
1398        let handler = MockEffectHandler;
1399        let provider = MockProvider;
1400        let mut user = make_battler(MockSpecies::Alpha, 100, 50, 30);
1401        let mut target = make_battler(MockSpecies::Beta, 100, 40, 40);
1402
1403        let result = handler.handle_effect(MoveEffect::Damage, &mut user, &mut target, &provider);
1404        assert!(matches!(result, EffectResult::DamageDealt { amount: 10 }));
1405        assert_eq!(target.hp, 90);
1406    }
1407
1408    #[test]
1409    fn effect_handler_heal_works() {
1410        let handler = MockEffectHandler;
1411        let provider = MockProvider;
1412        let mut user = make_battler(MockSpecies::Alpha, 50, 50, 30);
1413        user.hp = 50;
1414        let mut target = make_battler(MockSpecies::Beta, 100, 40, 40);
1415
1416        let result = handler.handle_effect(MoveEffect::Heal, &mut user, &mut target, &provider);
1417        assert!(matches!(result, EffectResult::Healed { amount: 10 }));
1418        assert_eq!(target.hp, 100); // 100 + capped at max
1419    }
1420
1421    #[test]
1422    fn effect_handler_status_and_stat() {
1423        let handler = MockEffectHandler;
1424        let provider = MockProvider;
1425        let mut user = make_battler(MockSpecies::Alpha, 100, 50, 30);
1426        let mut target = make_battler(MockSpecies::Beta, 100, 40, 40);
1427
1428        let r1 = handler.handle_effect(
1429            MoveEffect::StatusCondition,
1430            &mut user,
1431            &mut target,
1432            &provider,
1433        );
1434        assert_eq!(r1, EffectResult::StatusInflicted);
1435
1436        let r2 = handler.handle_effect(MoveEffect::StatChange, &mut user, &mut target, &provider);
1437        assert_eq!(r2, EffectResult::StatModified { stages: 1 });
1438    }
1439
1440    // ── Tests: BattlerState ───────────────────────────────────────────
1441
1442    #[test]
1443    fn battler_state_take_damage() {
1444        let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1445        battler.take_damage(30);
1446        assert_eq!(battler.hp, 70);
1447    }
1448
1449    #[test]
1450    fn battler_state_take_damage_no_underflow() {
1451        let mut battler = make_battler(MockSpecies::Alpha, 10, 50, 30);
1452        battler.take_damage(999);
1453        assert_eq!(battler.hp, 0);
1454    }
1455
1456    #[test]
1457    fn battler_state_heal() {
1458        let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1459        battler.hp = 50;
1460        battler.heal(30);
1461        assert_eq!(battler.hp, 80);
1462    }
1463
1464    #[test]
1465    fn battler_state_heal_caps_at_max() {
1466        let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1467        battler.hp = 90;
1468        battler.heal(50);
1469        assert_eq!(battler.hp, 100);
1470    }
1471
1472    // ── Tests: BattleState ────────────────────────────────────────────
1473
1474    #[test]
1475    fn battle_state_defaults() {
1476        let p1 = make_battler(MockSpecies::Alpha, 100, 50, 30);
1477        let o1 = make_battler(MockSpecies::Beta, 100, 40, 40);
1478        let state = BattleState::<MockProvider>::new(vec![p1.clone()], vec![o1.clone()]);
1479
1480        assert_eq!(state.player_battlers.len(), 1);
1481        assert_eq!(state.opponent_battlers.len(), 1);
1482        assert_eq!(state.turn_count, 0);
1483        assert!(matches!(state.weather, Weather::Clear));
1484        assert!(matches!(state.terrain, Terrain::Normal));
1485    }
1486
1487    #[test]
1488    fn battle_state_active_player() {
1489        let p1 = make_battler(MockSpecies::Alpha, 100, 50, 30);
1490        let o1 = make_battler(MockSpecies::Beta, 100, 40, 40);
1491        let state = BattleState::<MockProvider>::new(vec![p1], vec![o1]);
1492
1493        let active = state.active_player().unwrap();
1494        assert_eq!(active.hp, 100);
1495    }
1496
1497    // ── Tests: EnumMap ────────────────────────────────────────────────
1498
1499    #[test]
1500    fn enum_map_set_and_get() {
1501        let mut map: EnumMap<MockStat, u16> = EnumMap::new();
1502        map.set(MockStat::Attack, 50);
1503        map.set(MockStat::Defense, 30);
1504
1505        assert_eq!(map.get(MockStat::Attack), Some(&50));
1506        assert_eq!(map.get(MockStat::Defense), Some(&30));
1507        assert_eq!(map.get(MockStat::Hp), None);
1508    }
1509
1510    #[test]
1511    fn enum_map_overwrite() {
1512        let mut map: EnumMap<MockStat, u16> = EnumMap::new();
1513        map.set(MockStat::Hp, 100);
1514        map.set(MockStat::Hp, 200);
1515
1516        assert_eq!(map.get(MockStat::Hp), Some(&200));
1517        assert_eq!(map.len(), 1);
1518    }
1519
1520    #[test]
1521    fn enum_map_default_is_empty() {
1522        let map: EnumMap<MockStat, u16> = EnumMap::default();
1523        assert!(map.is_empty());
1524        assert_eq!(map.len(), 0);
1525    }
1526
1527    // ── Tests: ResourcePool (the generic MP/SP/mana pool, doc 13 §4) ──────
1528
1529    #[test]
1530    fn resource_pool_default_is_empty_and_inert() {
1531        let pool = ResourcePool::default();
1532        assert!(pool.is_empty());
1533        assert_eq!(pool.len(), 0);
1534        // An empty pool: a 0 cost is always payable (inert); any positive cost on
1535        // an undeclared resource is NOT payable.
1536        assert!(pool.can_pay(0, 0), "0 cost is always payable");
1537        assert!(
1538            !pool.can_pay(0, 1),
1539            "positive cost on undeclared resource ⇒ not payable"
1540        );
1541        assert_eq!(pool.current(0), None);
1542    }
1543
1544    #[test]
1545    fn resource_pool_set_can_pay_and_pay() {
1546        let mut pool = ResourcePool::new();
1547        pool.set(7, 10, 10); // resource id 7, current 10, max 10
1548        assert_eq!(pool.current(7), Some(10));
1549        assert_eq!(pool.max(7), Some(10));
1550        assert!(pool.can_pay(7, 4));
1551        assert!(pool.can_pay(7, 10), "exact balance is payable");
1552        assert!(!pool.can_pay(7, 11), "over balance is not payable");
1553
1554        assert!(pool.pay(7, 4), "deduction applied");
1555        assert_eq!(pool.current(7), Some(6), "10 - 4 = 6");
1556        assert!(!pool.pay(7, 0), "0 deduction is a no-op (returns false)");
1557        assert_eq!(pool.current(7), Some(6), "0 deduction left it unchanged");
1558    }
1559
1560    #[test]
1561    fn resource_pool_pay_saturates_and_restore_clamps() {
1562        let mut pool = ResourcePool::new();
1563        pool.set(0, 3, 10);
1564        pool.pay(0, 100); // over-pay saturates at 0
1565        assert_eq!(pool.current(0), Some(0));
1566        pool.restore(0, 4);
1567        assert_eq!(pool.current(0), Some(4));
1568        pool.restore(0, 1000); // restore clamps to max
1569        assert_eq!(pool.current(0), Some(10));
1570    }
1571
1572    #[test]
1573    fn resource_pool_set_clamps_current_to_max() {
1574        let mut pool = ResourcePool::new();
1575        pool.set(0, 50, 20); // current > max
1576        assert_eq!(pool.current(0), Some(20), "current clamped to max");
1577    }
1578
1579    #[test]
1580    fn battler_state_resources_default_empty() {
1581        // A battler built via `new` (the constructor all 16 impls call) declares NO
1582        // resources — the additivity invariant.
1583        let b: BattlerState<MockProvider> =
1584            BattlerState::new(MockSpecies::Alpha, 100, 100, EnumMap::new(), vec![]);
1585        assert!(
1586            b.resources.is_empty(),
1587            "default battler has an empty resource pool"
1588        );
1589        assert!(b.can_pay_resource(0, 0), "0 cost payable on an empty pool");
1590        assert!(
1591            !b.can_pay_resource(0, 5),
1592            "positive cost unpayable on an empty pool"
1593        );
1594
1595        // `with_resource` declares one and the pay helpers work end to end.
1596        let mut b = b.with_resource(0, 8);
1597        assert!(b.can_pay_resource(0, 5));
1598        assert!(b.pay_resource(0, 5));
1599        assert_eq!(b.resources.current(0), Some(3));
1600    }
1601
1602    // ── Tests: Weather / Terrain ──────────────────────────────────────
1603
1604    #[test]
1605    fn weather_default_is_clear() {
1606        assert_eq!(Weather::default(), Weather::Clear);
1607    }
1608
1609    #[test]
1610    fn terrain_default_is_normal() {
1611        assert_eq!(Terrain::default(), Terrain::Normal);
1612    }
1613
1614    // ── Tests: MoveEffect / EffectResult ──────────────────────────────
1615
1616    #[test]
1617    fn move_effect_variants_are_available() {
1618        // Ensure all variants can be constructed.
1619        let _effects = [
1620            MoveEffect::Damage,
1621            MoveEffect::Heal,
1622            MoveEffect::StatusCondition,
1623            MoveEffect::StatChange,
1624            MoveEffect::MultiHit,
1625            MoveEffect::Recharge,
1626            MoveEffect::DrainHp,
1627            MoveEffect::Recoil,
1628            MoveEffect::Flinch,
1629            MoveEffect::FieldEffect,
1630            MoveEffect::SpecialDamage,
1631            MoveEffect::Ohko,
1632            MoveEffect::MultiTurn,
1633        ];
1634    }
1635
1636    #[test]
1637    fn effect_result_variants_are_available() {
1638        let _results = [
1639            EffectResult::NoEffect,
1640            EffectResult::DamageDealt { amount: 0 },
1641            EffectResult::Healed { amount: 0 },
1642            EffectResult::StatusInflicted,
1643            EffectResult::StatusFailed,
1644            EffectResult::StatModified { stages: 1 },
1645            EffectResult::StatBlocked,
1646            EffectResult::HpDrained { drained: 0 },
1647            EffectResult::RecoilDamage { recoil: 0 },
1648            EffectResult::Fainted,
1649            EffectResult::Miss,
1650            EffectResult::CriticalHit,
1651            EffectResult::MultiHit { hits: 2 },
1652            EffectResult::MustRecharge,
1653            EffectResult::FieldEffectSet,
1654        ];
1655    }
1656}
1657
1658// ─── Driver tests (P0b) ────────────────────────────────────────────────
1659
1660#[cfg(test)]
1661mod driver_tests {
1662    use super::driver::{BattleDriver, BattleEnd, TurnEvent};
1663    use super::rng::{BattleRng, ScriptedRng};
1664    use super::*;
1665
1666    // ── Mock world for the driver ────────────────────────────────────
1667
1668    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1669    enum DStat {
1670        Speed,
1671    }
1672    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1673    enum DStatus {
1674        Sleep,
1675    }
1676    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1677    enum DType {
1678        Normal,
1679    }
1680    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1681    enum DSpecies {
1682        Mon,
1683    }
1684    #[derive(Debug, Clone, PartialEq)]
1685    struct DMove {
1686        power: u8,
1687        accuracy: u8,
1688    }
1689
1690    /// A deterministic provider exercising every driver hook.
1691    ///
1692    /// * `turn_order_key` ranks by Speed stat (faster acts first), with a
1693    ///   coin-flip tie-break drawn from the rng (Gen-1-style speed tie).
1694    /// * `before_move` skips a battler whose status is `Sleep`.
1695    /// * `accuracy_check` consults a draw vs the move's accuracy.
1696    /// * `calculate_damage` returns the move's `power` as flat damage.
1697    /// * `end_of_turn` applies `residual` damage to every living battler.
1698    struct DProvider {
1699        residual: u16,
1700    }
1701
1702    impl BattleProvider for DProvider {
1703        type Monster = ();
1704        type Move = DMove;
1705        type Ability = ();
1706        type Status = DStatus;
1707        type Stat = DStat;
1708        type Species = DSpecies;
1709        type Type = DType;
1710        type Item = ();
1711
1712        fn calculate_damage(
1713            &self,
1714            move_: &Self::Move,
1715            _attacker: &BattlerState<Self>,
1716            _defender: &BattlerState<Self>,
1717            _random: u8,
1718            _is_critical: bool,
1719        ) -> DamageResult {
1720            DamageResult {
1721                damage: move_.power as u16,
1722                effectiveness: 1.0,
1723                is_miss: false,
1724            }
1725        }
1726
1727        fn select_move(
1728            &self,
1729            battler: &BattlerState<Self>,
1730            _state: &BattleState<Self>,
1731        ) -> Self::Move {
1732            battler.moves.first().cloned().unwrap()
1733        }
1734
1735        fn apply_move_effect(
1736            &self,
1737            _effect: MoveEffect,
1738            _user: &mut BattlerState<Self>,
1739            _target: &mut BattlerState<Self>,
1740        ) -> EffectResult {
1741            EffectResult::NoEffect
1742        }
1743
1744        fn create_monster(&self, species: Self::Species, _level: u8) -> BattlerState<Self> {
1745            BattlerState::new(species, 100, 100, EnumMap::new(), Vec::new())
1746        }
1747
1748        // ── New P0b hooks ──
1749
1750        fn turn_order_key(
1751            &self,
1752            state: &BattleState<Self>,
1753            who: BattlerRef,
1754            _action: &BattleAction<Self>,
1755            rng: &mut dyn BattleRng,
1756        ) -> OrderKey {
1757            let party = if who.side == 0 {
1758                &state.player_battlers
1759            } else {
1760                &state.opponent_battlers
1761            };
1762            let speed = party
1763                .get(who.slot as usize)
1764                .and_then(|b| b.stats.get(DStat::Speed).copied())
1765                .unwrap_or(0) as i32;
1766            // Negate speed so the faster battler sorts first; draw a coin-flip
1767            // tie-break from the rng (the draw happens for *every* actor so the
1768            // sequence is deterministic).
1769            let tiebreak = rng.next_u8() as u32;
1770            OrderKey(0, -speed, tiebreak)
1771        }
1772
1773        fn before_move(
1774            &self,
1775            state: &mut BattleState<Self>,
1776            who: BattlerRef,
1777            _action: &BattleAction<Self>,
1778            _rng: &mut dyn BattleRng,
1779        ) -> MoveGate<Self> {
1780            let party = if who.side == 0 {
1781                &state.player_battlers
1782            } else {
1783                &state.opponent_battlers
1784            };
1785            if let Some(b) = party.get(who.slot as usize) {
1786                if b.status == Some(DStatus::Sleep) {
1787                    return MoveGate::Prevented(EffectResult::NoEffect);
1788                }
1789            }
1790            MoveGate::Acts
1791        }
1792
1793        fn accuracy_check(
1794            &self,
1795            _state: &BattleState<Self>,
1796            _who: BattlerRef,
1797            _target: BattlerRef,
1798            move_: &Self::Move,
1799            rng: &mut dyn BattleRng,
1800        ) -> bool {
1801            // Hits if the drawn byte is below the move's accuracy.
1802            (rng.next_u8() as u32) < move_.accuracy as u32
1803        }
1804
1805        fn end_of_turn(
1806            &self,
1807            state: &mut BattleState<Self>,
1808            _rng: &mut dyn BattleRng,
1809        ) -> Vec<EffectResult> {
1810            let mut out = Vec::new();
1811            for party in [&mut state.player_battlers, &mut state.opponent_battlers] {
1812                for b in party.iter_mut() {
1813                    if b.hp > 0 {
1814                        b.take_damage(self.residual);
1815                        out.push(EffectResult::DamageDealt {
1816                            amount: self.residual,
1817                        });
1818                    }
1819                }
1820            }
1821            out
1822        }
1823    }
1824
1825    /// A provider that *always* lands a critical hit, drawing **no** rng in
1826    /// `roll_critical`, and doubles damage when told `is_critical`. Used to
1827    /// prove a provider-reported crit reaches both the damage formula and the
1828    /// `Damage` event's `critical` flag.
1829    struct CritProvider;
1830
1831    impl BattleProvider for CritProvider {
1832        type Monster = ();
1833        type Move = DMove;
1834        type Ability = ();
1835        type Status = DStatus;
1836        type Stat = DStat;
1837        type Species = DSpecies;
1838        type Type = DType;
1839        type Item = ();
1840
1841        fn calculate_damage(
1842            &self,
1843            move_: &Self::Move,
1844            _attacker: &BattlerState<Self>,
1845            _defender: &BattlerState<Self>,
1846            _random: u8,
1847            is_critical: bool,
1848        ) -> DamageResult {
1849            let base = move_.power as u16;
1850            DamageResult {
1851                damage: if is_critical { base * 2 } else { base },
1852                effectiveness: 1.0,
1853                is_miss: false,
1854            }
1855        }
1856
1857        fn select_move(
1858            &self,
1859            battler: &BattlerState<Self>,
1860            _state: &BattleState<Self>,
1861        ) -> Self::Move {
1862            battler.moves.first().cloned().unwrap()
1863        }
1864
1865        fn apply_move_effect(
1866            &self,
1867            _effect: MoveEffect,
1868            _user: &mut BattlerState<Self>,
1869            _target: &mut BattlerState<Self>,
1870        ) -> EffectResult {
1871            EffectResult::NoEffect
1872        }
1873
1874        fn create_monster(&self, species: Self::Species, _level: u8) -> BattlerState<Self> {
1875            BattlerState::new(species, 100, 100, EnumMap::new(), Vec::new())
1876        }
1877
1878        /// Always crit; draw no rng so byte accounting stays predictable.
1879        fn roll_critical(
1880            &self,
1881            _state: &BattleState<Self>,
1882            _who: BattlerRef,
1883            _target: BattlerRef,
1884            _move_: &Self::Move,
1885            _rng: &mut dyn BattleRng,
1886        ) -> bool {
1887            true
1888        }
1889    }
1890
1891    fn crit_mon(hp: u16, speed: u16, power: u8) -> BattlerState<CritProvider> {
1892        let mut stats = EnumMap::new();
1893        stats.set(DStat::Speed, speed);
1894        BattlerState::new(
1895            DSpecies::Mon,
1896            hp,
1897            hp,
1898            stats,
1899            vec![DMove {
1900                power,
1901                accuracy: 255,
1902            }],
1903        )
1904    }
1905
1906    fn mon(hp: u16, speed: u16, accuracy: u8, power: u8) -> BattlerState<DProvider> {
1907        let mut stats = EnumMap::new();
1908        stats.set(DStat::Speed, speed);
1909        BattlerState::new(
1910            DSpecies::Mon,
1911            hp,
1912            hp,
1913            stats,
1914            vec![DMove { power, accuracy }],
1915        )
1916    }
1917
1918    /// A connecting (accuracy 255) move action of the given power.
1919    fn fight_pow(power: u8) -> BattleAction<DProvider> {
1920        BattleAction::Fight {
1921            move_: DMove {
1922                power,
1923                accuracy: 255,
1924            },
1925        }
1926    }
1927
1928    /// A connecting move action of power 20.
1929    fn fight() -> BattleAction<DProvider> {
1930        fight_pow(20)
1931    }
1932
1933    fn first_mover(out: &TurnOutcome<DProvider>) -> BattlerRef {
1934        out.events
1935            .iter()
1936            .find_map(|e| match e {
1937                TurnEvent::MoveUsed { who, .. } => Some(*who),
1938                _ => None,
1939            })
1940            .unwrap()
1941    }
1942
1943    // ── Tests ────────────────────────────────────────────────────────
1944
1945    #[test]
1946    fn turn_order_faster_battler_acts_first() {
1947        let provider = DProvider { residual: 0 };
1948        // Player slow (speed 10), opponent fast (speed 99). Always-hit moves.
1949        let mut state = BattleState::new(vec![mon(100, 10, 255, 20)], vec![mon(100, 99, 255, 20)]);
1950        // RNG: turn_order_key draws one byte per actor (2), then accuracy + damage
1951        // bytes per fight. Keep accuracy bytes below 255 so moves connect.
1952        let mut rng = ScriptedRng::new(vec![5, 5, 0, 0, 0, 0]);
1953        let out = BattleDriver::execute_turn(&provider, &mut state, [fight(), fight()], &mut rng);
1954        assert_eq!(
1955            first_mover(&out),
1956            BattlerRef::OPPONENT,
1957            "faster opponent acts first"
1958        );
1959    }
1960
1961    #[test]
1962    fn turn_order_tie_uses_rng_tiebreak() {
1963        let provider = DProvider { residual: 0 };
1964        // Equal speed → tie broken by the rng draw. Player draws 9, opp draws 1,
1965        // so opp's tiebreak is smaller → opp first.
1966        let mut state = BattleState::new(vec![mon(100, 50, 255, 20)], vec![mon(100, 50, 255, 20)]);
1967        let mut rng = ScriptedRng::new(vec![9, 1, 0, 0, 0, 0]);
1968        let out = BattleDriver::execute_turn(&provider, &mut state, [fight(), fight()], &mut rng);
1969        assert_eq!(
1970            first_mover(&out),
1971            BattlerRef::OPPONENT,
1972            "smaller tiebreak acts first"
1973        );
1974    }
1975
1976    #[test]
1977    fn before_move_gate_skips_asleep_battler() {
1978        let provider = DProvider { residual: 0 };
1979        let mut player = mon(100, 99, 255, 20); // fast so it would act first
1980        player.status = Some(DStatus::Sleep);
1981        let mut state = BattleState::new(vec![player], vec![mon(100, 10, 255, 20)]);
1982        let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
1983        let out = BattleDriver::execute_turn(&provider, &mut state, [fight(), fight()], &mut rng);
1984
1985        assert!(
1986            out.events.iter().any(|e| matches!(
1987                e,
1988                TurnEvent::ActionPrevented { who, .. } if *who == BattlerRef::PLAYER
1989            )),
1990            "asleep player should be prevented"
1991        );
1992        assert!(
1993            out.events.iter().any(|e| matches!(
1994                e,
1995                TurnEvent::MoveUsed { who, .. } if *who == BattlerRef::OPPONENT
1996            )),
1997            "opponent should still act"
1998        );
1999    }
2000
2001    #[test]
2002    fn move_execution_applies_damage_via_hook() {
2003        let provider = DProvider { residual: 0 };
2004        // The driver uses the *action's* move (power 30), passed to the
2005        // provider's calculate_damage hook.
2006        let mut state = BattleState::new(
2007            vec![mon(100, 99, 255, 0)], // player fast
2008            vec![mon(100, 10, 255, 0)],
2009        );
2010        let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
2011        let _ = BattleDriver::execute_turn(
2012            &provider,
2013            &mut state,
2014            [fight_pow(30), fight_pow(30)],
2015            &mut rng,
2016        );
2017        // Opponent took 30 (player's move); player took 30 (opponent's move).
2018        assert_eq!(state.opponent_battlers[0].hp, 70);
2019        assert_eq!(state.player_battlers[0].hp, 70);
2020    }
2021
2022    #[test]
2023    fn accuracy_check_can_miss() {
2024        let provider = DProvider { residual: 0 };
2025        let mut state = BattleState::new(vec![mon(100, 99, 255, 20)], vec![mon(100, 10, 255, 20)]);
2026        // Player's move has accuracy 0 → always misses; opponent's connects.
2027        let player_fight = BattleAction::Fight {
2028            move_: DMove {
2029                power: 20,
2030                accuracy: 0,
2031            },
2032        };
2033        let opp_fight = BattleAction::Fight {
2034            move_: DMove {
2035                power: 20,
2036                accuracy: 255,
2037            },
2038        };
2039        let mut rng = ScriptedRng::new(vec![0, 0, 100, 0, 100, 0]);
2040        let out =
2041            BattleDriver::execute_turn(&provider, &mut state, [player_fight, opp_fight], &mut rng);
2042        // Opponent untouched (player missed); player took 20.
2043        assert_eq!(state.opponent_battlers[0].hp, 100);
2044        assert_eq!(state.player_battlers[0].hp, 80);
2045        assert!(out.events.iter().any(|e| matches!(
2046            e,
2047            TurnEvent::Missed { who, .. } if *who == BattlerRef::PLAYER
2048        )));
2049    }
2050
2051    #[test]
2052    fn end_of_turn_residual_ticks() {
2053        let provider = DProvider { residual: 5 };
2054        let mut state = BattleState::new(vec![mon(100, 50, 255, 0)], vec![mon(100, 50, 255, 0)]);
2055        // Power-0 moves so only the residual changes HP.
2056        let mut rng = ScriptedRng::new(vec![1, 2, 0, 0, 0, 0]);
2057        let out = BattleDriver::execute_turn(
2058            &provider,
2059            &mut state,
2060            [fight_pow(0), fight_pow(0)],
2061            &mut rng,
2062        );
2063        assert_eq!(state.player_battlers[0].hp, 95);
2064        assert_eq!(state.opponent_battlers[0].hp, 95);
2065        assert_eq!(
2066            out.events
2067                .iter()
2068                .filter(|e| matches!(e, TurnEvent::Residual { .. }))
2069                .count(),
2070            2
2071        );
2072    }
2073
2074    #[test]
2075    fn faint_leads_to_battle_end_player_win() {
2076        let provider = DProvider { residual: 0 };
2077        // Player fast; its power-200 move faints the opponent (100 hp) → PlayerWin.
2078        let mut state = BattleState::new(vec![mon(100, 99, 255, 0)], vec![mon(100, 10, 255, 0)]);
2079        let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
2080        let out =
2081            BattleDriver::execute_turn(&provider, &mut state, [fight_pow(200), fight()], &mut rng);
2082        assert_eq!(state.opponent_battlers[0].hp, 0);
2083        assert_eq!(out.battle_over, Some(BattleEnd::PlayerWin));
2084        assert!(out
2085            .events
2086            .iter()
2087            .any(|e| matches!(e, TurnEvent::Faint { who } if *who == BattlerRef::OPPONENT)));
2088        // Opponent should NOT have acted (battle short-circuited after faint).
2089        assert!(!out.events.iter().any(|e| matches!(
2090            e,
2091            TurnEvent::MoveUsed { who, .. } if *who == BattlerRef::OPPONENT
2092        )));
2093    }
2094
2095    #[test]
2096    fn faint_leads_to_battle_end_player_loss() {
2097        let provider = DProvider { residual: 0 };
2098        // Opponent fast; its power-200 move faints the player first → PlayerLoss.
2099        let mut state = BattleState::new(vec![mon(100, 10, 255, 0)], vec![mon(100, 99, 255, 0)]);
2100        let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
2101        let out =
2102            BattleDriver::execute_turn(&provider, &mut state, [fight(), fight_pow(200)], &mut rng);
2103        assert_eq!(state.player_battlers[0].hp, 0);
2104        assert_eq!(out.battle_over, Some(BattleEnd::PlayerLoss));
2105    }
2106
2107    #[test]
2108    fn switch_swaps_active_slot() {
2109        let provider = DProvider { residual: 0 };
2110        let mut state = BattleState::new(
2111            vec![mon(100, 50, 255, 20), mon(80, 50, 255, 20)],
2112            vec![mon(100, 50, 255, 20)],
2113        );
2114        let switch = BattleAction::Switch { to_slot: 1 };
2115        // Equal speed: tie keeps submission order, so player's switch resolves
2116        // first, then opponent fights the newly-active slot.
2117        let mut rng = ScriptedRng::new(vec![5, 5, 0, 0]);
2118        let _ = BattleDriver::execute_turn(&provider, &mut state, [switch, fight()], &mut rng);
2119        // Slot 0 is now the former slot-1 mon (80 max hp), which took 20 from the
2120        // opponent's fight → 60.
2121        assert_eq!(state.player_battlers[0].max_hp, 80);
2122        assert_eq!(state.player_battlers[0].hp, 60);
2123    }
2124
2125    #[test]
2126    fn scripted_rng_draw_order_is_deterministic() {
2127        // Two identical runs with the same script produce identical outcomes,
2128        // proving the draw order is stable / game-controlled.
2129        let run = || {
2130            let provider = DProvider { residual: 3 };
2131            let mut state =
2132                BattleState::new(vec![mon(100, 50, 200, 15)], vec![mon(100, 50, 200, 15)]);
2133            let mut rng = ScriptedRng::new(vec![7, 2, 10, 0, 10, 0]);
2134            let _ = BattleDriver::execute_turn(&provider, &mut state, [fight(), fight()], &mut rng);
2135            (
2136                state.player_battlers[0].hp,
2137                state.opponent_battlers[0].hp,
2138                rng.consumed(),
2139            )
2140        };
2141        assert_eq!(run(), run());
2142    }
2143
2144    #[test]
2145    fn provider_critical_hit_surfaces_into_damage_event() {
2146        // `CritProvider::roll_critical` always crits (drawing no rng) and its
2147        // `calculate_damage` doubles damage on a crit. We assert the crit both
2148        // (a) flows into the formula (doubled damage / hp) and (b) surfaces as
2149        // `critical: true` on the player's `Damage` event.
2150        let provider = CritProvider;
2151        // Player fast (acts first), power-10 move → 20 damage on a crit.
2152        let mut state = BattleState::new(vec![crit_mon(100, 99, 10)], vec![crit_mon(100, 10, 10)]);
2153        // Default turn_order/accuracy/roll_critical draw no rng; only the two
2154        // `calculate_damage` random bytes are consumed.
2155        let mut rng = ScriptedRng::new(vec![0, 0]);
2156        let crit_fight = || BattleAction::<CritProvider>::Fight {
2157            move_: DMove {
2158                power: 10,
2159                accuracy: 255,
2160            },
2161        };
2162        let out = BattleDriver::execute_turn(
2163            &provider,
2164            &mut state,
2165            [crit_fight(), crit_fight()],
2166            &mut rng,
2167        );
2168
2169        // (a) Crit doubled the damage in the formula.
2170        assert_eq!(
2171            state.opponent_battlers[0].hp, 80,
2172            "crit doubled 10 → 20 dmg"
2173        );
2174
2175        // (b) The player's Damage event reports the crit.
2176        let player_dmg = out
2177            .events
2178            .iter()
2179            .find_map(|e| match e {
2180                TurnEvent::Damage {
2181                    who,
2182                    critical,
2183                    amount,
2184                    ..
2185                } if *who == BattlerRef::PLAYER => Some((*critical, *amount)),
2186                _ => None,
2187            })
2188            .expect("player should have a Damage event");
2189        assert!(player_dmg.0, "provider crit must surface as critical: true");
2190        assert_eq!(player_dmg.1, 20, "Damage event amount reflects the crit");
2191    }
2192
2193    #[test]
2194    fn default_roll_critical_yields_non_critical_damage_event() {
2195        // `DProvider` does not override `roll_critical`, so the default (never
2196        // crit, no rng) keeps `critical: false` on the Damage event.
2197        let provider = DProvider { residual: 0 };
2198        let mut state = BattleState::new(vec![mon(100, 99, 255, 30)], vec![mon(100, 10, 255, 0)]);
2199        let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
2200        let out = BattleDriver::execute_turn(
2201            &provider,
2202            &mut state,
2203            [fight_pow(30), fight_pow(0)],
2204            &mut rng,
2205        );
2206        assert!(out.events.iter().any(|e| matches!(
2207            e,
2208            TurnEvent::Damage { who, critical: false, .. } if *who == BattlerRef::PLAYER
2209        )));
2210    }
2211}