1use 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
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_ } => {
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
476pub enum MoveGate<P: BattleProvider + ?Sized> {
483 Acts,
485 Prevented(EffectResult),
488 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
504pub trait BattleProvider {
527 type Monster: Clone + fmt::Debug;
529 type Move: Clone + fmt::Debug;
531 type Ability: Clone + fmt::Debug;
533 type Status: Clone + PartialEq + fmt::Debug;
535 type Stat: Copy + PartialEq + fmt::Debug;
537 type Species: Clone + fmt::Debug;
539 type Type: Clone + PartialEq + fmt::Debug;
541 type Item: Clone + fmt::Debug;
543
544 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 fn select_move(
560 &self,
561 battler: &BattlerState<Self>,
562 state: &BattleState<Self>,
563 ) -> Self::Move;
564
565 fn apply_move_effect(
568 &self,
569 effect: MoveEffect,
570 user: &mut BattlerState<Self>,
571 target: &mut BattlerState<Self>,
572 ) -> EffectResult;
573
574 fn create_monster(&self, species: Self::Species, level: u8) -> BattlerState<Self>;
579
580 fn check_faint(&self, battler: &BattlerState<Self>) -> bool {
582 battler.hp == 0
583 }
584
585 fn move_cost(&self, _move_: &Self::Move) -> &[(u16, u16)] {
607 &[]
608 }
609
610 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 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 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 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 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
715pub struct BattlerState<P: BattleProvider + ?Sized> {
723 pub species: P::Species,
725 pub hp: u16,
727 pub max_hp: u16,
729 pub level: u8,
735 pub stats: EnumMap<P::Stat, u16>,
737 pub stat_stages: EnumMap<P::Stat, i8>,
739 pub status: Option<P::Status>,
741 pub moves: Vec<P::Move>,
743 pub resources: ResourcePool,
749}
750
751impl<P: BattleProvider + ?Sized> BattlerState<P> {
752 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 pub fn with_level(mut self, level: u8) -> Self {
775 self.level = level;
776 self
777 }
778
779 pub fn take_damage(&mut self, amount: u16) {
781 self.hp = self.hp.saturating_sub(amount);
782 }
783
784 pub fn heal(&mut self, amount: u16) {
786 self.hp = self.hp.saturating_add(amount).min(self.max_hp);
787 }
788
789 pub fn with_resource(mut self, id: u16, max: u16) -> Self {
793 self.resources.set(id, max, max);
794 self
795 }
796
797 pub fn can_pay_resource(&self, id: u16, amount: u16) -> bool {
801 self.resources.can_pay(id, amount)
802 }
803
804 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
841pub struct BattleState<P: BattleProvider + ?Sized> {
849 pub player_battlers: Vec<BattlerState<P>>,
851 pub opponent_battlers: Vec<BattlerState<P>>,
853 pub turn_order: Vec<usize>,
855 pub weather: Weather,
857 pub terrain: Terrain,
859 pub turn_count: u32,
861}
862
863impl<P: BattleProvider + ?Sized> BattleState<P> {
864 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 pub fn active_player(&self) -> Option<&BattlerState<P>> {
881 self.player_battlers.first()
882 }
883
884 pub fn active_player_mut(&mut self) -> Option<&mut BattlerState<P>> {
886 self.player_battlers.first_mut()
887 }
888
889 pub fn active_opponent(&self) -> Option<&BattlerState<P>> {
891 self.opponent_battlers.first()
892 }
893
894 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
926pub trait TypeChart {
934 type Type: PartialEq + fmt::Debug;
936
937 fn effectiveness(attacking: &Self::Type, defending: &[Self::Type]) -> f32;
946}
947
948pub trait BattleAI<P: BattleProvider + ?Sized> {
955 fn select_move(&self, battler: &BattlerState<P>, state: &BattleState<P>) -> P::Move;
957
958 fn should_switch(&self, battler: &BattlerState<P>) -> bool;
960
961 fn should_use_item(&self, battler: &BattlerState<P>) -> Option<P::Item>;
963}
964
965pub trait EffectHandler<P: BattleProvider + ?Sized> {
973 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#[cfg(test)]
991mod tests {
992 use super::*;
993
994 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
999 enum MockType {
1000 TypeA,
1001 TypeB,
1002 TypeC,
1003 }
1004
1005 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1007 enum MockStat {
1008 Hp,
1009 Attack,
1010 Defense,
1011 }
1012
1013 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1015 #[allow(dead_code)]
1016 enum MockStatus {
1017 Poison,
1018 Burn,
1019 Sleep,
1020 }
1021
1022 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1024 #[allow(dead_code)]
1025 enum MockSpecies {
1026 Alpha,
1027 Beta,
1028 Gamma,
1029 }
1030
1031 #[derive(Debug, Clone, PartialEq)]
1033 struct MockMove {
1034 name: String,
1035 power: u8,
1036 move_type: MockType,
1037 accuracy: u8,
1038 }
1039
1040 #[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 struct MockTypeChart;
1053
1054 impl TypeChart for MockTypeChart {
1055 type Type = MockType;
1056
1057 fn effectiveness(attacking: &Self::Type, defending: &[Self::Type]) -> f32 {
1058 let def = defending.first().copied().unwrap_or(MockType::TypeA);
1060 match (attacking, def) {
1061 (MockType::TypeA, MockType::TypeB) => 2.0,
1063 (MockType::TypeB, MockType::TypeC) => 2.0,
1065 (MockType::TypeC, MockType::TypeA) => 2.0,
1067 (MockType::TypeA, MockType::TypeC) => 0.5,
1069 (MockType::TypeB, MockType::TypeA) => 0.5,
1070 (MockType::TypeC, MockType::TypeB) => 0.5,
1071 (a, d) if *a == d => 0.5,
1073 _ => 1.0,
1075 }
1076 }
1077 }
1078
1079 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![]; 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 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 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 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 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 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 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 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 #[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 #[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 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); }
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 #[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 #[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); }
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 #[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 #[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 #[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 #[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 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); 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); 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); 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); assert_eq!(pool.current(0), Some(20), "current clamped to max");
1558 }
1559
1560 #[test]
1561 fn battler_state_resources_default_empty() {
1562 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 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 #[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 #[test]
1592 fn move_effect_variants_are_available() {
1593 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#[cfg(test)]
1636mod driver_tests {
1637 use super::driver::{BattleDriver, BattleEnd, TurnEvent};
1638 use super::rng::{BattleRng, ScriptedRng};
1639 use super::*;
1640
1641 #[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 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 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 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 (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 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 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 fn fight_pow(power: u8) -> BattleAction<DProvider> {
1886 BattleAction::Fight {
1887 move_: DMove {
1888 power,
1889 accuracy: 255,
1890 },
1891 }
1892 }
1893
1894 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 #[test]
1912 fn turn_order_faster_battler_acts_first() {
1913 let provider = DProvider { residual: 0 };
1914 let mut state = BattleState::new(vec![mon(100, 10, 255, 20)], vec![mon(100, 99, 255, 20)]);
1916 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 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); 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 let mut state = BattleState::new(
1965 vec![mon(100, 99, 255, 0)], 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 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 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 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 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 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 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 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 let mut rng = ScriptedRng::new(vec![5, 5, 0, 0]);
2072 let _ = BattleDriver::execute_turn(&provider, &mut state, [switch, fight()], &mut rng);
2073 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 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 let provider = CritProvider;
2105 let mut state =
2107 BattleState::new(vec![crit_mon(100, 99, 10)], vec![crit_mon(100, 10, 10)]);
2108 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 assert_eq!(state.opponent_battlers[0].hp, 80, "crit doubled 10 → 20 dmg");
2126
2127 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 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}