1use 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
36pub struct EnumMap<K, V> {
48 entries: Vec<(K, V)>,
49}
50
51impl<K: PartialEq, V> EnumMap<K, V> {
52 pub fn new() -> Self {
54 Self {
55 entries: Vec::new(),
56 }
57 }
58
59 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 pub fn get(&self, key: K) -> Option<&V> {
70 self.entries.iter().find(|(k, _)| *k == key).map(|(_, v)| v)
71 }
72
73 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 pub fn len(&self) -> usize {
83 self.entries.len()
84 }
85
86 pub fn is_empty(&self) -> bool {
88 self.entries.is_empty()
89 }
90
91 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#[derive(Default)]
144pub struct ResourcePool {
145 entries: Vec<(u16, u16, u16)>,
147}
148
149impl ResourcePool {
150 pub fn new() -> Self {
152 Self {
153 entries: Vec::new(),
154 }
155 }
156
157 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 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 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 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 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 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 pub fn len(&self) -> usize {
221 self.entries.len()
222 }
223
224 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#[derive(Debug, Clone, Copy, PartialEq)]
250pub struct DamageResult {
251 pub damage: u16,
253 pub effectiveness: f32,
255 pub is_miss: bool,
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum MoveEffect {
268 Damage,
270 Heal,
272 StatusCondition,
274 StatChange,
276 MultiHit,
278 Recharge,
280 DrainHp,
282 Recoil,
284 Flinch,
286 FieldEffect,
288 SpecialDamage,
290 Ohko,
292 MultiTurn,
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub enum EffectResult {
301 NoEffect,
303 DamageDealt { amount: u16 },
305 Healed { amount: u16 },
307 StatusInflicted,
309 StatusFailed,
311 StatModified { stages: i8 },
313 StatBlocked,
315 HpDrained { drained: u16 },
317 RecoilDamage { recoil: u16 },
319 Fainted,
321 Miss,
323 CriticalHit,
325 MultiHit { hits: u8 },
327 MustRecharge,
329 FieldEffectSet,
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
337pub enum Weather {
338 #[default]
340 Clear,
341 Rain,
343 Sun,
345 Sandstorm,
347 Snow,
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
353pub enum Terrain {
354 #[default]
356 Normal,
357 Electric,
359 Grassy,
361 Misty,
363 Psychic,
365}
366
367#[derive(Clone, Copy, Debug, PartialEq, Eq)]
376pub struct BattlerRef {
377 pub side: u8,
379 pub slot: u8,
381}
382
383impl BattlerRef {
384 pub const fn new(side: u8, slot: u8) -> Self {
386 Self { side, slot }
387 }
388
389 pub const PLAYER: Self = Self { side: 0, slot: 0 };
391 pub const OPPONENT: Self = Self { side: 1, slot: 0 };
393}
394
395#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
414pub struct OrderKey(pub i32, pub i32, pub u32);
415
416pub enum BattleAction<P: BattleProvider + ?Sized> {
423 Fight {
425 move_: P::Move,
427 },
428 Switch {
430 to_slot: usize,
432 },
433 UseItem {
435 item: P::Item,
437 },
438 Run,
440 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
474pub enum MoveGate<P: BattleProvider + ?Sized> {
481 Acts,
483 Prevented(EffectResult),
486 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
502pub trait BattleProvider {
525 type Monster: Clone + fmt::Debug;
527 type Move: Clone + fmt::Debug;
529 type Ability: Clone + fmt::Debug;
531 type Status: Clone + PartialEq + fmt::Debug;
533 type Stat: Copy + PartialEq + fmt::Debug;
535 type Species: Clone + fmt::Debug;
537 type Type: Clone + PartialEq + fmt::Debug;
539 type Item: Clone + fmt::Debug;
541
542 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 fn select_move(&self, battler: &BattlerState<Self>, state: &BattleState<Self>) -> Self::Move;
558
559 fn apply_move_effect(
562 &self,
563 effect: MoveEffect,
564 user: &mut BattlerState<Self>,
565 target: &mut BattlerState<Self>,
566 ) -> EffectResult;
567
568 fn create_monster(&self, species: Self::Species, level: u8) -> BattlerState<Self>;
573
574 fn check_faint(&self, battler: &BattlerState<Self>) -> bool {
576 battler.hp == 0
577 }
578
579 fn move_cost(&self, _move_: &Self::Move) -> &[(u16, u16)] {
601 &[]
602 }
603
604 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 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 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 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 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
709pub struct BattlerState<P: BattleProvider + ?Sized> {
717 pub species: P::Species,
719 pub hp: u16,
721 pub max_hp: u16,
723 pub level: u8,
729 pub stats: EnumMap<P::Stat, u16>,
731 pub stat_stages: EnumMap<P::Stat, i8>,
733 pub status: Option<P::Status>,
735 pub moves: Vec<P::Move>,
737 pub resources: ResourcePool,
743}
744
745impl<P: BattleProvider + ?Sized> BattlerState<P> {
746 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 pub fn with_level(mut self, level: u8) -> Self {
769 self.level = level;
770 self
771 }
772
773 pub fn take_damage(&mut self, amount: u16) {
775 self.hp = self.hp.saturating_sub(amount);
776 }
777
778 pub fn heal(&mut self, amount: u16) {
780 self.hp = self.hp.saturating_add(amount).min(self.max_hp);
781 }
782
783 pub fn with_resource(mut self, id: u16, max: u16) -> Self {
787 self.resources.set(id, max, max);
788 self
789 }
790
791 pub fn can_pay_resource(&self, id: u16, amount: u16) -> bool {
795 self.resources.can_pay(id, amount)
796 }
797
798 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
835pub struct BattleState<P: BattleProvider + ?Sized> {
843 pub player_battlers: Vec<BattlerState<P>>,
845 pub opponent_battlers: Vec<BattlerState<P>>,
847 pub turn_order: Vec<usize>,
849 pub weather: Weather,
851 pub terrain: Terrain,
853 pub turn_count: u32,
855}
856
857impl<P: BattleProvider + ?Sized> BattleState<P> {
858 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 pub fn active_player(&self) -> Option<&BattlerState<P>> {
875 self.player_battlers.first()
876 }
877
878 pub fn active_player_mut(&mut self) -> Option<&mut BattlerState<P>> {
880 self.player_battlers.first_mut()
881 }
882
883 pub fn active_opponent(&self) -> Option<&BattlerState<P>> {
885 self.opponent_battlers.first()
886 }
887
888 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
920pub trait TypeChart {
928 type Type: PartialEq + fmt::Debug;
930
931 fn effectiveness(attacking: &Self::Type, defending: &[Self::Type]) -> f32;
940}
941
942pub trait BattleAI<P: BattleProvider + ?Sized> {
949 fn select_move(&self, battler: &BattlerState<P>, state: &BattleState<P>) -> P::Move;
951
952 fn should_switch(&self, battler: &BattlerState<P>) -> bool;
954
955 fn should_use_item(&self, battler: &BattlerState<P>) -> Option<P::Item>;
957}
958
959pub trait EffectHandler<P: BattleProvider + ?Sized> {
967 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#[cfg(test)]
985mod tests {
986 use super::*;
987
988 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
993 enum MockType {
994 TypeA,
995 TypeB,
996 TypeC,
997 }
998
999 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1001 enum MockStat {
1002 Hp,
1003 Attack,
1004 Defense,
1005 }
1006
1007 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1009 #[allow(dead_code)]
1010 enum MockStatus {
1011 Poison,
1012 Burn,
1013 Sleep,
1014 }
1015
1016 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1018 #[allow(dead_code)]
1019 enum MockSpecies {
1020 Alpha,
1021 Beta,
1022 Gamma,
1023 }
1024
1025 #[derive(Debug, Clone, PartialEq)]
1027 struct MockMove {
1028 name: String,
1029 power: u8,
1030 move_type: MockType,
1031 accuracy: u8,
1032 }
1033
1034 #[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 struct MockTypeChart;
1047
1048 impl TypeChart for MockTypeChart {
1049 type Type = MockType;
1050
1051 fn effectiveness(attacking: &Self::Type, defending: &[Self::Type]) -> f32 {
1052 let def = defending.first().copied().unwrap_or(MockType::TypeA);
1054 match (attacking, def) {
1055 (MockType::TypeA, MockType::TypeB) => 2.0,
1057 (MockType::TypeB, MockType::TypeC) => 2.0,
1059 (MockType::TypeC, MockType::TypeA) => 2.0,
1061 (MockType::TypeA, MockType::TypeC) => 0.5,
1063 (MockType::TypeB, MockType::TypeA) => 0.5,
1064 (MockType::TypeC, MockType::TypeB) => 0.5,
1065 (a, d) if *a == d => 0.5,
1067 _ => 1.0,
1069 }
1070 }
1071 }
1072
1073 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![]; 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 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 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 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 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 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 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 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 #[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 #[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 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); }
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 #[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 #[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); }
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 #[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 #[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 #[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 #[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 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); 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); 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); 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); assert_eq!(pool.current(0), Some(20), "current clamped to max");
1577 }
1578
1579 #[test]
1580 fn battler_state_resources_default_empty() {
1581 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 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 #[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 #[test]
1617 fn move_effect_variants_are_available() {
1618 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#[cfg(test)]
1661mod driver_tests {
1662 use super::driver::{BattleDriver, BattleEnd, TurnEvent};
1663 use super::rng::{BattleRng, ScriptedRng};
1664 use super::*;
1665
1666 #[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 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 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 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 (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 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 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 fn fight_pow(power: u8) -> BattleAction<DProvider> {
1920 BattleAction::Fight {
1921 move_: DMove {
1922 power,
1923 accuracy: 255,
1924 },
1925 }
1926 }
1927
1928 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 #[test]
1946 fn turn_order_faster_battler_acts_first() {
1947 let provider = DProvider { residual: 0 };
1948 let mut state = BattleState::new(vec![mon(100, 10, 255, 20)], vec![mon(100, 99, 255, 20)]);
1950 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 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); 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 let mut state = BattleState::new(
2007 vec![mon(100, 99, 255, 0)], 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 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 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 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 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 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 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 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 let mut rng = ScriptedRng::new(vec![5, 5, 0, 0]);
2118 let _ = BattleDriver::execute_turn(&provider, &mut state, [switch, fight()], &mut rng);
2119 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 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 let provider = CritProvider;
2151 let mut state = BattleState::new(vec![crit_mon(100, 99, 10)], vec![crit_mon(100, 10, 10)]);
2153 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 assert_eq!(
2171 state.opponent_battlers[0].hp, 80,
2172 "crit doubled 10 → 20 dmg"
2173 );
2174
2175 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 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}