use std::fmt;
pub mod ai;
pub mod driver;
pub mod rng;
pub mod stack;
pub use ai::{BattleAi, BattleAiProvider};
pub use driver::{BattleDriver, BattleEnd, TurnEvent, TurnOutcome};
pub use rng::BattleRng;
pub struct EnumMap<K, V> {
entries: Vec<(K, V)>,
}
impl<K: PartialEq, V> EnumMap<K, V> {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn set(&mut self, key: K, value: V) {
if let Some(entry) = self.entries.iter_mut().find(|(k, _)| *k == key) {
entry.1 = value;
} else {
self.entries.push((key, value));
}
}
pub fn get(&self, key: K) -> Option<&V> {
self.entries.iter().find(|(k, _)| *k == key).map(|(_, v)| v)
}
pub fn get_mut(&mut self, key: K) -> Option<&mut V> {
self.entries
.iter_mut()
.find(|(k, _)| *k == key)
.map(|(_, v)| v)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
self.entries.iter().map(|(k, v)| (k, v))
}
}
impl<K: PartialEq, V> Default for EnumMap<K, V> {
fn default() -> Self {
Self::new()
}
}
impl<K: Clone + PartialEq, V: Clone> Clone for EnumMap<K, V> {
fn clone(&self) -> Self {
Self {
entries: self.entries.clone(),
}
}
}
impl<K: fmt::Debug + PartialEq, V: fmt::Debug> fmt::Debug for EnumMap<K, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_map()
.entries(self.entries.iter().map(|(k, v)| (k, v)))
.finish()
}
}
#[derive(Default)]
pub struct ResourcePool {
entries: Vec<(u16, u16, u16)>,
}
impl ResourcePool {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn set(&mut self, id: u16, current: u16, max: u16) {
let current = current.min(max);
if let Some(e) = self.entries.iter_mut().find(|(k, _, _)| *k == id) {
e.1 = current;
e.2 = max;
} else {
self.entries.push((id, current, max));
}
}
pub fn current(&self, id: u16) -> Option<u16> {
self.entries
.iter()
.find(|(k, _, _)| *k == id)
.map(|(_, cur, _)| *cur)
}
pub fn max(&self, id: u16) -> Option<u16> {
self.entries
.iter()
.find(|(k, _, _)| *k == id)
.map(|(_, _, m)| *m)
}
pub fn can_pay(&self, id: u16, amount: u16) -> bool {
if amount == 0 {
return true;
}
self.current(id).map(|cur| cur >= amount).unwrap_or(false)
}
pub fn pay(&mut self, id: u16, amount: u16) -> bool {
if amount == 0 {
return false;
}
if let Some(e) = self.entries.iter_mut().find(|(k, _, _)| *k == id) {
e.1 = e.1.saturating_sub(amount);
true
} else {
false
}
}
pub fn restore(&mut self, id: u16, amount: u16) {
if let Some(e) = self.entries.iter_mut().find(|(k, _, _)| *k == id) {
e.1 = e.1.saturating_add(amount).min(e.2);
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl Clone for ResourcePool {
fn clone(&self) -> Self {
Self {
entries: self.entries.clone(),
}
}
}
impl fmt::Debug for ResourcePool {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_map()
.entries(self.entries.iter().map(|(k, cur, max)| (k, (cur, max))))
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DamageResult {
pub damage: u16,
pub effectiveness: f32,
pub is_miss: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MoveEffect {
Damage,
Heal,
StatusCondition,
StatChange,
MultiHit,
Recharge,
DrainHp,
Recoil,
Flinch,
FieldEffect,
SpecialDamage,
Ohko,
MultiTurn,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EffectResult {
NoEffect,
DamageDealt { amount: u16 },
Healed { amount: u16 },
StatusInflicted,
StatusFailed,
StatModified { stages: i8 },
StatBlocked,
HpDrained { drained: u16 },
RecoilDamage { recoil: u16 },
Fainted,
Miss,
CriticalHit,
MultiHit { hits: u8 },
MustRecharge,
FieldEffectSet,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Weather {
#[default]
Clear,
Rain,
Sun,
Sandstorm,
Snow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Terrain {
#[default]
Normal,
Electric,
Grassy,
Misty,
Psychic,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BattlerRef {
pub side: u8,
pub slot: u8,
}
impl BattlerRef {
pub const fn new(side: u8, slot: u8) -> Self {
Self { side, slot }
}
pub const PLAYER: Self = Self { side: 0, slot: 0 };
pub const OPPONENT: Self = Self { side: 1, slot: 0 };
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct OrderKey(pub i32, pub i32, pub u32);
pub enum BattleAction<P: BattleProvider + ?Sized> {
Fight {
move_: P::Move,
},
Switch {
to_slot: usize,
},
UseItem {
item: P::Item,
},
Run,
Nothing,
}
impl<P: BattleProvider + ?Sized> Clone for BattleAction<P> {
fn clone(&self) -> Self {
match self {
BattleAction::Fight { move_ } => BattleAction::Fight {
move_: move_.clone(),
},
BattleAction::Switch { to_slot } => BattleAction::Switch { to_slot: *to_slot },
BattleAction::UseItem { item } => BattleAction::UseItem { item: item.clone() },
BattleAction::Run => BattleAction::Run,
BattleAction::Nothing => BattleAction::Nothing,
}
}
}
impl<P: BattleProvider + ?Sized> fmt::Debug for BattleAction<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BattleAction::Fight { move_ } => {
f.debug_struct("Fight").field("move_", move_).finish()
}
BattleAction::Switch { to_slot } => {
f.debug_struct("Switch").field("to_slot", to_slot).finish()
}
BattleAction::UseItem { item } => {
f.debug_struct("UseItem").field("item", item).finish()
}
BattleAction::Run => write!(f, "Run"),
BattleAction::Nothing => write!(f, "Nothing"),
}
}
}
pub enum MoveGate<P: BattleProvider + ?Sized> {
Acts,
Prevented(EffectResult),
ForcedAction(BattleAction<P>),
}
impl<P: BattleProvider + ?Sized> fmt::Debug for MoveGate<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MoveGate::Acts => write!(f, "Acts"),
MoveGate::Prevented(r) => f.debug_tuple("Prevented").field(r).finish(),
MoveGate::ForcedAction(a) => f.debug_tuple("ForcedAction").field(a).finish(),
}
}
}
pub trait BattleProvider {
type Monster: Clone + fmt::Debug;
type Move: Clone + fmt::Debug;
type Ability: Clone + fmt::Debug;
type Status: Clone + PartialEq + fmt::Debug;
type Stat: Copy + PartialEq + fmt::Debug;
type Species: Clone + fmt::Debug;
type Type: Clone + PartialEq + fmt::Debug;
type Item: Clone + fmt::Debug;
fn calculate_damage(
&self,
move_: &Self::Move,
attacker: &BattlerState<Self>,
defender: &BattlerState<Self>,
random: u8,
is_critical: bool,
) -> DamageResult;
fn select_move(
&self,
battler: &BattlerState<Self>,
state: &BattleState<Self>,
) -> Self::Move;
fn apply_move_effect(
&self,
effect: MoveEffect,
user: &mut BattlerState<Self>,
target: &mut BattlerState<Self>,
) -> EffectResult;
fn create_monster(&self, species: Self::Species, level: u8) -> BattlerState<Self>;
fn check_faint(&self, battler: &BattlerState<Self>) -> bool {
battler.hp == 0
}
fn move_cost(&self, _move_: &Self::Move) -> &[(u16, u16)] {
&[]
}
fn turn_order_key(
&self,
_state: &BattleState<Self>,
_who: BattlerRef,
_action: &BattleAction<Self>,
_rng: &mut dyn BattleRng,
) -> OrderKey
where
Self: Sized,
{
OrderKey::default()
}
fn before_move(
&self,
_state: &mut BattleState<Self>,
_who: BattlerRef,
_action: &BattleAction<Self>,
_rng: &mut dyn BattleRng,
) -> MoveGate<Self>
where
Self: Sized,
{
MoveGate::Acts
}
fn accuracy_check(
&self,
_state: &BattleState<Self>,
_who: BattlerRef,
_target: BattlerRef,
_move_: &Self::Move,
_rng: &mut dyn BattleRng,
) -> bool
where
Self: Sized,
{
true
}
fn end_of_turn(
&self,
_state: &mut BattleState<Self>,
_rng: &mut dyn BattleRng,
) -> Vec<EffectResult>
where
Self: Sized,
{
Vec::new()
}
fn roll_critical(
&self,
_state: &BattleState<Self>,
_who: BattlerRef,
_target: BattlerRef,
_move_: &Self::Move,
_rng: &mut dyn BattleRng,
) -> bool
where
Self: Sized,
{
false
}
}
pub struct BattlerState<P: BattleProvider + ?Sized> {
pub species: P::Species,
pub hp: u16,
pub max_hp: u16,
pub level: u8,
pub stats: EnumMap<P::Stat, u16>,
pub stat_stages: EnumMap<P::Stat, i8>,
pub status: Option<P::Status>,
pub moves: Vec<P::Move>,
pub resources: ResourcePool,
}
impl<P: BattleProvider + ?Sized> BattlerState<P> {
pub fn new(
species: P::Species,
hp: u16,
max_hp: u16,
stats: EnumMap<P::Stat, u16>,
moves: Vec<P::Move>,
) -> Self {
Self {
species,
hp,
max_hp,
level: 50,
stats,
stat_stages: EnumMap::default(),
status: None,
moves,
resources: ResourcePool::new(),
}
}
pub fn with_level(mut self, level: u8) -> Self {
self.level = level;
self
}
pub fn take_damage(&mut self, amount: u16) {
self.hp = self.hp.saturating_sub(amount);
}
pub fn heal(&mut self, amount: u16) {
self.hp = self.hp.saturating_add(amount).min(self.max_hp);
}
pub fn with_resource(mut self, id: u16, max: u16) -> Self {
self.resources.set(id, max, max);
self
}
pub fn can_pay_resource(&self, id: u16, amount: u16) -> bool {
self.resources.can_pay(id, amount)
}
pub fn pay_resource(&mut self, id: u16, amount: u16) -> bool {
self.resources.pay(id, amount)
}
}
impl<P: BattleProvider + ?Sized> fmt::Debug for BattlerState<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BattlerState")
.field("species", &self.species)
.field("hp", &self.hp)
.field("max_hp", &self.max_hp)
.field("stats", &self.stats)
.field("stat_stages", &self.stat_stages)
.field("status", &self.status)
.field("moves", &self.moves)
.field("resources", &self.resources)
.finish()
}
}
impl<P: BattleProvider + ?Sized> Clone for BattlerState<P> {
fn clone(&self) -> Self {
Self {
species: self.species.clone(),
hp: self.hp,
max_hp: self.max_hp,
level: self.level,
stats: self.stats.clone(),
stat_stages: self.stat_stages.clone(),
status: self.status.clone(),
moves: self.moves.clone(),
resources: self.resources.clone(),
}
}
}
pub struct BattleState<P: BattleProvider + ?Sized> {
pub player_battlers: Vec<BattlerState<P>>,
pub opponent_battlers: Vec<BattlerState<P>>,
pub turn_order: Vec<usize>,
pub weather: Weather,
pub terrain: Terrain,
pub turn_count: u32,
}
impl<P: BattleProvider + ?Sized> BattleState<P> {
pub fn new(
player_battlers: Vec<BattlerState<P>>,
opponent_battlers: Vec<BattlerState<P>>,
) -> Self {
Self {
player_battlers,
opponent_battlers,
turn_order: Vec::new(),
weather: Weather::default(),
terrain: Terrain::default(),
turn_count: 0,
}
}
pub fn active_player(&self) -> Option<&BattlerState<P>> {
self.player_battlers.first()
}
pub fn active_player_mut(&mut self) -> Option<&mut BattlerState<P>> {
self.player_battlers.first_mut()
}
pub fn active_opponent(&self) -> Option<&BattlerState<P>> {
self.opponent_battlers.first()
}
pub fn active_opponent_mut(&mut self) -> Option<&mut BattlerState<P>> {
self.opponent_battlers.first_mut()
}
}
impl<P: BattleProvider + ?Sized> fmt::Debug for BattleState<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BattleState")
.field("player_battlers", &self.player_battlers)
.field("opponent_battlers", &self.opponent_battlers)
.field("turn_order", &self.turn_order)
.field("weather", &self.weather)
.field("terrain", &self.terrain)
.field("turn_count", &self.turn_count)
.finish()
}
}
impl<P: BattleProvider + ?Sized> Clone for BattleState<P> {
fn clone(&self) -> Self {
Self {
player_battlers: self.player_battlers.clone(),
opponent_battlers: self.opponent_battlers.clone(),
turn_order: self.turn_order.clone(),
weather: self.weather,
terrain: self.terrain,
turn_count: self.turn_count,
}
}
}
pub trait TypeChart {
type Type: PartialEq + fmt::Debug;
fn effectiveness(attacking: &Self::Type, defending: &[Self::Type]) -> f32;
}
pub trait BattleAI<P: BattleProvider + ?Sized> {
fn select_move(&self, battler: &BattlerState<P>, state: &BattleState<P>) -> P::Move;
fn should_switch(&self, battler: &BattlerState<P>) -> bool;
fn should_use_item(&self, battler: &BattlerState<P>) -> Option<P::Item>;
}
pub trait EffectHandler<P: BattleProvider + ?Sized> {
fn handle_effect(
&self,
effect: MoveEffect,
user: &mut BattlerState<P>,
target: &mut BattlerState<P>,
provider: &P,
) -> EffectResult;
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MockType {
TypeA,
TypeB,
TypeC,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MockStat {
Hp,
Attack,
Defense,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
enum MockStatus {
Poison,
Burn,
Sleep,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
enum MockSpecies {
Alpha,
Beta,
Gamma,
}
#[derive(Debug, Clone, PartialEq)]
struct MockMove {
name: String,
power: u8,
move_type: MockType,
accuracy: u8,
}
#[derive(Debug, Clone, PartialEq)]
struct MockMonster {
name: String,
monster_type: MockType,
hp: u16,
attack: u16,
defense: u16,
}
struct MockTypeChart;
impl TypeChart for MockTypeChart {
type Type = MockType;
fn effectiveness(attacking: &Self::Type, defending: &[Self::Type]) -> f32 {
let def = defending.first().copied().unwrap_or(MockType::TypeA);
match (attacking, def) {
(MockType::TypeA, MockType::TypeB) => 2.0,
(MockType::TypeB, MockType::TypeC) => 2.0,
(MockType::TypeC, MockType::TypeA) => 2.0,
(MockType::TypeA, MockType::TypeC) => 0.5,
(MockType::TypeB, MockType::TypeA) => 0.5,
(MockType::TypeC, MockType::TypeB) => 0.5,
(a, d) if *a == d => 0.5,
_ => 1.0,
}
}
}
struct MockProvider;
impl BattleProvider for MockProvider {
type Monster = MockMonster;
type Move = MockMove;
type Ability = String;
type Status = MockStatus;
type Stat = MockStat;
type Species = MockSpecies;
type Type = MockType;
type Item = String;
fn calculate_damage(
&self,
move_: &Self::Move,
attacker: &BattlerState<Self>,
defender: &BattlerState<Self>,
_random: u8,
_is_critical: bool,
) -> DamageResult {
let atk = attacker.stats.get(MockStat::Attack).copied().unwrap_or(0);
let def = defender.stats.get(MockStat::Defense).copied().unwrap_or(1).max(1);
let defender_types: Vec<MockType> = vec![];
let effectiveness =
MockTypeChart::effectiveness(&move_.move_type, &defender_types);
if effectiveness == 0.0 {
return DamageResult {
damage: 0,
effectiveness,
is_miss: true,
};
}
let base = (move_.power as u32 * atk as u32 / def as u32) as u16;
let damage = ((base as f32) * effectiveness) as u16;
DamageResult {
damage: damage.max(1),
effectiveness,
is_miss: false,
}
}
fn select_move(
&self,
battler: &BattlerState<Self>,
_state: &BattleState<Self>,
) -> Self::Move {
battler.moves.first().cloned().unwrap()
}
fn apply_move_effect(
&self,
_effect: MoveEffect,
user: &mut BattlerState<Self>,
target: &mut BattlerState<Self>,
) -> EffectResult {
let power = user
.moves
.first()
.map(|m| m.power)
.unwrap_or(0);
if power == 0 {
return EffectResult::NoEffect;
}
target.take_damage(power as u16);
EffectResult::DamageDealt {
amount: power as u16,
}
}
fn create_monster(&self, species: Self::Species, _level: u8) -> BattlerState<Self> {
let (hp, atk, def, _monster_type) = match species {
MockSpecies::Alpha => (100, 50, 30, MockType::TypeA),
MockSpecies::Beta => (120, 40, 40, MockType::TypeB),
MockSpecies::Gamma => (90, 60, 20, MockType::TypeC),
};
let mut stats = EnumMap::new();
stats.set(MockStat::Hp, hp);
stats.set(MockStat::Attack, atk);
stats.set(MockStat::Defense, def);
BattlerState::new(species, hp, hp, stats, Vec::new())
}
}
struct MockAI;
impl BattleAI<MockProvider> for MockAI {
fn select_move(
&self,
battler: &BattlerState<MockProvider>,
_state: &BattleState<MockProvider>,
) -> MockMove {
battler.moves.first().cloned().unwrap()
}
fn should_switch(&self, _battler: &BattlerState<MockProvider>) -> bool {
false
}
fn should_use_item(
&self,
_battler: &BattlerState<MockProvider>,
) -> Option<String> {
None
}
}
struct MockEffectHandler;
impl EffectHandler<MockProvider> for MockEffectHandler {
fn handle_effect(
&self,
effect: MoveEffect,
_user: &mut BattlerState<MockProvider>,
target: &mut BattlerState<MockProvider>,
_provider: &MockProvider,
) -> EffectResult {
match effect {
MoveEffect::Damage => {
target.take_damage(10);
EffectResult::DamageDealt { amount: 10 }
}
MoveEffect::Heal => {
target.heal(10);
EffectResult::Healed { amount: 10 }
}
MoveEffect::StatusCondition => EffectResult::StatusInflicted,
MoveEffect::StatChange => EffectResult::StatModified { stages: 1 },
MoveEffect::MultiHit => EffectResult::MultiHit { hits: 3 },
MoveEffect::Recharge => EffectResult::MustRecharge,
_ => EffectResult::NoEffect,
}
}
}
fn make_battler(species: MockSpecies, hp: u16, atk: u16, def: u16) -> BattlerState<MockProvider> {
let mut stats = EnumMap::new();
stats.set(MockStat::Hp, hp);
stats.set(MockStat::Attack, atk);
stats.set(MockStat::Defense, def);
BattlerState::new(species, hp, hp, stats, Vec::new())
}
fn make_move(name: &str, power: u8, move_type: MockType) -> MockMove {
MockMove {
name: name.to_string(),
power,
move_type,
accuracy: 255,
}
}
#[test]
fn type_a_beats_type_b() {
let eff = MockTypeChart::effectiveness(&MockType::TypeA, &[MockType::TypeB]);
assert!((eff - 2.0).abs() < f32::EPSILON, "TypeA should be 2x vs TypeB, got {eff}");
}
#[test]
fn type_a_vs_type_a_is_half() {
let eff = MockTypeChart::effectiveness(&MockType::TypeA, &[MockType::TypeA]);
assert!((eff - 0.5).abs() < f32::EPSILON, "TypeA vs TypeA should be 0.5x, got {eff}");
}
#[test]
fn type_b_beats_type_c() {
let eff = MockTypeChart::effectiveness(&MockType::TypeB, &[MockType::TypeC]);
assert!((eff - 2.0).abs() < f32::EPSILON, "TypeB should be 2x vs TypeC, got {eff}");
}
#[test]
fn type_c_beats_type_a() {
let eff = MockTypeChart::effectiveness(&MockType::TypeC, &[MockType::TypeA]);
assert!((eff - 2.0).abs() < f32::EPSILON, "TypeC should be 2x vs TypeA, got {eff}");
}
#[test]
fn type_b_vs_type_a_is_half() {
let eff = MockTypeChart::effectiveness(&MockType::TypeB, &[MockType::TypeA]);
assert!((eff - 0.5).abs() < f32::EPSILON, "TypeB vs TypeA should be 0.5x, got {eff}");
}
#[test]
fn provider_can_be_implemented() {
let provider = MockProvider;
let battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
assert!(!provider.check_faint(&battler));
}
#[test]
fn create_monster_returns_battler() {
let provider = MockProvider;
let battler = provider.create_monster(MockSpecies::Alpha, 50);
assert_eq!(battler.hp, 100);
assert_eq!(battler.max_hp, 100);
}
#[test]
fn calculate_damage_with_type_advantage() {
let provider = MockProvider;
let attacker = make_battler(MockSpecies::Alpha, 100, 50, 30);
let defender = make_battler(MockSpecies::Beta, 100, 40, 40);
let mv = make_move("SuperPunch", 60, MockType::TypeA);
let result = provider.calculate_damage(&mv, &attacker, &defender, 255, false);
assert!(result.damage > 0);
assert!(!result.is_miss);
}
#[test]
fn select_move_returns_first_move() {
let provider = MockProvider;
let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
let mv = make_move("Tackle", 40, MockType::TypeA);
battler.moves = vec![mv.clone()];
let state = BattleState::<MockProvider>::new(
vec![battler.clone()],
vec![make_battler(MockSpecies::Beta, 100, 40, 40)],
);
let selected = provider.select_move(&battler, &state);
assert_eq!(selected.name, "Tackle");
}
#[test]
fn apply_move_effect_deals_damage() {
let provider = MockProvider;
let mut user = make_battler(MockSpecies::Alpha, 100, 50, 30);
let mv = make_move("Tackle", 40, MockType::TypeA);
user.moves = vec![mv];
let mut target = make_battler(MockSpecies::Beta, 100, 40, 40);
let result = provider.apply_move_effect(MoveEffect::Damage, &mut user, &mut target);
assert!(matches!(result, EffectResult::DamageDealt { .. }));
assert_eq!(target.hp, 60); }
#[test]
fn check_faint_detects_zero_hp() {
let provider = MockProvider;
let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
assert!(!provider.check_faint(&battler));
battler.hp = 0;
assert!(provider.check_faint(&battler));
}
#[test]
fn ai_trait_can_be_implemented() {
let ai = MockAI;
let battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
let _state = BattleState::<MockProvider>::new(
vec![battler.clone()],
vec![make_battler(MockSpecies::Beta, 100, 40, 40)],
);
assert!(!ai.should_switch(&battler));
assert!(ai.should_use_item(&battler).is_none());
}
#[test]
fn ai_select_move_works() {
let ai = MockAI;
let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
let mv = make_move("Fireball", 50, MockType::TypeA);
battler.moves = vec![mv.clone()];
let state = BattleState::<MockProvider>::new(
vec![battler.clone()],
vec![make_battler(MockSpecies::Beta, 100, 40, 40)],
);
let chosen = ai.select_move(&battler, &state);
assert_eq!(chosen.name, "Fireball");
}
#[test]
fn effect_handler_trait_can_be_implemented() {
let handler = MockEffectHandler;
let provider = MockProvider;
let mut user = make_battler(MockSpecies::Alpha, 100, 50, 30);
let mut target = make_battler(MockSpecies::Beta, 100, 40, 40);
let result = handler.handle_effect(MoveEffect::Damage, &mut user, &mut target, &provider);
assert!(matches!(result, EffectResult::DamageDealt { amount: 10 }));
assert_eq!(target.hp, 90);
}
#[test]
fn effect_handler_heal_works() {
let handler = MockEffectHandler;
let provider = MockProvider;
let mut user = make_battler(MockSpecies::Alpha, 50, 50, 30);
user.hp = 50;
let mut target = make_battler(MockSpecies::Beta, 100, 40, 40);
let result = handler.handle_effect(MoveEffect::Heal, &mut user, &mut target, &provider);
assert!(matches!(result, EffectResult::Healed { amount: 10 }));
assert_eq!(target.hp, 100); }
#[test]
fn effect_handler_status_and_stat() {
let handler = MockEffectHandler;
let provider = MockProvider;
let mut user = make_battler(MockSpecies::Alpha, 100, 50, 30);
let mut target = make_battler(MockSpecies::Beta, 100, 40, 40);
let r1 = handler.handle_effect(MoveEffect::StatusCondition, &mut user, &mut target, &provider);
assert_eq!(r1, EffectResult::StatusInflicted);
let r2 = handler.handle_effect(MoveEffect::StatChange, &mut user, &mut target, &provider);
assert_eq!(r2, EffectResult::StatModified { stages: 1 });
}
#[test]
fn battler_state_take_damage() {
let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
battler.take_damage(30);
assert_eq!(battler.hp, 70);
}
#[test]
fn battler_state_take_damage_no_underflow() {
let mut battler = make_battler(MockSpecies::Alpha, 10, 50, 30);
battler.take_damage(999);
assert_eq!(battler.hp, 0);
}
#[test]
fn battler_state_heal() {
let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
battler.hp = 50;
battler.heal(30);
assert_eq!(battler.hp, 80);
}
#[test]
fn battler_state_heal_caps_at_max() {
let mut battler = make_battler(MockSpecies::Alpha, 100, 50, 30);
battler.hp = 90;
battler.heal(50);
assert_eq!(battler.hp, 100);
}
#[test]
fn battle_state_defaults() {
let p1 = make_battler(MockSpecies::Alpha, 100, 50, 30);
let o1 = make_battler(MockSpecies::Beta, 100, 40, 40);
let state = BattleState::<MockProvider>::new(vec![p1.clone()], vec![o1.clone()]);
assert_eq!(state.player_battlers.len(), 1);
assert_eq!(state.opponent_battlers.len(), 1);
assert_eq!(state.turn_count, 0);
assert!(matches!(state.weather, Weather::Clear));
assert!(matches!(state.terrain, Terrain::Normal));
}
#[test]
fn battle_state_active_player() {
let p1 = make_battler(MockSpecies::Alpha, 100, 50, 30);
let o1 = make_battler(MockSpecies::Beta, 100, 40, 40);
let state = BattleState::<MockProvider>::new(vec![p1], vec![o1]);
let active = state.active_player().unwrap();
assert_eq!(active.hp, 100);
}
#[test]
fn enum_map_set_and_get() {
let mut map: EnumMap<MockStat, u16> = EnumMap::new();
map.set(MockStat::Attack, 50);
map.set(MockStat::Defense, 30);
assert_eq!(map.get(MockStat::Attack), Some(&50));
assert_eq!(map.get(MockStat::Defense), Some(&30));
assert_eq!(map.get(MockStat::Hp), None);
}
#[test]
fn enum_map_overwrite() {
let mut map: EnumMap<MockStat, u16> = EnumMap::new();
map.set(MockStat::Hp, 100);
map.set(MockStat::Hp, 200);
assert_eq!(map.get(MockStat::Hp), Some(&200));
assert_eq!(map.len(), 1);
}
#[test]
fn enum_map_default_is_empty() {
let map: EnumMap<MockStat, u16> = EnumMap::default();
assert!(map.is_empty());
assert_eq!(map.len(), 0);
}
#[test]
fn resource_pool_default_is_empty_and_inert() {
let pool = ResourcePool::default();
assert!(pool.is_empty());
assert_eq!(pool.len(), 0);
assert!(pool.can_pay(0, 0), "0 cost is always payable");
assert!(!pool.can_pay(0, 1), "positive cost on undeclared resource ⇒ not payable");
assert_eq!(pool.current(0), None);
}
#[test]
fn resource_pool_set_can_pay_and_pay() {
let mut pool = ResourcePool::new();
pool.set(7, 10, 10); assert_eq!(pool.current(7), Some(10));
assert_eq!(pool.max(7), Some(10));
assert!(pool.can_pay(7, 4));
assert!(pool.can_pay(7, 10), "exact balance is payable");
assert!(!pool.can_pay(7, 11), "over balance is not payable");
assert!(pool.pay(7, 4), "deduction applied");
assert_eq!(pool.current(7), Some(6), "10 - 4 = 6");
assert!(!pool.pay(7, 0), "0 deduction is a no-op (returns false)");
assert_eq!(pool.current(7), Some(6), "0 deduction left it unchanged");
}
#[test]
fn resource_pool_pay_saturates_and_restore_clamps() {
let mut pool = ResourcePool::new();
pool.set(0, 3, 10);
pool.pay(0, 100); assert_eq!(pool.current(0), Some(0));
pool.restore(0, 4);
assert_eq!(pool.current(0), Some(4));
pool.restore(0, 1000); assert_eq!(pool.current(0), Some(10));
}
#[test]
fn resource_pool_set_clamps_current_to_max() {
let mut pool = ResourcePool::new();
pool.set(0, 50, 20); assert_eq!(pool.current(0), Some(20), "current clamped to max");
}
#[test]
fn battler_state_resources_default_empty() {
let b: BattlerState<MockProvider> =
BattlerState::new(MockSpecies::Alpha, 100, 100, EnumMap::new(), vec![]);
assert!(b.resources.is_empty(), "default battler has an empty resource pool");
assert!(b.can_pay_resource(0, 0), "0 cost payable on an empty pool");
assert!(!b.can_pay_resource(0, 5), "positive cost unpayable on an empty pool");
let mut b = b.with_resource(0, 8);
assert!(b.can_pay_resource(0, 5));
assert!(b.pay_resource(0, 5));
assert_eq!(b.resources.current(0), Some(3));
}
#[test]
fn weather_default_is_clear() {
assert_eq!(Weather::default(), Weather::Clear);
}
#[test]
fn terrain_default_is_normal() {
assert_eq!(Terrain::default(), Terrain::Normal);
}
#[test]
fn move_effect_variants_are_available() {
let _effects = [
MoveEffect::Damage,
MoveEffect::Heal,
MoveEffect::StatusCondition,
MoveEffect::StatChange,
MoveEffect::MultiHit,
MoveEffect::Recharge,
MoveEffect::DrainHp,
MoveEffect::Recoil,
MoveEffect::Flinch,
MoveEffect::FieldEffect,
MoveEffect::SpecialDamage,
MoveEffect::Ohko,
MoveEffect::MultiTurn,
];
}
#[test]
fn effect_result_variants_are_available() {
let _results = [
EffectResult::NoEffect,
EffectResult::DamageDealt { amount: 0 },
EffectResult::Healed { amount: 0 },
EffectResult::StatusInflicted,
EffectResult::StatusFailed,
EffectResult::StatModified { stages: 1 },
EffectResult::StatBlocked,
EffectResult::HpDrained { drained: 0 },
EffectResult::RecoilDamage { recoil: 0 },
EffectResult::Fainted,
EffectResult::Miss,
EffectResult::CriticalHit,
EffectResult::MultiHit { hits: 2 },
EffectResult::MustRecharge,
EffectResult::FieldEffectSet,
];
}
}
#[cfg(test)]
mod driver_tests {
use super::driver::{BattleDriver, BattleEnd, TurnEvent};
use super::rng::{BattleRng, ScriptedRng};
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DStat {
Speed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DStatus {
Sleep,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DType {
Normal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DSpecies {
Mon,
}
#[derive(Debug, Clone, PartialEq)]
struct DMove {
power: u8,
accuracy: u8,
}
struct DProvider {
residual: u16,
}
impl BattleProvider for DProvider {
type Monster = ();
type Move = DMove;
type Ability = ();
type Status = DStatus;
type Stat = DStat;
type Species = DSpecies;
type Type = DType;
type Item = ();
fn calculate_damage(
&self,
move_: &Self::Move,
_attacker: &BattlerState<Self>,
_defender: &BattlerState<Self>,
_random: u8,
_is_critical: bool,
) -> DamageResult {
DamageResult {
damage: move_.power as u16,
effectiveness: 1.0,
is_miss: false,
}
}
fn select_move(
&self,
battler: &BattlerState<Self>,
_state: &BattleState<Self>,
) -> Self::Move {
battler.moves.first().cloned().unwrap()
}
fn apply_move_effect(
&self,
_effect: MoveEffect,
_user: &mut BattlerState<Self>,
_target: &mut BattlerState<Self>,
) -> EffectResult {
EffectResult::NoEffect
}
fn create_monster(&self, species: Self::Species, _level: u8) -> BattlerState<Self> {
BattlerState::new(species, 100, 100, EnumMap::new(), Vec::new())
}
fn turn_order_key(
&self,
state: &BattleState<Self>,
who: BattlerRef,
_action: &BattleAction<Self>,
rng: &mut dyn BattleRng,
) -> OrderKey {
let party = if who.side == 0 {
&state.player_battlers
} else {
&state.opponent_battlers
};
let speed = party
.get(who.slot as usize)
.and_then(|b| b.stats.get(DStat::Speed).copied())
.unwrap_or(0) as i32;
let tiebreak = rng.next_u8() as u32;
OrderKey(0, -speed, tiebreak)
}
fn before_move(
&self,
state: &mut BattleState<Self>,
who: BattlerRef,
_action: &BattleAction<Self>,
_rng: &mut dyn BattleRng,
) -> MoveGate<Self> {
let party = if who.side == 0 {
&state.player_battlers
} else {
&state.opponent_battlers
};
if let Some(b) = party.get(who.slot as usize) {
if b.status == Some(DStatus::Sleep) {
return MoveGate::Prevented(EffectResult::NoEffect);
}
}
MoveGate::Acts
}
fn accuracy_check(
&self,
_state: &BattleState<Self>,
_who: BattlerRef,
_target: BattlerRef,
move_: &Self::Move,
rng: &mut dyn BattleRng,
) -> bool {
(rng.next_u8() as u32) < move_.accuracy as u32
}
fn end_of_turn(
&self,
state: &mut BattleState<Self>,
_rng: &mut dyn BattleRng,
) -> Vec<EffectResult> {
let mut out = Vec::new();
for party in [&mut state.player_battlers, &mut state.opponent_battlers] {
for b in party.iter_mut() {
if b.hp > 0 {
b.take_damage(self.residual);
out.push(EffectResult::DamageDealt {
amount: self.residual,
});
}
}
}
out
}
}
struct CritProvider;
impl BattleProvider for CritProvider {
type Monster = ();
type Move = DMove;
type Ability = ();
type Status = DStatus;
type Stat = DStat;
type Species = DSpecies;
type Type = DType;
type Item = ();
fn calculate_damage(
&self,
move_: &Self::Move,
_attacker: &BattlerState<Self>,
_defender: &BattlerState<Self>,
_random: u8,
is_critical: bool,
) -> DamageResult {
let base = move_.power as u16;
DamageResult {
damage: if is_critical { base * 2 } else { base },
effectiveness: 1.0,
is_miss: false,
}
}
fn select_move(
&self,
battler: &BattlerState<Self>,
_state: &BattleState<Self>,
) -> Self::Move {
battler.moves.first().cloned().unwrap()
}
fn apply_move_effect(
&self,
_effect: MoveEffect,
_user: &mut BattlerState<Self>,
_target: &mut BattlerState<Self>,
) -> EffectResult {
EffectResult::NoEffect
}
fn create_monster(&self, species: Self::Species, _level: u8) -> BattlerState<Self> {
BattlerState::new(species, 100, 100, EnumMap::new(), Vec::new())
}
fn roll_critical(
&self,
_state: &BattleState<Self>,
_who: BattlerRef,
_target: BattlerRef,
_move_: &Self::Move,
_rng: &mut dyn BattleRng,
) -> bool {
true
}
}
fn crit_mon(hp: u16, speed: u16, power: u8) -> BattlerState<CritProvider> {
let mut stats = EnumMap::new();
stats.set(DStat::Speed, speed);
BattlerState::new(
DSpecies::Mon,
hp,
hp,
stats,
vec![DMove { power, accuracy: 255 }],
)
}
fn mon(hp: u16, speed: u16, accuracy: u8, power: u8) -> BattlerState<DProvider> {
let mut stats = EnumMap::new();
stats.set(DStat::Speed, speed);
BattlerState::new(DSpecies::Mon, hp, hp, stats, vec![DMove { power, accuracy }])
}
fn fight_pow(power: u8) -> BattleAction<DProvider> {
BattleAction::Fight {
move_: DMove {
power,
accuracy: 255,
},
}
}
fn fight() -> BattleAction<DProvider> {
fight_pow(20)
}
fn first_mover(out: &TurnOutcome<DProvider>) -> BattlerRef {
out.events
.iter()
.find_map(|e| match e {
TurnEvent::MoveUsed { who, .. } => Some(*who),
_ => None,
})
.unwrap()
}
#[test]
fn turn_order_faster_battler_acts_first() {
let provider = DProvider { residual: 0 };
let mut state = BattleState::new(vec![mon(100, 10, 255, 20)], vec![mon(100, 99, 255, 20)]);
let mut rng = ScriptedRng::new(vec![5, 5, 0, 0, 0, 0]);
let out = BattleDriver::execute_turn(&provider, &mut state, [fight(), fight()], &mut rng);
assert_eq!(first_mover(&out), BattlerRef::OPPONENT, "faster opponent acts first");
}
#[test]
fn turn_order_tie_uses_rng_tiebreak() {
let provider = DProvider { residual: 0 };
let mut state = BattleState::new(vec![mon(100, 50, 255, 20)], vec![mon(100, 50, 255, 20)]);
let mut rng = ScriptedRng::new(vec![9, 1, 0, 0, 0, 0]);
let out = BattleDriver::execute_turn(&provider, &mut state, [fight(), fight()], &mut rng);
assert_eq!(first_mover(&out), BattlerRef::OPPONENT, "smaller tiebreak acts first");
}
#[test]
fn before_move_gate_skips_asleep_battler() {
let provider = DProvider { residual: 0 };
let mut player = mon(100, 99, 255, 20); player.status = Some(DStatus::Sleep);
let mut state = BattleState::new(vec![player], vec![mon(100, 10, 255, 20)]);
let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
let out = BattleDriver::execute_turn(&provider, &mut state, [fight(), fight()], &mut rng);
assert!(
out.events.iter().any(|e| matches!(
e,
TurnEvent::ActionPrevented { who, .. } if *who == BattlerRef::PLAYER
)),
"asleep player should be prevented"
);
assert!(
out.events.iter().any(|e| matches!(
e,
TurnEvent::MoveUsed { who, .. } if *who == BattlerRef::OPPONENT
)),
"opponent should still act"
);
}
#[test]
fn move_execution_applies_damage_via_hook() {
let provider = DProvider { residual: 0 };
let mut state = BattleState::new(
vec![mon(100, 99, 255, 0)], vec![mon(100, 10, 255, 0)],
);
let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
let _ = BattleDriver::execute_turn(
&provider,
&mut state,
[fight_pow(30), fight_pow(30)],
&mut rng,
);
assert_eq!(state.opponent_battlers[0].hp, 70);
assert_eq!(state.player_battlers[0].hp, 70);
}
#[test]
fn accuracy_check_can_miss() {
let provider = DProvider { residual: 0 };
let mut state = BattleState::new(vec![mon(100, 99, 255, 20)], vec![mon(100, 10, 255, 20)]);
let player_fight = BattleAction::Fight {
move_: DMove {
power: 20,
accuracy: 0,
},
};
let opp_fight = BattleAction::Fight {
move_: DMove {
power: 20,
accuracy: 255,
},
};
let mut rng = ScriptedRng::new(vec![0, 0, 100, 0, 100, 0]);
let out =
BattleDriver::execute_turn(&provider, &mut state, [player_fight, opp_fight], &mut rng);
assert_eq!(state.opponent_battlers[0].hp, 100);
assert_eq!(state.player_battlers[0].hp, 80);
assert!(out.events.iter().any(|e| matches!(
e,
TurnEvent::Missed { who, .. } if *who == BattlerRef::PLAYER
)));
}
#[test]
fn end_of_turn_residual_ticks() {
let provider = DProvider { residual: 5 };
let mut state = BattleState::new(vec![mon(100, 50, 255, 0)], vec![mon(100, 50, 255, 0)]);
let mut rng = ScriptedRng::new(vec![1, 2, 0, 0, 0, 0]);
let out =
BattleDriver::execute_turn(&provider, &mut state, [fight_pow(0), fight_pow(0)], &mut rng);
assert_eq!(state.player_battlers[0].hp, 95);
assert_eq!(state.opponent_battlers[0].hp, 95);
assert_eq!(
out.events
.iter()
.filter(|e| matches!(e, TurnEvent::Residual { .. }))
.count(),
2
);
}
#[test]
fn faint_leads_to_battle_end_player_win() {
let provider = DProvider { residual: 0 };
let mut state = BattleState::new(vec![mon(100, 99, 255, 0)], vec![mon(100, 10, 255, 0)]);
let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
let out =
BattleDriver::execute_turn(&provider, &mut state, [fight_pow(200), fight()], &mut rng);
assert_eq!(state.opponent_battlers[0].hp, 0);
assert_eq!(out.battle_over, Some(BattleEnd::PlayerWin));
assert!(out
.events
.iter()
.any(|e| matches!(e, TurnEvent::Faint { who } if *who == BattlerRef::OPPONENT)));
assert!(!out.events.iter().any(|e| matches!(
e,
TurnEvent::MoveUsed { who, .. } if *who == BattlerRef::OPPONENT
)));
}
#[test]
fn faint_leads_to_battle_end_player_loss() {
let provider = DProvider { residual: 0 };
let mut state = BattleState::new(vec![mon(100, 10, 255, 0)], vec![mon(100, 99, 255, 0)]);
let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
let out =
BattleDriver::execute_turn(&provider, &mut state, [fight(), fight_pow(200)], &mut rng);
assert_eq!(state.player_battlers[0].hp, 0);
assert_eq!(out.battle_over, Some(BattleEnd::PlayerLoss));
}
#[test]
fn switch_swaps_active_slot() {
let provider = DProvider { residual: 0 };
let mut state = BattleState::new(
vec![mon(100, 50, 255, 20), mon(80, 50, 255, 20)],
vec![mon(100, 50, 255, 20)],
);
let switch = BattleAction::Switch { to_slot: 1 };
let mut rng = ScriptedRng::new(vec![5, 5, 0, 0]);
let _ = BattleDriver::execute_turn(&provider, &mut state, [switch, fight()], &mut rng);
assert_eq!(state.player_battlers[0].max_hp, 80);
assert_eq!(state.player_battlers[0].hp, 60);
}
#[test]
fn scripted_rng_draw_order_is_deterministic() {
let run = || {
let provider = DProvider { residual: 3 };
let mut state =
BattleState::new(vec![mon(100, 50, 200, 15)], vec![mon(100, 50, 200, 15)]);
let mut rng = ScriptedRng::new(vec![7, 2, 10, 0, 10, 0]);
let _ = BattleDriver::execute_turn(&provider, &mut state, [fight(), fight()], &mut rng);
(
state.player_battlers[0].hp,
state.opponent_battlers[0].hp,
rng.consumed(),
)
};
assert_eq!(run(), run());
}
#[test]
fn provider_critical_hit_surfaces_into_damage_event() {
let provider = CritProvider;
let mut state =
BattleState::new(vec![crit_mon(100, 99, 10)], vec![crit_mon(100, 10, 10)]);
let mut rng = ScriptedRng::new(vec![0, 0]);
let crit_fight = || BattleAction::<CritProvider>::Fight {
move_: DMove {
power: 10,
accuracy: 255,
},
};
let out = BattleDriver::execute_turn(
&provider,
&mut state,
[crit_fight(), crit_fight()],
&mut rng,
);
assert_eq!(state.opponent_battlers[0].hp, 80, "crit doubled 10 → 20 dmg");
let player_dmg = out
.events
.iter()
.find_map(|e| match e {
TurnEvent::Damage {
who,
critical,
amount,
..
} if *who == BattlerRef::PLAYER => Some((*critical, *amount)),
_ => None,
})
.expect("player should have a Damage event");
assert!(player_dmg.0, "provider crit must surface as critical: true");
assert_eq!(player_dmg.1, 20, "Damage event amount reflects the crit");
}
#[test]
fn default_roll_critical_yields_non_critical_damage_event() {
let provider = DProvider { residual: 0 };
let mut state = BattleState::new(vec![mon(100, 99, 255, 30)], vec![mon(100, 10, 255, 0)]);
let mut rng = ScriptedRng::new(vec![0, 0, 0, 0, 0, 0]);
let out = BattleDriver::execute_turn(
&provider,
&mut state,
[fight_pow(30), fight_pow(0)],
&mut rng,
);
assert!(out.events.iter().any(|e| matches!(
e,
TurnEvent::Damage { who, critical: false, .. } if *who == BattlerRef::PLAYER
)));
}
}