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 std::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_ } => {
462                f.debug_struct("Fight").field("move_", move_).finish()
463            }
464            BattleAction::Switch { to_slot } => {
465                f.debug_struct("Switch").field("to_slot", to_slot).finish()
466            }
467            BattleAction::UseItem { item } => {
468                f.debug_struct("UseItem").field("item", item).finish()
469            }
470            BattleAction::Run => write!(f, "Run"),
471            BattleAction::Nothing => write!(f, "Nothing"),
472        }
473    }
474}
475
476/// Result of [`BattleProvider::before_move`]: the pre-move status gate.
477///
478/// The game owns every Gen-1 quirk here — sleep counter decremented *before*
479/// the move, freeze, the full-paralysis roll, flinch, Hyper Beam recharge,
480/// confusion self-hit, and partial-trap continuation — and reports the outcome
481/// to the engine, which only sequences it.
482pub enum MoveGate<P: BattleProvider + ?Sized> {
483    /// The battler may act with its chosen action.
484    Acts,
485    /// The battler is prevented from acting; the carried [`EffectResult`]
486    /// (e.g. `Miss`, `NoEffect`) is surfaced as a [`TurnEvent`](driver::TurnEvent).
487    Prevented(EffectResult),
488    /// The chosen action is replaced by a forced one (confusion self-hit,
489    /// thrash/petal-dance continuation, …). The engine executes the forced
490    /// action instead.
491    ForcedAction(BattleAction<P>),
492}
493
494impl<P: BattleProvider + ?Sized> fmt::Debug for MoveGate<P> {
495    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
496        match self {
497            MoveGate::Acts => write!(f, "Acts"),
498            MoveGate::Prevented(r) => f.debug_tuple("Prevented").field(r).finish(),
499            MoveGate::ForcedAction(a) => f.debug_tuple("ForcedAction").field(a).finish(),
500        }
501    }
502}
503
504// ─── BattleProvider ────────────────────────────────────────────────────
505
506/// Central trait for battle system data and formula implementations.
507///
508/// Implementations provide concrete types for monsters, moves, abilities,
509/// statuses, stats, species, and types, along with methods for damage
510/// calculation, monster creation, move selection, and effect application.
511///
512/// All methods take `&self` — the provider is a read-only data source.
513///
514/// # Associated Types
515///
516/// | Type       | Purpose                                              |
517/// |------------|------------------------------------------------------|
518/// | `Monster`  | Full monster/character data (species + stats + HP)   |
519/// | `Move`     | Move identifier (ID or inline data struct)           |
520/// | `Ability`  | Passive ability identifier                           |
521/// | `Status`   | Status condition identifier (burn, sleep, etc.)      |
522/// | `Stat`     | Stat identifier (HP, ATK, DEF, SPD, etc.)           |
523/// | `Species`  | Species/monster-type identifier                      |
524/// | `Type`     | Elemental type identifier                            |
525/// | `Item`     | Usable item identifier                               |
526pub trait BattleProvider {
527    /// The monster data type.
528    type Monster: Clone + fmt::Debug;
529    /// The move identifier or data type.
530    type Move: Clone + fmt::Debug;
531    /// The ability identifier type.
532    type Ability: Clone + fmt::Debug;
533    /// The status condition type.
534    type Status: Clone + PartialEq + fmt::Debug;
535    /// The stat identifier type. Must be `Copy` + `PartialEq` for [`EnumMap`] key usage.
536    type Stat: Copy + PartialEq + fmt::Debug;
537    /// The species identifier type.
538    type Species: Clone + fmt::Debug;
539    /// The elemental type identifier type.
540    type Type: Clone + PartialEq + fmt::Debug;
541    /// The item identifier type.
542    type Item: Clone + fmt::Debug;
543
544    /// Calculate damage for a move against a defender.
545    ///
546    /// The provider is responsible for implementing the damage formula,
547    /// consulting the type chart, and applying stat stage modifiers.
548    fn calculate_damage(
549        &self,
550        move_: &Self::Move,
551        attacker: &BattlerState<Self>,
552        defender: &BattlerState<Self>,
553        random: u8,
554        is_critical: bool,
555    ) -> DamageResult;
556
557    /// Select a move for the given battler. Typically delegates to a
558    /// [`BattleAI`] implementation.
559    fn select_move(
560        &self,
561        battler: &BattlerState<Self>,
562        state: &BattleState<Self>,
563    ) -> Self::Move;
564
565    /// Apply a move's effect after damage has been dealt. Typically
566    /// delegates to an [`EffectHandler`] implementation.
567    fn apply_move_effect(
568        &self,
569        effect: MoveEffect,
570        user: &mut BattlerState<Self>,
571        target: &mut BattlerState<Self>,
572    ) -> EffectResult;
573
574    /// Construct a battler state from species and level data.
575    ///
576    /// The provider looks up base stats, learnable moves, and type
577    /// information for the given species at the given level.
578    fn create_monster(&self, species: Self::Species, level: u8) -> BattlerState<Self>;
579
580    /// Check whether a battler has fainted (HP = 0).
581    fn check_faint(&self, battler: &BattlerState<Self>) -> bool {
582        battler.hp == 0
583    }
584
585    // ── Resource cost hook (doc 13 §4 — the MP/SP/mana cost gate) ─────
586    //
587    // DEFAULTED so all 16 existing `impl BattleProvider` blocks compile
588    // UNCHANGED. With the default empty slice (and the empty-by-default
589    // [`ResourcePool`]), the engine's cost gate is a pure NO-OP: every existing
590    // battle is byte-identical and the draw sequence is untouched (the gate is
591    // pure arithmetic — it consumes NO randomness).
592
593    /// The resource cost of `move_` as a list of `(resource_id, amount)` pairs.
594    ///
595    /// The engine assigns the resources no meaning (it is not "MP" to the engine
596    /// — the ids are game-defined and opaque). Before resolving a `Fight` action,
597    /// the [`StackDriver`](crate::battle::stack::StackDriver) checks the actor can
598    /// pay **all** of these against its [`ResourcePool`]; if it cannot, the move is
599    /// **prevented** (the existing `BeforeMove`/`Fail` prevention path), and if it
600    /// can, the engine deducts each cost. The check and deduction are **pure
601    /// arithmetic and consume no `rng`**, so a game that declares costs keeps its
602    /// exact draw order.
603    ///
604    /// The default is `&[]` ⇒ no cost ⇒ the gate is inert, so a battler with no
605    /// resources behaves exactly as today.
606    fn move_cost(&self, _move_: &Self::Move) -> &[(u16, u16)] {
607        &[]
608    }
609
610    // ── Turn-driver hooks (P0b) ──────────────────────────────────────
611    //
612    // All four are *defaulted* so pre-existing providers (and other
613    // games) keep compiling unchanged. The engine driver supplies the
614    // *sequencing*; these hooks supply the game's *numbers and rule
615    // decisions* (C3/C4). Randomness is injected via `&mut dyn BattleRng`
616    // so the engine never links `rand` (C2) and the game owns the exact
617    // draw order (critical for Gen-1 quirks).
618
619    /// Return the [`OrderKey`] used to sequence `who`'s `action` this turn.
620    ///
621    /// The engine stable-sorts actors ascending by this key (C4). The game
622    /// encodes the priority bracket, effective speed (after paralysis cut,
623    /// badge boosts, …), and any tie-break — e.g. the Gen-1 speed-tie coin
624    /// flip drawn from `rng`. The default treats every actor as equal, so the
625    /// driver falls back to stable submission order.
626    fn turn_order_key(
627        &self,
628        _state: &BattleState<Self>,
629        _who: BattlerRef,
630        _action: &BattleAction<Self>,
631        _rng: &mut dyn BattleRng,
632    ) -> OrderKey
633    where
634        Self: Sized,
635    {
636        OrderKey::default()
637    }
638
639    /// Pre-move status gate: may `who` act with `action` this turn?
640    ///
641    /// The game owns sleep (counter decremented *before* the move), freeze,
642    /// the full-paralysis roll, flinch, Hyper Beam recharge, confusion
643    /// self-hit, partial-trap continuation, etc. The default always allows
644    /// the action.
645    fn before_move(
646        &self,
647        _state: &mut BattleState<Self>,
648        _who: BattlerRef,
649        _action: &BattleAction<Self>,
650        _rng: &mut dyn BattleRng,
651    ) -> MoveGate<Self>
652    where
653        Self: Sized,
654    {
655        MoveGate::Acts
656    }
657
658    /// Accuracy check for `who`'s `move_` against `target` (incl. the Gen-1
659    /// 1/256 miss). Returns `true` if the move connects. Default: always hits.
660    fn accuracy_check(
661        &self,
662        _state: &BattleState<Self>,
663        _who: BattlerRef,
664        _target: BattlerRef,
665        _move_: &Self::Move,
666        _rng: &mut dyn BattleRng,
667    ) -> bool
668    where
669        Self: Sized,
670    {
671        true
672    }
673
674    /// End-of-turn residual hook: poison/burn tick, leech seed, wrap damage,
675    /// weather, etc. The game owns ordering and numbers; it returns the
676    /// resulting [`EffectResult`]s for the UI. Default: no residuals.
677    fn end_of_turn(
678        &self,
679        _state: &mut BattleState<Self>,
680        _rng: &mut dyn BattleRng,
681    ) -> Vec<EffectResult>
682    where
683        Self: Sized,
684    {
685        Vec::new()
686    }
687
688    /// Roll whether `who`'s `move_` against `target` lands a **critical hit**.
689    ///
690    /// The driver calls this *before* [`calculate_damage`](Self::calculate_damage)
691    /// and feeds the result back in as its `is_critical` argument, then surfaces
692    /// it into the [`TurnEvent::Damage`](crate::battle::driver::TurnEvent::Damage)
693    /// event's `critical` flag — so a provider-computed crit reaches both the
694    /// damage formula and the UI/event stream.
695    ///
696    /// The game owns the crit-rate math (base speed / Focus Energy / high-crit
697    /// moves and every Gen-1 quirk), drawing from `rng` as needed. The default
698    /// never crits and draws **no** randomness, so pre-existing providers and
699    /// other games keep their exact draw sequence unchanged.
700    fn roll_critical(
701        &self,
702        _state: &BattleState<Self>,
703        _who: BattlerRef,
704        _target: BattlerRef,
705        _move_: &Self::Move,
706        _rng: &mut dyn BattleRng,
707    ) -> bool
708    where
709        Self: Sized,
710    {
711        false
712    }
713}
714
715// ─── BattlerState ──────────────────────────────────────────────────────
716
717/// The battle state of a single monster/character.
718///
719/// Tracks HP, stats, stat-stage modifiers, status condition, and known
720/// moves. Generic over the [`BattleProvider`] that supplies the concrete
721/// type identifiers.
722pub struct BattlerState<P: BattleProvider + ?Sized> {
723    /// Species of this monster.
724    pub species: P::Species,
725    /// Current hit points.
726    pub hp: u16,
727    /// Maximum hit points.
728    pub max_hp: u16,
729    /// The battler's level. **Defaults to `50`** in [`new`](Self::new) (set it via
730    /// [`with_level`](Self::with_level) or the field directly). The engine never
731    /// interprets it; a provider's damage/effect logic reads it as needed (e.g. the
732    /// level term in a damage formula). Additive — existing callers that ignore it
733    /// keep the prior fixed-50 behaviour.
734    pub level: u8,
735    /// Base stat values, keyed by stat ID.
736    pub stats: EnumMap<P::Stat, u16>,
737    /// Stat-stage modifiers (-6 to +6), keyed by stat ID.
738    pub stat_stages: EnumMap<P::Stat, i8>,
739    /// Current status condition, if any.
740    pub status: Option<P::Status>,
741    /// Known moves.
742    pub moves: Vec<P::Move>,
743    /// Generic, game-defined consumable resources (MP / SP / mana / charge —
744    /// doc 13 §4). **Defaults to EMPTY**, so a battler that declares no resource
745    /// behaves exactly as before. Keyed by an opaque game-assigned `u16` id; the
746    /// engine never interprets a resource's *meaning* (it is not "MP" to the
747    /// engine). See [`ResourcePool`].
748    pub resources: ResourcePool,
749}
750
751impl<P: BattleProvider + ?Sized> BattlerState<P> {
752    /// Create a new battler state.
753    pub fn new(
754        species: P::Species,
755        hp: u16,
756        max_hp: u16,
757        stats: EnumMap<P::Stat, u16>,
758        moves: Vec<P::Move>,
759    ) -> Self {
760        Self {
761            species,
762            hp,
763            max_hp,
764            level: 50,
765            stats,
766            stat_stages: EnumMap::default(),
767            status: None,
768            moves,
769            resources: ResourcePool::new(),
770        }
771    }
772
773    /// Builder: set the battler's [`level`](Self::level).
774    pub fn with_level(mut self, level: u8) -> Self {
775        self.level = level;
776        self
777    }
778
779    /// Apply damage, clamping HP to zero (never negative).
780    pub fn take_damage(&mut self, amount: u16) {
781        self.hp = self.hp.saturating_sub(amount);
782    }
783
784    /// Heal HP, clamping to max_hp.
785    pub fn heal(&mut self, amount: u16) {
786        self.hp = self.hp.saturating_add(amount).min(self.max_hp);
787    }
788
789    /// Builder: declare a generic resource (id, current = max) on this battler.
790    /// Returns `self` so it chains after [`new`](Self::new) without changing the
791    /// constructor's signature (the additivity invariant).
792    pub fn with_resource(mut self, id: u16, max: u16) -> Self {
793        self.resources.set(id, max, max);
794        self
795    }
796
797    /// Whether this battler can pay `amount` of resource `id` (delegates to
798    /// [`ResourcePool::can_pay`]). A `0` cost is always payable; a positive cost
799    /// on an undeclared resource is not. **Pure — consumes no randomness.**
800    pub fn can_pay_resource(&self, id: u16, amount: u16) -> bool {
801        self.resources.can_pay(id, amount)
802    }
803
804    /// Deduct `amount` of resource `id` (saturating). **Pure arithmetic — no rng.**
805    pub fn pay_resource(&mut self, id: u16, amount: u16) -> bool {
806        self.resources.pay(id, amount)
807    }
808}
809
810impl<P: BattleProvider + ?Sized> fmt::Debug for BattlerState<P> {
811    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
812        f.debug_struct("BattlerState")
813            .field("species", &self.species)
814            .field("hp", &self.hp)
815            .field("max_hp", &self.max_hp)
816            .field("stats", &self.stats)
817            .field("stat_stages", &self.stat_stages)
818            .field("status", &self.status)
819            .field("moves", &self.moves)
820            .field("resources", &self.resources)
821            .finish()
822    }
823}
824
825impl<P: BattleProvider + ?Sized> Clone for BattlerState<P> {
826    fn clone(&self) -> Self {
827        Self {
828            species: self.species.clone(),
829            hp: self.hp,
830            max_hp: self.max_hp,
831            level: self.level,
832            stats: self.stats.clone(),
833            stat_stages: self.stat_stages.clone(),
834            status: self.status.clone(),
835            moves: self.moves.clone(),
836            resources: self.resources.clone(),
837        }
838    }
839}
840
841// ─── BattleState ───────────────────────────────────────────────────────
842
843/// The complete state of an ongoing battle.
844///
845/// Tracks both sides' parties, turn order, field conditions, and turn
846/// counter. Generic over the [`BattleProvider`] that supplies the concrete
847/// type identifiers.
848pub struct BattleState<P: BattleProvider + ?Sized> {
849    /// The player's party (one or more battlers).
850    pub player_battlers: Vec<BattlerState<P>>,
851    /// The opponent's party (one or more battlers).
852    pub opponent_battlers: Vec<BattlerState<P>>,
853    /// Turn order — indices into the combined battler list.
854    pub turn_order: Vec<usize>,
855    /// Current weather condition.
856    pub weather: Weather,
857    /// Current terrain condition.
858    pub terrain: Terrain,
859    /// Number of turns elapsed.
860    pub turn_count: u32,
861}
862
863impl<P: BattleProvider + ?Sized> BattleState<P> {
864    /// Create a new battle state.
865    pub fn new(
866        player_battlers: Vec<BattlerState<P>>,
867        opponent_battlers: Vec<BattlerState<P>>,
868    ) -> Self {
869        Self {
870            player_battlers,
871            opponent_battlers,
872            turn_order: Vec::new(),
873            weather: Weather::default(),
874            terrain: Terrain::default(),
875            turn_count: 0,
876        }
877    }
878
879    /// Get a reference to the active player battler (first in party).
880    pub fn active_player(&self) -> Option<&BattlerState<P>> {
881        self.player_battlers.first()
882    }
883
884    /// Get a mutable reference to the active player battler.
885    pub fn active_player_mut(&mut self) -> Option<&mut BattlerState<P>> {
886        self.player_battlers.first_mut()
887    }
888
889    /// Get a reference to the active opponent battler.
890    pub fn active_opponent(&self) -> Option<&BattlerState<P>> {
891        self.opponent_battlers.first()
892    }
893
894    /// Get a mutable reference to the active opponent battler.
895    pub fn active_opponent_mut(&mut self) -> Option<&mut BattlerState<P>> {
896        self.opponent_battlers.first_mut()
897    }
898}
899
900impl<P: BattleProvider + ?Sized> fmt::Debug for BattleState<P> {
901    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
902        f.debug_struct("BattleState")
903            .field("player_battlers", &self.player_battlers)
904            .field("opponent_battlers", &self.opponent_battlers)
905            .field("turn_order", &self.turn_order)
906            .field("weather", &self.weather)
907            .field("terrain", &self.terrain)
908            .field("turn_count", &self.turn_count)
909            .finish()
910    }
911}
912
913impl<P: BattleProvider + ?Sized> Clone for BattleState<P> {
914    fn clone(&self) -> Self {
915        Self {
916            player_battlers: self.player_battlers.clone(),
917            opponent_battlers: self.opponent_battlers.clone(),
918            turn_order: self.turn_order.clone(),
919            weather: self.weather,
920            terrain: self.terrain,
921            turn_count: self.turn_count,
922        }
923    }
924}
925
926// ─── TypeChart ─────────────────────────────────────────────────────────
927
928/// N×N type effectiveness matrix.
929///
930/// Returns a multiplier for an attacking type against a set of defending
931/// types. The implementation can be a lookup table, a procedural rule,
932/// or any combination thereof.
933pub trait TypeChart {
934    /// The elemental type identifier.
935    type Type: PartialEq + fmt::Debug;
936
937    /// Compute the effectiveness of an attacking type against one or more
938    /// defending types.
939    ///
940    /// The result is a multiplier: `1.0` = neutral, `2.0` = super effective,
941    /// `0.5` = not very effective, `0.0` = immune.
942    ///
943    /// For dual-type defenders, the caller passes both types in the
944    /// `defending` slice and the chart computes the combined multiplier.
945    fn effectiveness(attacking: &Self::Type, defending: &[Self::Type]) -> f32;
946}
947
948// ─── BattleAI ──────────────────────────────────────────────────────────
949
950/// AI decision-making for battle actions.
951///
952/// Implementations decide which move to use, whether to switch monsters,
953/// and whether to use items — all based on the current battle state.
954pub trait BattleAI<P: BattleProvider + ?Sized> {
955    /// Select a move for the given battler.
956    fn select_move(&self, battler: &BattlerState<P>, state: &BattleState<P>) -> P::Move;
957
958    /// Decide whether the battler should switch to a different monster.
959    fn should_switch(&self, battler: &BattlerState<P>) -> bool;
960
961    /// Decide whether the battler should use an item, and which one.
962    fn should_use_item(&self, battler: &BattlerState<P>) -> Option<P::Item>;
963}
964
965// ─── EffectHandler ─────────────────────────────────────────────────────
966
967/// Handles move effects on battler state.
968///
969/// After damage has been dealt, the effect handler applies additional
970/// effects such as status infliction, stat modification, healing, and
971/// field changes.
972pub trait EffectHandler<P: BattleProvider + ?Sized> {
973    /// Apply a move effect to the user and/or target.
974    ///
975    /// * `effect` — The category of effect to apply.
976    /// * `user` — The battler that used the move.
977    /// * `target` — The battler being targeted.
978    /// * `provider` — The battle data provider for lookups.
979    fn handle_effect(
980        &self,
981        effect: MoveEffect,
982        user: &mut BattlerState<P>,
983        target: &mut BattlerState<P>,
984        provider: &P,
985    ) -> EffectResult;
986}
987
988// ─── Tests ─────────────────────────────────────────────────────────────
989
990#[cfg(test)]
991mod tests {
992    use super::*;
993
994    // ── Mock Types ────────────────────────────────────────────────────
995
996    /// A simple three-type rock-paper-scissors system:
997    ///   TypeA beats TypeB, TypeB beats TypeC, TypeC beats TypeA.
998    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
999    enum MockType {
1000        TypeA,
1001        TypeB,
1002        TypeC,
1003    }
1004
1005    /// Stats for the mock battle system.
1006    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1007    enum MockStat {
1008        Hp,
1009        Attack,
1010        Defense,
1011    }
1012
1013    /// Status conditions.
1014    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1015    #[allow(dead_code)]
1016    enum MockStatus {
1017        Poison,
1018        Burn,
1019        Sleep,
1020    }
1021
1022    /// Species identifiers.
1023    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1024    #[allow(dead_code)]
1025    enum MockSpecies {
1026        Alpha,
1027        Beta,
1028        Gamma,
1029    }
1030
1031    /// A move with power, type, and accuracy.
1032    #[derive(Debug, Clone, PartialEq)]
1033    struct MockMove {
1034        name: String,
1035        power: u8,
1036        move_type: MockType,
1037        accuracy: u8,
1038    }
1039
1040    /// A monster (used as Monster associated type).
1041    #[derive(Debug, Clone, PartialEq)]
1042    struct MockMonster {
1043        name: String,
1044        monster_type: MockType,
1045        hp: u16,
1046        attack: u16,
1047        defense: u16,
1048    }
1049
1050    // ── Mock TypeChart ───────────────────────────────────────────────
1051
1052    struct MockTypeChart;
1053
1054    impl TypeChart for MockTypeChart {
1055        type Type = MockType;
1056
1057        fn effectiveness(attacking: &Self::Type, defending: &[Self::Type]) -> f32 {
1058            // Single-type check against the first defending type.
1059            let def = defending.first().copied().unwrap_or(MockType::TypeA);
1060            match (attacking, def) {
1061                // TypeA beats TypeB (super effective, 2x)
1062                (MockType::TypeA, MockType::TypeB) => 2.0,
1063                // TypeB beats TypeC (super effective, 2x)
1064                (MockType::TypeB, MockType::TypeC) => 2.0,
1065                // TypeC beats TypeA (super effective, 2x)
1066                (MockType::TypeC, MockType::TypeA) => 2.0,
1067                // Reversed: not very effective (0.5x)
1068                (MockType::TypeA, MockType::TypeC) => 0.5,
1069                (MockType::TypeB, MockType::TypeA) => 0.5,
1070                (MockType::TypeC, MockType::TypeB) => 0.5,
1071                // Same type: not very effective (0.5x)
1072                (a, d) if *a == d => 0.5,
1073                // Default: neutral
1074                _ => 1.0,
1075            }
1076        }
1077    }
1078
1079    // ── Mock Provider ─────────────────────────────────────────────────
1080
1081    struct MockProvider;
1082
1083    impl BattleProvider for MockProvider {
1084        type Monster = MockMonster;
1085        type Move = MockMove;
1086        type Ability = String;
1087        type Status = MockStatus;
1088        type Stat = MockStat;
1089        type Species = MockSpecies;
1090        type Type = MockType;
1091        type Item = String;
1092
1093        fn calculate_damage(
1094            &self,
1095            move_: &Self::Move,
1096            attacker: &BattlerState<Self>,
1097            defender: &BattlerState<Self>,
1098            _random: u8,
1099            _is_critical: bool,
1100        ) -> DamageResult {
1101            let atk = attacker.stats.get(MockStat::Attack).copied().unwrap_or(0);
1102            let def = defender.stats.get(MockStat::Defense).copied().unwrap_or(1).max(1);
1103            let defender_types: Vec<MockType> = vec![]; // Simplified: uses move_type only
1104
1105            let effectiveness =
1106                MockTypeChart::effectiveness(&move_.move_type, &defender_types);
1107
1108            if effectiveness == 0.0 {
1109                return DamageResult {
1110                    damage: 0,
1111                    effectiveness,
1112                    is_miss: true,
1113                };
1114            }
1115
1116            // Simple formula: (power * attack / defense) * effectiveness
1117            let base = (move_.power as u32 * atk as u32 / def as u32) as u16;
1118            let damage = ((base as f32) * effectiveness) as u16;
1119
1120            DamageResult {
1121                damage: damage.max(1),
1122                effectiveness,
1123                is_miss: false,
1124            }
1125        }
1126
1127        fn select_move(
1128            &self,
1129            battler: &BattlerState<Self>,
1130            _state: &BattleState<Self>,
1131        ) -> Self::Move {
1132            // Always pick the first move.
1133            battler.moves.first().cloned().unwrap()
1134        }
1135
1136        fn apply_move_effect(
1137            &self,
1138            _effect: MoveEffect,
1139            user: &mut BattlerState<Self>,
1140            target: &mut BattlerState<Self>,
1141        ) -> EffectResult {
1142            // Simplified: always deal damage based on first move's power.
1143            let power = user
1144                .moves
1145                .first()
1146                .map(|m| m.power)
1147                .unwrap_or(0);
1148            if power == 0 {
1149                return EffectResult::NoEffect;
1150            }
1151            target.take_damage(power as u16);
1152            EffectResult::DamageDealt {
1153                amount: power as u16,
1154            }
1155        }
1156
1157        fn create_monster(&self, species: Self::Species, _level: u8) -> BattlerState<Self> {
1158            // Simplified: hardcoded stats per species.
1159            let (hp, atk, def, _monster_type) = match species {
1160                MockSpecies::Alpha => (100, 50, 30, MockType::TypeA),
1161                MockSpecies::Beta => (120, 40, 40, MockType::TypeB),
1162                MockSpecies::Gamma => (90, 60, 20, MockType::TypeC),
1163            };
1164            let mut stats = EnumMap::new();
1165            stats.set(MockStat::Hp, hp);
1166            stats.set(MockStat::Attack, atk);
1167            stats.set(MockStat::Defense, def);
1168            BattlerState::new(species, hp, hp, stats, Vec::new())
1169        }
1170    }
1171
1172    // ── Mock AI ───────────────────────────────────────────────────────
1173
1174    struct MockAI;
1175
1176    impl BattleAI<MockProvider> for MockAI {
1177        fn select_move(
1178            &self,
1179            battler: &BattlerState<MockProvider>,
1180            _state: &BattleState<MockProvider>,
1181        ) -> MockMove {
1182            battler.moves.first().cloned().unwrap()
1183        }
1184
1185        fn should_switch(&self, _battler: &BattlerState<MockProvider>) -> bool {
1186            false
1187        }
1188
1189        fn should_use_item(
1190            &self,
1191            _battler: &BattlerState<MockProvider>,
1192        ) -> Option<String> {
1193            None
1194        }
1195    }
1196
1197    // ── Mock EffectHandler ────────────────────────────────────────────
1198
1199    struct MockEffectHandler;
1200
1201    impl EffectHandler<MockProvider> for MockEffectHandler {
1202        fn handle_effect(
1203            &self,
1204            effect: MoveEffect,
1205            _user: &mut BattlerState<MockProvider>,
1206            target: &mut BattlerState<MockProvider>,
1207            _provider: &MockProvider,
1208        ) -> EffectResult {
1209            match effect {
1210                MoveEffect::Damage => {
1211                    target.take_damage(10);
1212                    EffectResult::DamageDealt { amount: 10 }
1213                }
1214                MoveEffect::Heal => {
1215                    target.heal(10);
1216                    EffectResult::Healed { amount: 10 }
1217                }
1218                MoveEffect::StatusCondition => EffectResult::StatusInflicted,
1219                MoveEffect::StatChange => EffectResult::StatModified { stages: 1 },
1220                MoveEffect::MultiHit => EffectResult::MultiHit { hits: 3 },
1221                MoveEffect::Recharge => EffectResult::MustRecharge,
1222                _ => EffectResult::NoEffect,
1223            }
1224        }
1225    }
1226
1227    // ── Helper ────────────────────────────────────────────────────────
1228
1229    /// Create a mock battler with given type and stats.
1230    fn make_battler(species: MockSpecies, hp: u16, atk: u16, def: u16) -> BattlerState<MockProvider> {
1231        let mut stats = EnumMap::new();
1232        stats.set(MockStat::Hp, hp);
1233        stats.set(MockStat::Attack, atk);
1234        stats.set(MockStat::Defense, def);
1235        BattlerState::new(species, hp, hp, stats, Vec::new())
1236    }
1237
1238    fn make_move(name: &str, power: u8, move_type: MockType) -> MockMove {
1239        MockMove {
1240            name: name.to_string(),
1241            power,
1242            move_type,
1243            accuracy: 255,
1244        }
1245    }
1246
1247    // ── Tests: TypeChart ──────────────────────────────────────────────
1248
1249    #[test]
1250    fn type_a_beats_type_b() {
1251        let eff = MockTypeChart::effectiveness(&MockType::TypeA, &[MockType::TypeB]);
1252        assert!((eff - 2.0).abs() < f32::EPSILON, "TypeA should be 2x vs TypeB, got {eff}");
1253    }
1254
1255    #[test]
1256    fn type_a_vs_type_a_is_half() {
1257        let eff = MockTypeChart::effectiveness(&MockType::TypeA, &[MockType::TypeA]);
1258        assert!((eff - 0.5).abs() < f32::EPSILON, "TypeA vs TypeA should be 0.5x, got {eff}");
1259    }
1260
1261    #[test]
1262    fn type_b_beats_type_c() {
1263        let eff = MockTypeChart::effectiveness(&MockType::TypeB, &[MockType::TypeC]);
1264        assert!((eff - 2.0).abs() < f32::EPSILON, "TypeB should be 2x vs TypeC, got {eff}");
1265    }
1266
1267    #[test]
1268    fn type_c_beats_type_a() {
1269        let eff = MockTypeChart::effectiveness(&MockType::TypeC, &[MockType::TypeA]);
1270        assert!((eff - 2.0).abs() < f32::EPSILON, "TypeC should be 2x vs TypeA, got {eff}");
1271    }
1272
1273    #[test]
1274    fn type_b_vs_type_a_is_half() {
1275        let eff = MockTypeChart::effectiveness(&MockType::TypeB, &[MockType::TypeA]);
1276        assert!((eff - 0.5).abs() < f32::EPSILON, "TypeB vs TypeA should be 0.5x, got {eff}");
1277    }
1278
1279    // ── Tests: BattleProvider ─────────────────────────────────────────
1280
1281    #[test]
1282    fn provider_can_be_implemented() {
1283        let provider = MockProvider;
1284        let battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1285        assert!(!provider.check_faint(&battler));
1286    }
1287
1288    #[test]
1289    fn create_monster_returns_battler() {
1290        let provider = MockProvider;
1291        let battler = provider.create_monster(MockSpecies::Alpha, 50);
1292        assert_eq!(battler.hp, 100);
1293        assert_eq!(battler.max_hp, 100);
1294    }
1295
1296    #[test]
1297    fn calculate_damage_with_type_advantage() {
1298        let provider = MockProvider;
1299        let attacker = make_battler(MockSpecies::Alpha, 100, 50, 30);
1300        let defender = make_battler(MockSpecies::Beta, 100, 40, 40);
1301        let mv = make_move("SuperPunch", 60, MockType::TypeA);
1302
1303        let result = provider.calculate_damage(&mv, &attacker, &defender, 255, false);
1304
1305        // TypeA vs defender... but our simplified formula uses move_type only.
1306        // The effectiveness for TypeA in MockTypeChart depends on defender's type.
1307        // Since we don't pass defender type to the calculator, it defaults to neutral.
1308        // The damage = (60 * 50 / 40) * 1.0 = 75
1309        assert!(result.damage > 0);
1310        assert!(!result.is_miss);
1311    }
1312
1313    #[test]
1314    fn select_move_returns_first_move() {
1315        let provider = MockProvider;
1316        let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1317        let mv = make_move("Tackle", 40, MockType::TypeA);
1318        battler.moves = vec![mv.clone()];
1319
1320        let state = BattleState::<MockProvider>::new(
1321            vec![battler.clone()],
1322            vec![make_battler(MockSpecies::Beta, 100, 40, 40)],
1323        );
1324
1325        let selected = provider.select_move(&battler, &state);
1326        assert_eq!(selected.name, "Tackle");
1327    }
1328
1329    #[test]
1330    fn apply_move_effect_deals_damage() {
1331        let provider = MockProvider;
1332        let mut user = make_battler(MockSpecies::Alpha, 100, 50, 30);
1333        let mv = make_move("Tackle", 40, MockType::TypeA);
1334        user.moves = vec![mv];
1335        let mut target = make_battler(MockSpecies::Beta, 100, 40, 40);
1336
1337        let result = provider.apply_move_effect(MoveEffect::Damage, &mut user, &mut target);
1338        assert!(matches!(result, EffectResult::DamageDealt { .. }));
1339        assert_eq!(target.hp, 60); // 100 - 40
1340    }
1341
1342    #[test]
1343    fn check_faint_detects_zero_hp() {
1344        let provider = MockProvider;
1345        let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1346        assert!(!provider.check_faint(&battler));
1347
1348        battler.hp = 0;
1349        assert!(provider.check_faint(&battler));
1350    }
1351
1352    // ── Tests: BattleAI trait ─────────────────────────────────────────
1353
1354    #[test]
1355    fn ai_trait_can_be_implemented() {
1356        let ai = MockAI;
1357        let battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1358        let _state = BattleState::<MockProvider>::new(
1359            vec![battler.clone()],
1360            vec![make_battler(MockSpecies::Beta, 100, 40, 40)],
1361        );
1362
1363        assert!(!ai.should_switch(&battler));
1364        assert!(ai.should_use_item(&battler).is_none());
1365    }
1366
1367    #[test]
1368    fn ai_select_move_works() {
1369        let ai = MockAI;
1370        let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1371        let mv = make_move("Fireball", 50, MockType::TypeA);
1372        battler.moves = vec![mv.clone()];
1373
1374        let state = BattleState::<MockProvider>::new(
1375            vec![battler.clone()],
1376            vec![make_battler(MockSpecies::Beta, 100, 40, 40)],
1377        );
1378
1379        let chosen = ai.select_move(&battler, &state);
1380        assert_eq!(chosen.name, "Fireball");
1381    }
1382
1383    // ── Tests: EffectHandler trait ────────────────────────────────────
1384
1385    #[test]
1386    fn effect_handler_trait_can_be_implemented() {
1387        let handler = MockEffectHandler;
1388        let provider = MockProvider;
1389        let mut user = make_battler(MockSpecies::Alpha, 100, 50, 30);
1390        let mut target = make_battler(MockSpecies::Beta, 100, 40, 40);
1391
1392        let result = handler.handle_effect(MoveEffect::Damage, &mut user, &mut target, &provider);
1393        assert!(matches!(result, EffectResult::DamageDealt { amount: 10 }));
1394        assert_eq!(target.hp, 90);
1395    }
1396
1397    #[test]
1398    fn effect_handler_heal_works() {
1399        let handler = MockEffectHandler;
1400        let provider = MockProvider;
1401        let mut user = make_battler(MockSpecies::Alpha, 50, 50, 30);
1402        user.hp = 50;
1403        let mut target = make_battler(MockSpecies::Beta, 100, 40, 40);
1404
1405        let result = handler.handle_effect(MoveEffect::Heal, &mut user, &mut target, &provider);
1406        assert!(matches!(result, EffectResult::Healed { amount: 10 }));
1407        assert_eq!(target.hp, 100); // 100 + capped at max
1408    }
1409
1410    #[test]
1411    fn effect_handler_status_and_stat() {
1412        let handler = MockEffectHandler;
1413        let provider = MockProvider;
1414        let mut user = make_battler(MockSpecies::Alpha, 100, 50, 30);
1415        let mut target = make_battler(MockSpecies::Beta, 100, 40, 40);
1416
1417        let r1 = handler.handle_effect(MoveEffect::StatusCondition, &mut user, &mut target, &provider);
1418        assert_eq!(r1, EffectResult::StatusInflicted);
1419
1420        let r2 = handler.handle_effect(MoveEffect::StatChange, &mut user, &mut target, &provider);
1421        assert_eq!(r2, EffectResult::StatModified { stages: 1 });
1422    }
1423
1424    // ── Tests: BattlerState ───────────────────────────────────────────
1425
1426    #[test]
1427    fn battler_state_take_damage() {
1428        let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1429        battler.take_damage(30);
1430        assert_eq!(battler.hp, 70);
1431    }
1432
1433    #[test]
1434    fn battler_state_take_damage_no_underflow() {
1435        let mut battler = make_battler(MockSpecies::Alpha, 10, 50, 30);
1436        battler.take_damage(999);
1437        assert_eq!(battler.hp, 0);
1438    }
1439
1440    #[test]
1441    fn battler_state_heal() {
1442        let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1443        battler.hp = 50;
1444        battler.heal(30);
1445        assert_eq!(battler.hp, 80);
1446    }
1447
1448    #[test]
1449    fn battler_state_heal_caps_at_max() {
1450        let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
1451        battler.hp = 90;
1452        battler.heal(50);
1453        assert_eq!(battler.hp, 100);
1454    }
1455
1456    // ── Tests: BattleState ────────────────────────────────────────────
1457
1458    #[test]
1459    fn battle_state_defaults() {
1460        let p1 = make_battler(MockSpecies::Alpha, 100, 50, 30);
1461        let o1 = make_battler(MockSpecies::Beta, 100, 40, 40);
1462        let state = BattleState::<MockProvider>::new(vec![p1.clone()], vec![o1.clone()]);
1463
1464        assert_eq!(state.player_battlers.len(), 1);
1465        assert_eq!(state.opponent_battlers.len(), 1);
1466        assert_eq!(state.turn_count, 0);
1467        assert!(matches!(state.weather, Weather::Clear));
1468        assert!(matches!(state.terrain, Terrain::Normal));
1469    }
1470
1471    #[test]
1472    fn battle_state_active_player() {
1473        let p1 = make_battler(MockSpecies::Alpha, 100, 50, 30);
1474        let o1 = make_battler(MockSpecies::Beta, 100, 40, 40);
1475        let state = BattleState::<MockProvider>::new(vec![p1], vec![o1]);
1476
1477        let active = state.active_player().unwrap();
1478        assert_eq!(active.hp, 100);
1479    }
1480
1481    // ── Tests: EnumMap ────────────────────────────────────────────────
1482
1483    #[test]
1484    fn enum_map_set_and_get() {
1485        let mut map: EnumMap<MockStat, u16> = EnumMap::new();
1486        map.set(MockStat::Attack, 50);
1487        map.set(MockStat::Defense, 30);
1488
1489        assert_eq!(map.get(MockStat::Attack), Some(&50));
1490        assert_eq!(map.get(MockStat::Defense), Some(&30));
1491        assert_eq!(map.get(MockStat::Hp), None);
1492    }
1493
1494    #[test]
1495    fn enum_map_overwrite() {
1496        let mut map: EnumMap<MockStat, u16> = EnumMap::new();
1497        map.set(MockStat::Hp, 100);
1498        map.set(MockStat::Hp, 200);
1499
1500        assert_eq!(map.get(MockStat::Hp), Some(&200));
1501        assert_eq!(map.len(), 1);
1502    }
1503
1504    #[test]
1505    fn enum_map_default_is_empty() {
1506        let map: EnumMap<MockStat, u16> = EnumMap::default();
1507        assert!(map.is_empty());
1508        assert_eq!(map.len(), 0);
1509    }
1510
1511    // ── Tests: ResourcePool (the generic MP/SP/mana pool, doc 13 §4) ──────
1512
1513    #[test]
1514    fn resource_pool_default_is_empty_and_inert() {
1515        let pool = ResourcePool::default();
1516        assert!(pool.is_empty());
1517        assert_eq!(pool.len(), 0);
1518        // An empty pool: a 0 cost is always payable (inert); any positive cost on
1519        // an undeclared resource is NOT payable.
1520        assert!(pool.can_pay(0, 0), "0 cost is always payable");
1521        assert!(!pool.can_pay(0, 1), "positive cost on undeclared resource ⇒ not payable");
1522        assert_eq!(pool.current(0), None);
1523    }
1524
1525    #[test]
1526    fn resource_pool_set_can_pay_and_pay() {
1527        let mut pool = ResourcePool::new();
1528        pool.set(7, 10, 10); // resource id 7, current 10, max 10
1529        assert_eq!(pool.current(7), Some(10));
1530        assert_eq!(pool.max(7), Some(10));
1531        assert!(pool.can_pay(7, 4));
1532        assert!(pool.can_pay(7, 10), "exact balance is payable");
1533        assert!(!pool.can_pay(7, 11), "over balance is not payable");
1534
1535        assert!(pool.pay(7, 4), "deduction applied");
1536        assert_eq!(pool.current(7), Some(6), "10 - 4 = 6");
1537        assert!(!pool.pay(7, 0), "0 deduction is a no-op (returns false)");
1538        assert_eq!(pool.current(7), Some(6), "0 deduction left it unchanged");
1539    }
1540
1541    #[test]
1542    fn resource_pool_pay_saturates_and_restore_clamps() {
1543        let mut pool = ResourcePool::new();
1544        pool.set(0, 3, 10);
1545        pool.pay(0, 100); // over-pay saturates at 0
1546        assert_eq!(pool.current(0), Some(0));
1547        pool.restore(0, 4);
1548        assert_eq!(pool.current(0), Some(4));
1549        pool.restore(0, 1000); // restore clamps to max
1550        assert_eq!(pool.current(0), Some(10));
1551    }
1552
1553    #[test]
1554    fn resource_pool_set_clamps_current_to_max() {
1555        let mut pool = ResourcePool::new();
1556        pool.set(0, 50, 20); // current > max
1557        assert_eq!(pool.current(0), Some(20), "current clamped to max");
1558    }
1559
1560    #[test]
1561    fn battler_state_resources_default_empty() {
1562        // A battler built via `new` (the constructor all 16 impls call) declares NO
1563        // resources — the additivity invariant.
1564        let b: BattlerState<MockProvider> =
1565            BattlerState::new(MockSpecies::Alpha, 100, 100, EnumMap::new(), vec![]);
1566        assert!(b.resources.is_empty(), "default battler has an empty resource pool");
1567        assert!(b.can_pay_resource(0, 0), "0 cost payable on an empty pool");
1568        assert!(!b.can_pay_resource(0, 5), "positive cost unpayable on an empty pool");
1569
1570        // `with_resource` declares one and the pay helpers work end to end.
1571        let mut b = b.with_resource(0, 8);
1572        assert!(b.can_pay_resource(0, 5));
1573        assert!(b.pay_resource(0, 5));
1574        assert_eq!(b.resources.current(0), Some(3));
1575    }
1576
1577    // ── Tests: Weather / Terrain ──────────────────────────────────────
1578
1579    #[test]
1580    fn weather_default_is_clear() {
1581        assert_eq!(Weather::default(), Weather::Clear);
1582    }
1583
1584    #[test]
1585    fn terrain_default_is_normal() {
1586        assert_eq!(Terrain::default(), Terrain::Normal);
1587    }
1588
1589    // ── Tests: MoveEffect / EffectResult ──────────────────────────────
1590
1591    #[test]
1592    fn move_effect_variants_are_available() {
1593        // Ensure all variants can be constructed.
1594        let _effects = [
1595            MoveEffect::Damage,
1596            MoveEffect::Heal,
1597            MoveEffect::StatusCondition,
1598            MoveEffect::StatChange,
1599            MoveEffect::MultiHit,
1600            MoveEffect::Recharge,
1601            MoveEffect::DrainHp,
1602            MoveEffect::Recoil,
1603            MoveEffect::Flinch,
1604            MoveEffect::FieldEffect,
1605            MoveEffect::SpecialDamage,
1606            MoveEffect::Ohko,
1607            MoveEffect::MultiTurn,
1608        ];
1609    }
1610
1611    #[test]
1612    fn effect_result_variants_are_available() {
1613        let _results = [
1614            EffectResult::NoEffect,
1615            EffectResult::DamageDealt { amount: 0 },
1616            EffectResult::Healed { amount: 0 },
1617            EffectResult::StatusInflicted,
1618            EffectResult::StatusFailed,
1619            EffectResult::StatModified { stages: 1 },
1620            EffectResult::StatBlocked,
1621            EffectResult::HpDrained { drained: 0 },
1622            EffectResult::RecoilDamage { recoil: 0 },
1623            EffectResult::Fainted,
1624            EffectResult::Miss,
1625            EffectResult::CriticalHit,
1626            EffectResult::MultiHit { hits: 2 },
1627            EffectResult::MustRecharge,
1628            EffectResult::FieldEffectSet,
1629        ];
1630    }
1631}
1632
1633// ─── Driver tests (P0b) ────────────────────────────────────────────────
1634
1635#[cfg(test)]
1636mod driver_tests {
1637    use super::driver::{BattleDriver, BattleEnd, TurnEvent};
1638    use super::rng::{BattleRng, ScriptedRng};
1639    use super::*;
1640
1641    // ── Mock world for the driver ────────────────────────────────────
1642
1643    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1644    enum DStat {
1645        Speed,
1646    }
1647    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1648    enum DStatus {
1649        Sleep,
1650    }
1651    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1652    enum DType {
1653        Normal,
1654    }
1655    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1656    enum DSpecies {
1657        Mon,
1658    }
1659    #[derive(Debug, Clone, PartialEq)]
1660    struct DMove {
1661        power: u8,
1662        accuracy: u8,
1663    }
1664
1665    /// A deterministic provider exercising every driver hook.
1666    ///
1667    /// * `turn_order_key` ranks by Speed stat (faster acts first), with a
1668    ///   coin-flip tie-break drawn from the rng (Gen-1-style speed tie).
1669    /// * `before_move` skips a battler whose status is `Sleep`.
1670    /// * `accuracy_check` consults a draw vs the move's accuracy.
1671    /// * `calculate_damage` returns the move's `power` as flat damage.
1672    /// * `end_of_turn` applies `residual` damage to every living battler.
1673    struct DProvider {
1674        residual: u16,
1675    }
1676
1677    impl BattleProvider for DProvider {
1678        type Monster = ();
1679        type Move = DMove;
1680        type Ability = ();
1681        type Status = DStatus;
1682        type Stat = DStat;
1683        type Species = DSpecies;
1684        type Type = DType;
1685        type Item = ();
1686
1687        fn calculate_damage(
1688            &self,
1689            move_: &Self::Move,
1690            _attacker: &BattlerState<Self>,
1691            _defender: &BattlerState<Self>,
1692            _random: u8,
1693            _is_critical: bool,
1694        ) -> DamageResult {
1695            DamageResult {
1696                damage: move_.power as u16,
1697                effectiveness: 1.0,
1698                is_miss: false,
1699            }
1700        }
1701
1702        fn select_move(
1703            &self,
1704            battler: &BattlerState<Self>,
1705            _state: &BattleState<Self>,
1706        ) -> Self::Move {
1707            battler.moves.first().cloned().unwrap()
1708        }
1709
1710        fn apply_move_effect(
1711            &self,
1712            _effect: MoveEffect,
1713            _user: &mut BattlerState<Self>,
1714            _target: &mut BattlerState<Self>,
1715        ) -> EffectResult {
1716            EffectResult::NoEffect
1717        }
1718
1719        fn create_monster(&self, species: Self::Species, _level: u8) -> BattlerState<Self> {
1720            BattlerState::new(species, 100, 100, EnumMap::new(), Vec::new())
1721        }
1722
1723        // ── New P0b hooks ──
1724
1725        fn turn_order_key(
1726            &self,
1727            state: &BattleState<Self>,
1728            who: BattlerRef,
1729            _action: &BattleAction<Self>,
1730            rng: &mut dyn BattleRng,
1731        ) -> OrderKey {
1732            let party = if who.side == 0 {
1733                &state.player_battlers
1734            } else {
1735                &state.opponent_battlers
1736            };
1737            let speed = party
1738                .get(who.slot as usize)
1739                .and_then(|b| b.stats.get(DStat::Speed).copied())
1740                .unwrap_or(0) as i32;
1741            // Negate speed so the faster battler sorts first; draw a coin-flip
1742            // tie-break from the rng (the draw happens for *every* actor so the
1743            // sequence is deterministic).
1744            let tiebreak = rng.next_u8() as u32;
1745            OrderKey(0, -speed, tiebreak)
1746        }
1747
1748        fn before_move(
1749            &self,
1750            state: &mut BattleState<Self>,
1751            who: BattlerRef,
1752            _action: &BattleAction<Self>,
1753            _rng: &mut dyn BattleRng,
1754        ) -> MoveGate<Self> {
1755            let party = if who.side == 0 {
1756                &state.player_battlers
1757            } else {
1758                &state.opponent_battlers
1759            };
1760            if let Some(b) = party.get(who.slot as usize) {
1761                if b.status == Some(DStatus::Sleep) {
1762                    return MoveGate::Prevented(EffectResult::NoEffect);
1763                }
1764            }
1765            MoveGate::Acts
1766        }
1767
1768        fn accuracy_check(
1769            &self,
1770            _state: &BattleState<Self>,
1771            _who: BattlerRef,
1772            _target: BattlerRef,
1773            move_: &Self::Move,
1774            rng: &mut dyn BattleRng,
1775        ) -> bool {
1776            // Hits if the drawn byte is below the move's accuracy.
1777            (rng.next_u8() as u32) < move_.accuracy as u32
1778        }
1779
1780        fn end_of_turn(
1781            &self,
1782            state: &mut BattleState<Self>,
1783            _rng: &mut dyn BattleRng,
1784        ) -> Vec<EffectResult> {
1785            let mut out = Vec::new();
1786            for party in [&mut state.player_battlers, &mut state.opponent_battlers] {
1787                for b in party.iter_mut() {
1788                    if b.hp > 0 {
1789                        b.take_damage(self.residual);
1790                        out.push(EffectResult::DamageDealt {
1791                            amount: self.residual,
1792                        });
1793                    }
1794                }
1795            }
1796            out
1797        }
1798    }
1799
1800    /// A provider that *always* lands a critical hit, drawing **no** rng in
1801    /// `roll_critical`, and doubles damage when told `is_critical`. Used to
1802    /// prove a provider-reported crit reaches both the damage formula and the
1803    /// `Damage` event's `critical` flag.
1804    struct CritProvider;
1805
1806    impl BattleProvider for CritProvider {
1807        type Monster = ();
1808        type Move = DMove;
1809        type Ability = ();
1810        type Status = DStatus;
1811        type Stat = DStat;
1812        type Species = DSpecies;
1813        type Type = DType;
1814        type Item = ();
1815
1816        fn calculate_damage(
1817            &self,
1818            move_: &Self::Move,
1819            _attacker: &BattlerState<Self>,
1820            _defender: &BattlerState<Self>,
1821            _random: u8,
1822            is_critical: bool,
1823        ) -> DamageResult {
1824            let base = move_.power as u16;
1825            DamageResult {
1826                damage: if is_critical { base * 2 } else { base },
1827                effectiveness: 1.0,
1828                is_miss: false,
1829            }
1830        }
1831
1832        fn select_move(
1833            &self,
1834            battler: &BattlerState<Self>,
1835            _state: &BattleState<Self>,
1836        ) -> Self::Move {
1837            battler.moves.first().cloned().unwrap()
1838        }
1839
1840        fn apply_move_effect(
1841            &self,
1842            _effect: MoveEffect,
1843            _user: &mut BattlerState<Self>,
1844            _target: &mut BattlerState<Self>,
1845        ) -> EffectResult {
1846            EffectResult::NoEffect
1847        }
1848
1849        fn create_monster(&self, species: Self::Species, _level: u8) -> BattlerState<Self> {
1850            BattlerState::new(species, 100, 100, EnumMap::new(), Vec::new())
1851        }
1852
1853        /// Always crit; draw no rng so byte accounting stays predictable.
1854        fn roll_critical(
1855            &self,
1856            _state: &BattleState<Self>,
1857            _who: BattlerRef,
1858            _target: BattlerRef,
1859            _move_: &Self::Move,
1860            _rng: &mut dyn BattleRng,
1861        ) -> bool {
1862            true
1863        }
1864    }
1865
1866    fn crit_mon(hp: u16, speed: u16, power: u8) -> BattlerState<CritProvider> {
1867        let mut stats = EnumMap::new();
1868        stats.set(DStat::Speed, speed);
1869        BattlerState::new(
1870            DSpecies::Mon,
1871            hp,
1872            hp,
1873            stats,
1874            vec![DMove { power, accuracy: 255 }],
1875        )
1876    }
1877
1878    fn mon(hp: u16, speed: u16, accuracy: u8, power: u8) -> BattlerState<DProvider> {
1879        let mut stats = EnumMap::new();
1880        stats.set(DStat::Speed, speed);
1881        BattlerState::new(DSpecies::Mon, hp, hp, stats, vec![DMove { power, accuracy }])
1882    }
1883
1884    /// A connecting (accuracy 255) move action of the given power.
1885    fn fight_pow(power: u8) -> BattleAction<DProvider> {
1886        BattleAction::Fight {
1887            move_: DMove {
1888                power,
1889                accuracy: 255,
1890            },
1891        }
1892    }
1893
1894    /// A connecting move action of power 20.
1895    fn fight() -> BattleAction<DProvider> {
1896        fight_pow(20)
1897    }
1898
1899    fn first_mover(out: &TurnOutcome<DProvider>) -> BattlerRef {
1900        out.events
1901            .iter()
1902            .find_map(|e| match e {
1903                TurnEvent::MoveUsed { who, .. } => Some(*who),
1904                _ => None,
1905            })
1906            .unwrap()
1907    }
1908
1909    // ── Tests ────────────────────────────────────────────────────────
1910
1911    #[test]
1912    fn turn_order_faster_battler_acts_first() {
1913        let provider = DProvider { residual: 0 };
1914        // Player slow (speed 10), opponent fast (speed 99). Always-hit moves.
1915        let mut state = BattleState::new(vec![mon(100, 10, 255, 20)], vec![mon(100, 99, 255, 20)]);
1916        // RNG: turn_order_key draws one byte per actor (2), then accuracy + damage
1917        // bytes per fight. Keep accuracy bytes below 255 so moves connect.
1918        let mut rng = ScriptedRng::new(vec![5, 5, 0, 0, 0, 0]);
1919        let out = BattleDriver::execute_turn(&provider, &mut state, [fight(), fight()], &mut rng);
1920        assert_eq!(first_mover(&out), BattlerRef::OPPONENT, "faster opponent acts first");
1921    }
1922
1923    #[test]
1924    fn turn_order_tie_uses_rng_tiebreak() {
1925        let provider = DProvider { residual: 0 };
1926        // Equal speed → tie broken by the rng draw. Player draws 9, opp draws 1,
1927        // so opp's tiebreak is smaller → opp first.
1928        let mut state = BattleState::new(vec![mon(100, 50, 255, 20)], vec![mon(100, 50, 255, 20)]);
1929        let mut rng = ScriptedRng::new(vec![9, 1, 0, 0, 0, 0]);
1930        let out = BattleDriver::execute_turn(&provider, &mut state, [fight(), fight()], &mut rng);
1931        assert_eq!(first_mover(&out), BattlerRef::OPPONENT, "smaller tiebreak acts first");
1932    }
1933
1934    #[test]
1935    fn before_move_gate_skips_asleep_battler() {
1936        let provider = DProvider { residual: 0 };
1937        let mut player = mon(100, 99, 255, 20); // fast so it would act first
1938        player.status = Some(DStatus::Sleep);
1939        let mut state = BattleState::new(vec![player], vec![mon(100, 10, 255, 20)]);
1940        let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
1941        let out = BattleDriver::execute_turn(&provider, &mut state, [fight(), fight()], &mut rng);
1942
1943        assert!(
1944            out.events.iter().any(|e| matches!(
1945                e,
1946                TurnEvent::ActionPrevented { who, .. } if *who == BattlerRef::PLAYER
1947            )),
1948            "asleep player should be prevented"
1949        );
1950        assert!(
1951            out.events.iter().any(|e| matches!(
1952                e,
1953                TurnEvent::MoveUsed { who, .. } if *who == BattlerRef::OPPONENT
1954            )),
1955            "opponent should still act"
1956        );
1957    }
1958
1959    #[test]
1960    fn move_execution_applies_damage_via_hook() {
1961        let provider = DProvider { residual: 0 };
1962        // The driver uses the *action's* move (power 30), passed to the
1963        // provider's calculate_damage hook.
1964        let mut state = BattleState::new(
1965            vec![mon(100, 99, 255, 0)], // player fast
1966            vec![mon(100, 10, 255, 0)],
1967        );
1968        let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
1969        let _ = BattleDriver::execute_turn(
1970            &provider,
1971            &mut state,
1972            [fight_pow(30), fight_pow(30)],
1973            &mut rng,
1974        );
1975        // Opponent took 30 (player's move); player took 30 (opponent's move).
1976        assert_eq!(state.opponent_battlers[0].hp, 70);
1977        assert_eq!(state.player_battlers[0].hp, 70);
1978    }
1979
1980    #[test]
1981    fn accuracy_check_can_miss() {
1982        let provider = DProvider { residual: 0 };
1983        let mut state = BattleState::new(vec![mon(100, 99, 255, 20)], vec![mon(100, 10, 255, 20)]);
1984        // Player's move has accuracy 0 → always misses; opponent's connects.
1985        let player_fight = BattleAction::Fight {
1986            move_: DMove {
1987                power: 20,
1988                accuracy: 0,
1989            },
1990        };
1991        let opp_fight = BattleAction::Fight {
1992            move_: DMove {
1993                power: 20,
1994                accuracy: 255,
1995            },
1996        };
1997        let mut rng = ScriptedRng::new(vec![0, 0, 100, 0, 100, 0]);
1998        let out =
1999            BattleDriver::execute_turn(&provider, &mut state, [player_fight, opp_fight], &mut rng);
2000        // Opponent untouched (player missed); player took 20.
2001        assert_eq!(state.opponent_battlers[0].hp, 100);
2002        assert_eq!(state.player_battlers[0].hp, 80);
2003        assert!(out.events.iter().any(|e| matches!(
2004            e,
2005            TurnEvent::Missed { who, .. } if *who == BattlerRef::PLAYER
2006        )));
2007    }
2008
2009    #[test]
2010    fn end_of_turn_residual_ticks() {
2011        let provider = DProvider { residual: 5 };
2012        let mut state = BattleState::new(vec![mon(100, 50, 255, 0)], vec![mon(100, 50, 255, 0)]);
2013        // Power-0 moves so only the residual changes HP.
2014        let mut rng = ScriptedRng::new(vec![1, 2, 0, 0, 0, 0]);
2015        let out =
2016            BattleDriver::execute_turn(&provider, &mut state, [fight_pow(0), fight_pow(0)], &mut rng);
2017        assert_eq!(state.player_battlers[0].hp, 95);
2018        assert_eq!(state.opponent_battlers[0].hp, 95);
2019        assert_eq!(
2020            out.events
2021                .iter()
2022                .filter(|e| matches!(e, TurnEvent::Residual { .. }))
2023                .count(),
2024            2
2025        );
2026    }
2027
2028    #[test]
2029    fn faint_leads_to_battle_end_player_win() {
2030        let provider = DProvider { residual: 0 };
2031        // Player fast; its power-200 move faints the opponent (100 hp) → PlayerWin.
2032        let mut state = BattleState::new(vec![mon(100, 99, 255, 0)], vec![mon(100, 10, 255, 0)]);
2033        let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
2034        let out =
2035            BattleDriver::execute_turn(&provider, &mut state, [fight_pow(200), fight()], &mut rng);
2036        assert_eq!(state.opponent_battlers[0].hp, 0);
2037        assert_eq!(out.battle_over, Some(BattleEnd::PlayerWin));
2038        assert!(out
2039            .events
2040            .iter()
2041            .any(|e| matches!(e, TurnEvent::Faint { who } if *who == BattlerRef::OPPONENT)));
2042        // Opponent should NOT have acted (battle short-circuited after faint).
2043        assert!(!out.events.iter().any(|e| matches!(
2044            e,
2045            TurnEvent::MoveUsed { who, .. } if *who == BattlerRef::OPPONENT
2046        )));
2047    }
2048
2049    #[test]
2050    fn faint_leads_to_battle_end_player_loss() {
2051        let provider = DProvider { residual: 0 };
2052        // Opponent fast; its power-200 move faints the player first → PlayerLoss.
2053        let mut state = BattleState::new(vec![mon(100, 10, 255, 0)], vec![mon(100, 99, 255, 0)]);
2054        let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
2055        let out =
2056            BattleDriver::execute_turn(&provider, &mut state, [fight(), fight_pow(200)], &mut rng);
2057        assert_eq!(state.player_battlers[0].hp, 0);
2058        assert_eq!(out.battle_over, Some(BattleEnd::PlayerLoss));
2059    }
2060
2061    #[test]
2062    fn switch_swaps_active_slot() {
2063        let provider = DProvider { residual: 0 };
2064        let mut state = BattleState::new(
2065            vec![mon(100, 50, 255, 20), mon(80, 50, 255, 20)],
2066            vec![mon(100, 50, 255, 20)],
2067        );
2068        let switch = BattleAction::Switch { to_slot: 1 };
2069        // Equal speed: tie keeps submission order, so player's switch resolves
2070        // first, then opponent fights the newly-active slot.
2071        let mut rng = ScriptedRng::new(vec![5, 5, 0, 0]);
2072        let _ = BattleDriver::execute_turn(&provider, &mut state, [switch, fight()], &mut rng);
2073        // Slot 0 is now the former slot-1 mon (80 max hp), which took 20 from the
2074        // opponent's fight → 60.
2075        assert_eq!(state.player_battlers[0].max_hp, 80);
2076        assert_eq!(state.player_battlers[0].hp, 60);
2077    }
2078
2079    #[test]
2080    fn scripted_rng_draw_order_is_deterministic() {
2081        // Two identical runs with the same script produce identical outcomes,
2082        // proving the draw order is stable / game-controlled.
2083        let run = || {
2084            let provider = DProvider { residual: 3 };
2085            let mut state =
2086                BattleState::new(vec![mon(100, 50, 200, 15)], vec![mon(100, 50, 200, 15)]);
2087            let mut rng = ScriptedRng::new(vec![7, 2, 10, 0, 10, 0]);
2088            let _ = BattleDriver::execute_turn(&provider, &mut state, [fight(), fight()], &mut rng);
2089            (
2090                state.player_battlers[0].hp,
2091                state.opponent_battlers[0].hp,
2092                rng.consumed(),
2093            )
2094        };
2095        assert_eq!(run(), run());
2096    }
2097
2098    #[test]
2099    fn provider_critical_hit_surfaces_into_damage_event() {
2100        // `CritProvider::roll_critical` always crits (drawing no rng) and its
2101        // `calculate_damage` doubles damage on a crit. We assert the crit both
2102        // (a) flows into the formula (doubled damage / hp) and (b) surfaces as
2103        // `critical: true` on the player's `Damage` event.
2104        let provider = CritProvider;
2105        // Player fast (acts first), power-10 move → 20 damage on a crit.
2106        let mut state =
2107            BattleState::new(vec![crit_mon(100, 99, 10)], vec![crit_mon(100, 10, 10)]);
2108        // Default turn_order/accuracy/roll_critical draw no rng; only the two
2109        // `calculate_damage` random bytes are consumed.
2110        let mut rng = ScriptedRng::new(vec![0, 0]);
2111        let crit_fight = || BattleAction::<CritProvider>::Fight {
2112            move_: DMove {
2113                power: 10,
2114                accuracy: 255,
2115            },
2116        };
2117        let out = BattleDriver::execute_turn(
2118            &provider,
2119            &mut state,
2120            [crit_fight(), crit_fight()],
2121            &mut rng,
2122        );
2123
2124        // (a) Crit doubled the damage in the formula.
2125        assert_eq!(state.opponent_battlers[0].hp, 80, "crit doubled 10 → 20 dmg");
2126
2127        // (b) The player's Damage event reports the crit.
2128        let player_dmg = out
2129            .events
2130            .iter()
2131            .find_map(|e| match e {
2132                TurnEvent::Damage {
2133                    who,
2134                    critical,
2135                    amount,
2136                    ..
2137                } if *who == BattlerRef::PLAYER => Some((*critical, *amount)),
2138                _ => None,
2139            })
2140            .expect("player should have a Damage event");
2141        assert!(player_dmg.0, "provider crit must surface as critical: true");
2142        assert_eq!(player_dmg.1, 20, "Damage event amount reflects the crit");
2143    }
2144
2145    #[test]
2146    fn default_roll_critical_yields_non_critical_damage_event() {
2147        // `DProvider` does not override `roll_critical`, so the default (never
2148        // crit, no rng) keeps `critical: false` on the Damage event.
2149        let provider = DProvider { residual: 0 };
2150        let mut state = BattleState::new(vec![mon(100, 99, 255, 30)], vec![mon(100, 10, 255, 0)]);
2151        let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
2152        let out = BattleDriver::execute_turn(
2153            &provider,
2154            &mut state,
2155            [fight_pow(30), fight_pow(0)],
2156            &mut rng,
2157        );
2158        assert!(out.events.iter().any(|e| matches!(
2159            e,
2160            TurnEvent::Damage { who, critical: false, .. } if *who == BattlerRef::PLAYER
2161        )));
2162    }
2163}