#[macro_use]
pub mod authoring;
pub mod ctx;
pub mod dispatch;
pub mod driver;
pub mod event;
pub mod log;
pub use ctx::{BattleCtx, EffectHost, EffectProvider, EffectState, MoveContext};
pub use dispatch::{collect_handlers, compare, run_event, run_event_checked, CollectedHandler};
pub use driver::{FirstMover, StackDriver, StackTurnResult};
pub use log::{HpChangeCause, TurnEvent, TurnLog};
pub use event::{
Effect, EffectId, EffectType, Event, EventHook, HandlerFn, HandlerResult, RelayVar,
};
#[cfg(test)]
pub(crate) mod tests_support;
#[cfg(test)]
mod tests {
use super::*;
use crate::battle::rng::ScriptedRng;
use crate::battle::{
BattleProvider, BattleState, BattlerRef, BattlerState, DamageResult, EnumMap, MoveEffect,
EffectResult,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TStat {
Hp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)] enum TStatus {
Poisoned,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
enum TType {
N,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TSpecies {
Mon,
}
#[derive(Debug, Clone, PartialEq)]
struct TMove {
power: u8,
}
#[derive(Clone)]
#[allow(dead_code)] enum TKind {
None,
Toxic { counter: u8 },
}
struct TProvider;
impl BattleProvider for TProvider {
type Monster = ();
type Move = TMove;
type Ability = ();
type Status = TStatus;
type Stat = TStat;
type Species = TSpecies;
type Type = TType;
type Item = ();
fn calculate_damage(
&self,
move_: &Self::Move,
_a: &BattlerState<Self>,
_d: &BattlerState<Self>,
_r: u8,
_c: bool,
) -> DamageResult {
DamageResult {
damage: move_.power as u16,
effectiveness: 1.0,
is_miss: false,
}
}
fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
b.moves.first().cloned().unwrap()
}
fn apply_move_effect(
&self,
_e: MoveEffect,
_u: &mut BattlerState<Self>,
_t: &mut BattlerState<Self>,
) -> EffectResult {
EffectResult::NoEffect
}
fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
}
}
impl EffectProvider for TProvider {
type EffectStateKind = TKind;
fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
None
}
fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
None
}
fn turn_order_rank(
&self,
_state: &BattleState<Self>,
_who: BattlerRef,
_action: &Self::Move,
) -> (i32, i32) {
(0, 0) }
}
fn mon(hp: u16) -> BattlerState<TProvider> {
let mut stats = EnumMap::new();
stats.set(TStat::Hp, hp);
BattlerState::new(TSpecies::Mon, hp, hp, stats, vec![TMove { power: 10 }])
}
fn deal_5<P: EffectProvider<EffectStateKind = TKind> + ?Sized>(
ctx: &mut BattleCtx<'_, P>,
_relay: RelayVar,
target: BattlerRef,
_source: BattlerRef,
_eff: EffectId,
) -> HandlerResult {
ctx.battler_mut(target).take_damage(5);
HandlerResult::Unchanged
}
fn add_int<P: EffectProvider + ?Sized>(
_ctx: &mut BattleCtx<'_, P>,
relay: RelayVar,
_t: BattlerRef,
_s: BattlerRef,
_e: EffectId,
) -> HandlerResult {
let n = match relay {
RelayVar::Int(n) => n,
_ => 0,
};
HandlerResult::Set(RelayVar::Int(n + 1))
}
fn counter_shaped<P: EffectProvider + ?Sized>(
ctx: &mut BattleCtx<'_, P>,
_relay: RelayVar,
target: BattlerRef,
source: BattlerRef,
_eff: EffectId,
) -> HandlerResult {
let (tgt, src) = ctx.pair_mut(target, source);
let reflected = src.hp / 10; tgt.take_damage(reflected); HandlerResult::Unchanged
}
fn make_ctx_parts(
hp_a: u16,
hp_b: u16,
) -> (BattleState<TProvider>, Vec<EffectState<TProvider>>, MoveContext) {
let state = BattleState::new(vec![mon(hp_a)], vec![mon(hp_b)]);
(state, Vec::new(), MoveContext::default())
}
#[test]
fn fold_dispatch_runs_and_threads_relay() {
let (mut state, mut effects, mut mv) = make_ctx_parts(100, 100);
let mut rng = ScriptedRng::new(vec![0]);
let mut ctx = BattleCtx {
state: &mut state,
effects: &mut effects,
mv: &mut mv,
rng: &mut rng,
};
let hs = vec![
CollectedHandler {
order: 1,
priority: 0,
speed: 0,
sub_order: 6,
effect_order: 0,
target: BattlerRef::OPPONENT,
source: BattlerRef::PLAYER,
source_effect: EffectId(1),
call: add_int::<TProvider>,
},
CollectedHandler {
order: 2,
priority: 0,
speed: 0,
sub_order: 6,
effect_order: 1,
target: BattlerRef::OPPONENT,
source: BattlerRef::PLAYER,
source_effect: EffectId(2),
call: deal_5::<TProvider>,
},
];
let out = run_event(&mut ctx, hs, RelayVar::Int(0), false);
assert_eq!(out, RelayVar::Int(1), "relay threaded through add_int");
assert_eq!(state.opponent_battlers[0].hp, 95, "deal_5 mutated target");
}
#[test]
fn pair_mut_cross_side_branch_works() {
let (mut state, mut effects, mut mv) = make_ctx_parts(100, 50);
let mut rng = ScriptedRng::new(vec![0]);
let mut ctx = BattleCtx {
state: &mut state,
effects: &mut effects,
mv: &mut mv,
rng: &mut rng,
};
let (player, opp) = ctx.pair_mut(BattlerRef::PLAYER, BattlerRef::OPPONENT);
assert_eq!(player.hp, 100);
assert_eq!(opp.hp, 50);
let opp_hp = opp.hp;
player.take_damage(opp_hp);
assert_eq!(state.player_battlers[0].hp, 50);
assert_eq!(state.opponent_battlers[0].hp, 50);
}
#[test]
fn pair_mut_same_side_branch_works() {
let mut state = BattleState::new(vec![mon(100), mon(40)], vec![mon(100)]);
let mut effects: Vec<EffectState<TProvider>> = Vec::new();
let mut mv = MoveContext::default();
let mut rng = ScriptedRng::new(vec![0]);
let mut ctx = BattleCtx {
state: &mut state,
effects: &mut effects,
mv: &mut mv,
rng: &mut rng,
};
let a = BattlerRef::new(0, 1);
let b = BattlerRef::new(0, 0);
let (slot1, slot0) = ctx.pair_mut(a, b);
assert_eq!(slot1.hp, 40);
assert_eq!(slot0.hp, 100);
let read = slot0.hp;
slot1.take_damage(read / 4); assert_eq!(state.player_battlers[1].hp, 15);
assert_eq!(state.player_battlers[0].hp, 100);
}
#[test]
fn counter_shaped_handler_compiles_no_refcell() {
let (mut state, mut effects, mut mv) = make_ctx_parts(100, 70);
let mut rng = ScriptedRng::new(vec![0]);
let mut ctx = BattleCtx {
state: &mut state,
effects: &mut effects,
mv: &mut mv,
rng: &mut rng,
};
let hs = vec![CollectedHandler {
order: 1,
priority: 0,
speed: 0,
sub_order: 6,
effect_order: 0,
target: BattlerRef::OPPONENT,
source: BattlerRef::PLAYER,
source_effect: EffectId(1),
call: counter_shaped::<TProvider>,
}];
run_event(&mut ctx, hs, RelayVar::Unit, false);
assert_eq!(state.opponent_battlers[0].hp, 60);
}
#[test]
fn typed_effect_state_toxic_counter() {
let (mut state, mut effects, mut mv) = make_ctx_parts(100, 100);
effects.push(EffectState {
id: EffectId(7),
host: BattlerRef::PLAYER,
effect_order: 0,
kind: TKind::Toxic { counter: 0 },
});
let mut rng = ScriptedRng::new(vec![0]);
let mut ctx = BattleCtx {
state: &mut state,
effects: &mut effects,
mv: &mut mv,
rng: &mut rng,
};
let n = match &mut ctx.effect_mut(EffectId(7)).unwrap().kind {
TKind::Toxic { counter } => {
*counter = counter.saturating_add(1);
*counter
}
TKind::None => 0,
};
assert_eq!(n, 1);
assert!(ctx.effect_mut(EffectId(99)).is_none());
}
use crate::battle::BattleAction;
use crate::battle::rng::ScriptedRng as EngineScriptedRng;
fn force_dmg<P: EffectProvider + ?Sized>(
ctx: &mut BattleCtx<'_, P>,
_relay: RelayVar,
_target: BattlerRef,
_source: BattlerRef,
_eff: EffectId,
) -> HandlerResult {
ctx.mv.damage = 10;
HandlerResult::Unchanged
}
static FORCE_DMG_EFFECT: Effect<TForce> = Effect {
id: EffectId(1),
kind: EffectType::Move,
hooks: &[EventHook {
event: Event::ModifyDamage,
call: force_dmg::<TForce>,
order: 1000,
priority: 0,
sub_order: None,
}],
};
struct TForce;
impl BattleProvider for TForce {
type Monster = ();
type Move = TMove;
type Ability = ();
type Status = TStatus;
type Stat = TStat;
type Species = TSpecies;
type Type = TType;
type Item = ();
fn calculate_damage(
&self,
_m: &Self::Move,
_a: &BattlerState<Self>,
_d: &BattlerState<Self>,
_r: u8,
_c: bool,
) -> DamageResult {
DamageResult { damage: 0, effectiveness: 1.0, is_miss: false }
}
fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
b.moves.first().cloned().unwrap()
}
fn apply_move_effect(
&self,
_e: MoveEffect,
_u: &mut BattlerState<Self>,
_t: &mut BattlerState<Self>,
) -> EffectResult {
EffectResult::NoEffect
}
fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
}
}
impl EffectProvider for TForce {
type EffectStateKind = TKind;
fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
Some(&FORCE_DMG_EFFECT)
}
fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
None
}
fn turn_order_rank(
&self,
_state: &BattleState<Self>,
who: BattlerRef,
_action: &Self::Move,
) -> (i32, i32) {
if who.side == 0 { (0, 0) } else { (1, 0) }
}
fn forced_action(
&self,
effects: &[EffectState<Self>],
actor: BattlerRef,
chosen: &BattleAction<Self>,
) -> Option<BattleAction<Self>> {
let locked = effects
.iter()
.any(|e| e.host == actor && matches!(e.kind, TKind::Toxic { .. }));
if locked {
Some(BattleAction::Nothing)
} else {
let _ = chosen;
None
}
}
}
fn force_mon(hp: u16) -> BattlerState<TForce> {
let mut stats = EnumMap::new();
stats.set(TStat::Hp, hp);
BattlerState::new(TSpecies::Mon, hp, hp, stats, vec![TMove { power: 10 }])
}
#[test]
fn forced_action_overrides_chosen_action() {
let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
let mut effects: Vec<EffectState<TForce>> = vec![EffectState {
id: EffectId(50),
host: BattlerRef::PLAYER,
effect_order: 0,
kind: TKind::Toxic { counter: 0 },
}];
let actions = [
BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
];
let mut rng = EngineScriptedRng::new(vec![]);
let provider = TForce;
StackDriver::execute_turn(&provider, &mut state, &mut effects, actions, &mut rng);
assert_eq!(state.opponent_battlers[0].hp, 100, "locked player forced to Nothing");
assert_eq!(state.player_battlers[0].hp, 90, "unlocked opponent's Fight ran");
}
#[test]
fn forced_action_default_is_inert() {
let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
let mut effects: Vec<EffectState<TForce>> = Vec::new();
let actions = [
BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
];
let mut rng = EngineScriptedRng::new(vec![]);
let provider = TForce;
StackDriver::execute_turn(&provider, &mut state, &mut effects, actions, &mut rng);
assert_eq!(state.opponent_battlers[0].hp, 90, "player Fight ran (no lock)");
assert_eq!(state.player_battlers[0].hp, 90, "opponent Fight ran (no lock)");
}
use super::log::TurnEvent;
#[test]
fn logged_turn_is_behaviorally_identical_to_plain() {
let actions = || {
[
BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
]
};
let mut s_plain = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
let mut e_plain: Vec<EffectState<TForce>> = Vec::new();
let mut rng_plain = EngineScriptedRng::new(vec![]);
let r_plain =
StackDriver::execute_turn(&TForce, &mut s_plain, &mut e_plain, actions(), &mut rng_plain);
let mut s_log = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
let mut e_log: Vec<EffectState<TForce>> = Vec::new();
let mut rng_log = EngineScriptedRng::new(vec![]);
let (r_log, log) =
StackDriver::execute_turn_logged(&TForce, &mut s_log, &mut e_log, actions(), &mut rng_log);
assert_eq!(r_plain, r_log, "same StackTurnResult");
assert_eq!(rng_plain.consumed(), rng_log.consumed(), "same rng draw count");
assert_eq!(s_plain.player_battlers[0].hp, s_log.player_battlers[0].hp, "same player hp");
assert_eq!(
s_plain.opponent_battlers[0].hp, s_log.opponent_battlers[0].hp,
"same opponent hp"
);
assert!(!log.is_empty(), "the logged path recorded events");
}
#[test]
fn log_records_move_used_and_damage_both_movers() {
let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
let mut effects: Vec<EffectState<TForce>> = Vec::new();
let actions = [
BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
];
let mut rng = EngineScriptedRng::new(vec![]);
let (_r, log) =
StackDriver::execute_turn_logged(&TForce, &mut state, &mut effects, actions, &mut rng);
let p = BattlerRef::PLAYER;
let o = BattlerRef::OPPONENT;
let ev = &log.events;
assert!(matches!(ev[0], TurnEvent::MoveUsed { actor, .. } if actor == p), "1: player MoveUsed");
assert!(matches!(ev[1], TurnEvent::Damaged { target, amount: 10, .. } if target == o), "2: opponent took 10");
assert!(matches!(ev[2], TurnEvent::MoveUsed { actor, .. } if actor == o), "3: opponent MoveUsed");
assert!(matches!(ev[3], TurnEvent::Damaged { target, amount: 10, .. } if target == p), "4: player took 10");
assert_eq!(ev.len(), 4, "exactly MoveUsed+Damaged ×2 (no crit/miss/status)");
}
#[test]
fn log_records_faint_and_cancels_second() {
let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(10)]);
let mut effects: Vec<EffectState<TForce>> = Vec::new();
let actions = [
BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
];
let mut rng = EngineScriptedRng::new(vec![]);
let (r, log) =
StackDriver::execute_turn_logged(&TForce, &mut state, &mut effects, actions, &mut rng);
assert!(r.second_cancelled, "defender KO'd → second move cancelled");
let o = BattlerRef::OPPONENT;
let ev = &log.events;
assert!(matches!(ev[0], TurnEvent::MoveUsed { .. }), "player MoveUsed");
assert!(matches!(ev[1], TurnEvent::Damaged { target, amount: 10, .. } if target == o), "opponent took 10");
assert!(matches!(ev[2], TurnEvent::Fainted { who } if who == o), "opponent fainted");
assert!(
!ev.iter().any(|e| matches!(e, TurnEvent::MoveUsed { actor, .. } if *actor == o)),
"cancelled opponent never logs a MoveUsed"
);
}
fn log_rich(
ctx: &mut BattleCtx<'_, TRich>,
_relay: RelayVar,
target: BattlerRef,
source: BattlerRef,
_eff: EffectId,
) -> HandlerResult {
ctx.mv.damage = 10; ctx.battler_mut(source).stat_stages.set(TStat::Hp, 1); ctx.battler_mut(target).status = Some(TStatus::Poisoned); let b = ctx.battler_mut(source);
b.hp = (b.hp + 20).min(b.max_hp); HandlerResult::Unchanged
}
static LOG_RICH_EFFECT: Effect<TRich> = Effect {
id: EffectId(1),
kind: EffectType::Move,
hooks: &[EventHook {
event: Event::ModifyDamage,
call: log_rich,
order: 1000,
priority: 0,
sub_order: None,
}],
};
struct TRich;
impl BattleProvider for TRich {
type Monster = ();
type Move = TMove;
type Ability = ();
type Status = TStatus;
type Stat = TStat;
type Species = TSpecies;
type Type = TType;
type Item = ();
fn calculate_damage(&self, _m: &Self::Move, _a: &BattlerState<Self>, _d: &BattlerState<Self>, _r: u8, _c: bool) -> DamageResult {
DamageResult { damage: 0, effectiveness: 1.0, is_miss: false }
}
fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
b.moves.first().cloned().unwrap()
}
fn apply_move_effect(&self, _e: MoveEffect, _u: &mut BattlerState<Self>, _t: &mut BattlerState<Self>) -> EffectResult {
EffectResult::NoEffect
}
fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
}
}
impl EffectProvider for TRich {
type EffectStateKind = TKind;
fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
Some(&LOG_RICH_EFFECT)
}
fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
None
}
fn turn_order_rank(&self, _state: &BattleState<Self>, who: BattlerRef, _action: &Self::Move) -> (i32, i32) {
if who.side == 0 { (0, 0) } else { (1, 0) }
}
}
#[test]
fn log_records_stat_status_and_heal_diffs() {
let mut player = BattlerState::<TRich>::new(TSpecies::Mon, 50, 100, EnumMap::new(), vec![TMove { power: 10 }]);
player.max_hp = 100;
let opponent = BattlerState::<TRich>::new(TSpecies::Mon, 100, 100, EnumMap::new(), vec![TMove { power: 10 }]);
let mut state = BattleState::new(vec![player], vec![opponent]);
let mut effects: Vec<EffectState<TRich>> = Vec::new();
let actions = [
BattleAction::<TRich>::Fight { move_: TMove { power: 10 } },
BattleAction::<TRich>::Nothing,
];
let mut rng = EngineScriptedRng::new(vec![]);
let (_r, log) =
StackDriver::execute_turn_logged(&TRich, &mut state, &mut effects, actions, &mut rng);
let p = BattlerRef::PLAYER;
let o = BattlerRef::OPPONENT;
let ev = &log.events;
assert!(ev.iter().any(|e| matches!(e, TurnEvent::MoveUsed { actor, .. } if *actor == p)), "MoveUsed");
assert!(ev.iter().any(|e| matches!(e, TurnEvent::Damaged { target, amount: 10, .. } if *target == o)), "Damaged target");
assert!(ev.iter().any(|e| matches!(e, TurnEvent::StatusInflicted { target, .. } if *target == o)), "StatusInflicted target");
assert!(ev.iter().any(|e| matches!(e, TurnEvent::StatChanged { target, delta: 1, .. } if *target == p)), "StatChanged actor +1");
assert!(ev.iter().any(|e| matches!(e, TurnEvent::Healed { target, amount: 20, .. } if *target == p)), "Healed actor +20");
}
const MP: u16 = 0;
static COST_DMG_EFFECT: Effect<TCost> = Effect {
id: EffectId(1),
kind: EffectType::Move,
hooks: &[EventHook {
event: Event::ModifyDamage,
call: force_dmg::<TCost>,
order: 1000,
priority: 0,
sub_order: None,
}],
};
struct TCost;
impl BattleProvider for TCost {
type Monster = ();
type Move = TMove;
type Ability = ();
type Status = TStatus;
type Stat = TStat;
type Species = TSpecies;
type Type = TType;
type Item = ();
fn calculate_damage(
&self,
_m: &Self::Move,
_a: &BattlerState<Self>,
_d: &BattlerState<Self>,
_r: u8,
_c: bool,
) -> DamageResult {
DamageResult { damage: 0, effectiveness: 1.0, is_miss: false }
}
fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
b.moves.first().cloned().unwrap()
}
fn apply_move_effect(
&self,
_e: MoveEffect,
_u: &mut BattlerState<Self>,
_t: &mut BattlerState<Self>,
) -> EffectResult {
EffectResult::NoEffect
}
fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
}
fn move_cost(&self, _move_: &Self::Move) -> &[(u16, u16)] {
&[(MP, 4)]
}
}
impl EffectProvider for TCost {
type EffectStateKind = TKind;
fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
Some(&COST_DMG_EFFECT)
}
fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
None
}
fn turn_order_rank(
&self,
_state: &BattleState<Self>,
who: BattlerRef,
_action: &Self::Move,
) -> (i32, i32) {
if who.side == 0 { (0, 0) } else { (1, 0) }
}
}
fn cost_mon(hp: u16, mp: u16) -> BattlerState<TCost> {
let mut stats = EnumMap::new();
stats.set(TStat::Hp, hp);
BattlerState::new(TSpecies::Mon, hp, hp, stats, vec![TMove { power: 10 }])
.with_resource(MP, mp)
}
#[test]
fn cost_gate_pays_and_deducts_when_affordable() {
let mut state = BattleState::new(vec![cost_mon(100, 10)], vec![cost_mon(100, 0)]);
let mut effects: Vec<EffectState<TCost>> = Vec::new();
let actions = [
BattleAction::<TCost>::Fight { move_: TMove { power: 10 } },
BattleAction::<TCost>::Fight { move_: TMove { power: 10 } },
];
let mut rng = EngineScriptedRng::new(vec![]);
StackDriver::execute_turn(&TCost, &mut state, &mut effects, actions, &mut rng);
assert_eq!(state.player_battlers[0].resources.current(MP), Some(6),
"affordable: 10 MP - 4 cost = 6 left");
assert_eq!(state.opponent_battlers[0].hp, 90, "paid move dealt its 10 damage");
assert_eq!(state.player_battlers[0].hp, 100, "insufficient opp move prevented → no damage");
assert_eq!(state.opponent_battlers[0].resources.current(MP), Some(0),
"insufficient: MP unchanged (no deduction on a prevented move)");
}
#[test]
fn cost_gate_prevents_when_unaffordable_and_leaves_mp_unchanged() {
let mut state = BattleState::new(vec![cost_mon(100, 3)], vec![cost_mon(100, 0)]);
let mut effects: Vec<EffectState<TCost>> = Vec::new();
let actions = [
BattleAction::<TCost>::Fight { move_: TMove { power: 10 } },
BattleAction::<TCost>::Fight { move_: TMove { power: 10 } },
];
let mut rng = EngineScriptedRng::new(vec![]);
StackDriver::execute_turn(&TCost, &mut state, &mut effects, actions, &mut rng);
assert_eq!(state.opponent_battlers[0].hp, 100,
"player could not pay → its move was prevented → opp unharmed");
assert_eq!(state.player_battlers[0].resources.current(MP), Some(3),
"prevented move deducts NOTHING → MP unchanged at 3");
}
#[test]
fn cost_gate_consumes_no_rng() {
let mut state = BattleState::new(vec![cost_mon(100, 10)], vec![cost_mon(100, 1)]);
let mut effects: Vec<EffectState<TCost>> = Vec::new();
let actions = [
BattleAction::<TCost>::Fight { move_: TMove { power: 10 } },
BattleAction::<TCost>::Fight { move_: TMove { power: 10 } },
];
let mut rng = EngineScriptedRng::new(vec![]);
StackDriver::execute_turn(&TCost, &mut state, &mut effects, actions, &mut rng);
assert_eq!(rng.consumed(), 0,
"the resource cost check + deduction consume NO randomness");
}
#[test]
fn no_cost_no_resources_is_inert() {
let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
let mut effects: Vec<EffectState<TForce>> = Vec::new();
let actions = [
BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
];
let mut rng = EngineScriptedRng::new(vec![]);
StackDriver::execute_turn(&TForce, &mut state, &mut effects, actions, &mut rng);
assert_eq!(state.opponent_battlers[0].hp, 90, "no-cost move ran (inert gate)");
assert_eq!(state.player_battlers[0].hp, 90, "no-cost move ran (inert gate)");
assert!(state.player_battlers[0].resources.is_empty(), "pool defaulted EMPTY");
}
#[test]
fn speed_tiebreak_draws_only_on_tie() {
let mk = |eo: u64| CollectedHandler::<TProvider> {
order: 1,
priority: 0,
speed: 0,
sub_order: 6,
effect_order: eo,
target: BattlerRef::OPPONENT,
source: BattlerRef::PLAYER,
source_effect: EffectId(eo as u32),
call: add_int::<TProvider>,
};
let mut distinct = vec![mk(0), mk(1)];
let mut rng = ScriptedRng::new(vec![200, 200]);
dispatch::speed_sort_tiebreak(&mut distinct, &mut rng);
assert_eq!(rng.consumed(), 0, "distinct effect_order ⇒ no tie ⇒ no draw");
let mut tied = vec![mk(0), mk(0)];
let mut rng2 = ScriptedRng::new(vec![200]);
dispatch::speed_sort_tiebreak(&mut tied, &mut rng2);
assert_eq!(rng2.consumed(), 1, "one tied pair ⇒ exactly one draw");
}
#[test]
fn comparator_lexical_order() {
use std::cmp::Ordering;
let h = |order, priority, eo| CollectedHandler::<TProvider> {
order,
priority,
speed: 0,
sub_order: 6,
effect_order: eo,
target: BattlerRef::OPPONENT,
source: BattlerRef::PLAYER,
source_effect: EffectId(0),
call: add_int::<TProvider>,
};
assert_eq!(compare(&h(1, 0, 0), &h(2, 0, 0)), Ordering::Less);
assert_eq!(compare(&h(1, 5, 0), &h(1, 1, 0)), Ordering::Less);
assert_eq!(compare(&h(1, 0, 3), &h(1, 0, 9)), Ordering::Less);
assert_eq!(compare(&h(1, 0, 0), &h(1, 0, 0)), Ordering::Equal);
}
}
#[cfg(test)]
mod multi_source_tests {
use super::dispatch::{collect_handlers, run_event, run_event_checked};
use super::tests_support::{
marker, mon, parts, TKind, TProvider, TSpecies, MOCK_VOLATILE,
};
use super::*;
use crate::battle::stack::ctx::EffectHost;
use crate::battle::{BattlerRef, EffectResult};
fn ctx_from<'a>(
state: &'a mut crate::battle::BattleState<TProvider>,
effects: &'a mut Vec<EffectState<TProvider>>,
mv: &'a mut MoveContext,
rng: &'a mut crate::battle::rng::ScriptedRng,
) -> BattleCtx<'a, TProvider> {
BattleCtx { state, effects, mv, rng }
}
#[test]
fn collects_ability_item_field_in_order() {
let provider = TProvider { field_on: true };
let (mut state, mut effects, mut mv, mut rng) =
parts(mon(100, TSpecies::HasBoth), mon(100, TSpecies::Plain));
let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
let mut hs = Vec::new();
collect_handlers(
&ctx,
&provider,
None, Event::DamagingHit,
BattlerRef::OPPONENT, BattlerRef::PLAYER, &mut hs,
);
assert_eq!(hs.len(), 3, "ability + item + field collected");
run_event(&mut ctx, hs, RelayVar::Unit, false);
assert_eq!(marker(&state, BattlerRef::PLAYER), 11, "ability+item fired on host");
assert_eq!(marker(&state, BattlerRef::OPPONENT), 100, "field fired on target");
}
#[test]
fn multi_source_comparator_order() {
let provider = TProvider { field_on: true };
let (mut state, mut effects, mut mv, mut rng) =
parts(mon(100, TSpecies::HasBoth), mon(100, TSpecies::Plain));
effects.push(EffectState {
id: MOCK_VOLATILE.id,
host: BattlerRef::PLAYER,
effect_order: 0,
kind: TKind::Vol,
});
let ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
let mut hs = Vec::new();
collect_handlers(
&ctx,
&provider,
None,
Event::DamagingHit,
BattlerRef::OPPONENT,
BattlerRef::PLAYER,
&mut hs,
);
hs.sort_by(super::dispatch::compare);
let orders: Vec<u32> = hs.iter().map(|h| h.order).collect();
assert_eq!(orders, vec![10, 15, 20, 30], "ability<volatile<item<field");
}
fn ko_target<P: EffectProvider + ?Sized>(
ctx: &mut BattleCtx<'_, P>,
_r: RelayVar,
target: BattlerRef,
_s: BattlerRef,
_e: EffectId,
) -> HandlerResult {
ctx.battler_mut(target).hp = 0; HandlerResult::Unchanged
}
fn touch_target<P: EffectProvider<Stat = super::tests_support::TStat> + ?Sized>(
ctx: &mut BattleCtx<'_, P>,
_r: RelayVar,
target: BattlerRef,
_s: BattlerRef,
_e: EffectId,
) -> HandlerResult {
super::tests_support::mark(ctx, target, 7); HandlerResult::Unchanged
}
#[test]
fn re_check_skips_handler_whose_target_was_koed_midfold() {
let (mut state, mut effects, mut mv, mut rng) =
parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
let hs = vec![
CollectedHandler {
order: 1, priority: 0, speed: 0, sub_order: 6, effect_order: 0,
target: BattlerRef::OPPONENT, source: BattlerRef::PLAYER,
source_effect: EffectId(1), call: ko_target::<TProvider>,
},
CollectedHandler {
order: 2, priority: 0, speed: 0, sub_order: 6, effect_order: 1,
target: BattlerRef::OPPONENT, source: BattlerRef::PLAYER,
source_effect: EffectId(2), call: touch_target::<TProvider>,
},
];
run_event_checked(&mut ctx, hs, RelayVar::Unit, false);
assert_eq!(state.opponent_battlers[0].hp, 0, "first handler KO'd target");
assert_eq!(
marker(&state, BattlerRef::OPPONENT), 0,
"re-check SKIPPED the second handler (dead target)"
);
}
#[test]
fn plain_run_event_does_not_re_check() {
let provider = TProvider::default();
let _ = &provider; let (mut state, mut effects, mut mv, mut rng) =
parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
let hs = vec![
CollectedHandler {
order: 1, priority: 0, speed: 0, sub_order: 6, effect_order: 0,
target: BattlerRef::OPPONENT, source: BattlerRef::PLAYER,
source_effect: EffectId(1), call: ko_target::<TProvider>,
},
CollectedHandler {
order: 2, priority: 0, speed: 0, sub_order: 6, effect_order: 1,
target: BattlerRef::OPPONENT, source: BattlerRef::PLAYER,
source_effect: EffectId(2), call: touch_target::<TProvider>,
},
];
run_event(&mut ctx, hs, RelayVar::Unit, false);
assert_eq!(
marker(&state, BattlerRef::OPPONENT), 7,
"plain run_event fired the second handler (no re-check) — slice contract"
);
}
fn remove_other_effect<P: EffectProvider + ?Sized>(
ctx: &mut BattleCtx<'_, P>,
_r: RelayVar,
_t: BattlerRef,
_s: BattlerRef,
_e: EffectId,
) -> HandlerResult {
ctx.effects.retain(|e| e.id != MOCK_VOLATILE.id);
HandlerResult::Unchanged
}
#[test]
fn handler_removing_effect_midfold_is_snapshot_safe() {
let provider = TProvider::default();
let _ = &provider; let (mut state, mut effects, mut mv, mut rng) =
parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
effects.push(EffectState {
id: MOCK_VOLATILE.id,
host: BattlerRef::PLAYER,
effect_order: 0,
kind: TKind::Vol,
});
let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
let hs = vec![
CollectedHandler {
order: 1, priority: 0, speed: 0, sub_order: 6, effect_order: 0,
target: BattlerRef::PLAYER, source: BattlerRef::PLAYER,
source_effect: EffectId(1), call: remove_other_effect::<TProvider>,
},
CollectedHandler {
order: 2, priority: 0, speed: 0, sub_order: 6, effect_order: 1,
target: BattlerRef::PLAYER, source: BattlerRef::PLAYER,
source_effect: EffectId(2), call: touch_target::<TProvider>,
},
];
run_event_checked(&mut ctx, hs, RelayVar::Unit, false);
assert!(effects.is_empty(), "volatile removed mid-fold");
assert_eq!(
marker(&state, BattlerRef::PLAYER), 7,
"second handler still fired from the owned snapshot (no invalidation)"
);
}
#[test]
fn effect_host_routes_field_and_battler() {
let es = EffectState::<TProvider> {
id: EffectId(1),
host: BattlerRef::PLAYER,
effect_order: 0,
kind: TKind::None,
};
assert_eq!(es.host_scope(), EffectHost::Battler(BattlerRef::PLAYER));
let h: EffectHost = BattlerRef::OPPONENT.into();
assert_eq!(h, EffectHost::Battler(BattlerRef::OPPONENT));
assert!(h == BattlerRef::OPPONENT, "EffectHost == BattlerRef");
assert!(BattlerRef::OPPONENT == h, "BattlerRef == EffectHost");
assert_ne!(EffectHost::Side(1), EffectHost::Field);
let provider = TProvider { field_on: true };
let (mut state, mut effects, mut mv, mut rng) =
parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
let mut hs = Vec::new();
collect_handlers(
&ctx, &provider, None, Event::DamagingHit,
BattlerRef::PLAYER, BattlerRef::PLAYER, &mut hs,
);
assert_eq!(hs.len(), 1, "only the field effect (no ability/item)");
run_event_checked(&mut ctx, hs, RelayVar::Unit, false);
assert_eq!(marker(&state, BattlerRef::PLAYER), 100, "field residual routed");
}
#[test]
fn event_with_no_subscribers_is_noop() {
let provider = TProvider::default(); let (mut state, mut effects, mut mv, mut rng) =
parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
let ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
let mut hs = Vec::new();
collect_handlers(
&ctx, &provider, None, Event::DamagingHit,
BattlerRef::OPPONENT, BattlerRef::PLAYER, &mut hs,
);
assert!(hs.is_empty(), "no live source ⇒ no handlers ⇒ no-op");
drop(ctx);
assert_eq!(marker(&state, BattlerRef::PLAYER), 0);
assert_eq!(marker(&state, BattlerRef::OPPONENT), 0);
let _ = EffectResult::NoEffect; }
#[test]
fn default_resolvers_reduce_to_single_source_identity() {
use super::dispatch::collect_from_effect;
static SRC: Effect<TProvider> = Effect {
id: EffectId(0x99),
kind: EffectType::Move,
hooks: &[EventHook {
event: Event::DamagingHit,
call: super::tests_support::mock_src_hit::<TProvider>,
order: 5,
priority: 0,
sub_order: None,
}],
};
let provider = TProvider::default();
let (mut state, mut effects, mut mv, mut rng) =
parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
let ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
let mut single = Vec::new();
collect_from_effect(
&ctx, &SRC, Event::DamagingHit,
BattlerRef::OPPONENT, BattlerRef::PLAYER, &mut single,
);
let mut multi = Vec::new();
collect_handlers(
&ctx, &provider, Some(&SRC), Event::DamagingHit,
BattlerRef::OPPONENT, BattlerRef::PLAYER, &mut multi,
);
assert_eq!(single.len(), 1);
assert_eq!(multi.len(), single.len(), "identity: same count");
for (a, b) in single.iter().zip(multi.iter()) {
assert_eq!(a.order, b.order);
assert_eq!(a.priority, b.priority);
assert_eq!(a.speed, b.speed);
assert_eq!(a.sub_order, b.sub_order);
assert_eq!(a.effect_order, b.effect_order);
assert_eq!(a.target, b.target);
assert_eq!(a.source, b.source);
assert_eq!(a.source_effect, b.source_effect);
}
}
}
#[cfg(test)]
mod effectiveness_fold_tests {
use super::*;
use crate::battle::rng::ScriptedRng as EngineScriptedRng;
use crate::battle::stack::ctx::{BattleCtx, EffectProvider, EffectState};
use crate::battle::stack::event::{
Effect, EffectId, EffectType, Event, EventHook, HandlerResult, RelayVar,
};
use crate::battle::{
BattleAction, BattleProvider, BattleState, BattlerRef, BattlerState, DamageResult,
EffectResult, EnumMap, MoveEffect,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EStat {
Hp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
enum EStatus {
Poisoned,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
enum EType {
N,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ESpecies {
NoSub,
Double,
Half,
Zero,
}
#[derive(Debug, Clone, PartialEq)]
struct EMove {
power: u8,
}
#[derive(Clone)]
#[allow(dead_code)]
enum EKind {
None,
}
struct EProvider;
impl BattleProvider for EProvider {
type Monster = ();
type Move = EMove;
type Ability = ();
type Status = EStatus;
type Stat = EStat;
type Species = ESpecies;
type Type = EType;
type Item = ();
fn calculate_damage(
&self,
_m: &Self::Move,
_a: &BattlerState<Self>,
_d: &BattlerState<Self>,
_r: u8,
_c: bool,
) -> DamageResult {
DamageResult { damage: 0, effectiveness: 1.0, is_miss: false }
}
fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
b.moves.first().cloned().unwrap()
}
fn apply_move_effect(
&self,
_e: MoveEffect,
_u: &mut BattlerState<Self>,
_t: &mut BattlerState<Self>,
) -> EffectResult {
EffectResult::NoEffect
}
fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
}
}
impl EffectProvider for EProvider {
type EffectStateKind = EKind;
fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
Some(&EFF_MOVE_EFFECT)
}
fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
None
}
fn turn_order_rank(
&self,
_state: &BattleState<Self>,
who: BattlerRef,
_action: &Self::Move,
) -> (i32, i32) {
if who.side == 0 { (0, 0) } else { (1, 0) }
}
}
fn eff_modify_damage(
ctx: &mut BattleCtx<'_, EProvider>,
_r: RelayVar,
_t: BattlerRef,
_s: BattlerRef,
_e: EffectId,
) -> HandlerResult {
ctx.mv.damage = 80;
HandlerResult::Unchanged
}
fn eff_effectiveness(
ctx: &mut BattleCtx<'_, EProvider>,
relay: RelayVar,
_target: BattlerRef,
source: BattlerRef,
_e: EffectId,
) -> HandlerResult {
let sp = ctx.battler(source).species;
let (num, den) = match sp {
ESpecies::Double => (2u32, 1u32),
ESpecies::Half => (1, 2),
ESpecies::Zero => (0, 1),
ESpecies::NoSub => (1, 1), };
HandlerResult::Set(relay.scale(num, den))
}
static EFF_MOVE_EFFECT: Effect<EProvider> = Effect {
id: EffectId(1),
kind: EffectType::Move,
hooks: &[
EventHook {
event: Event::ModifyDamage,
call: eff_modify_damage,
order: 1000,
priority: 0,
sub_order: None,
},
EventHook {
event: Event::Effectiveness,
call: eff_effectiveness,
order: 1000,
priority: 0,
sub_order: None,
},
],
};
struct EProviderNoSub;
impl BattleProvider for EProviderNoSub {
type Monster = ();
type Move = EMove;
type Ability = ();
type Status = EStatus;
type Stat = EStat;
type Species = ESpecies;
type Type = EType;
type Item = ();
fn calculate_damage(
&self,
_m: &Self::Move,
_a: &BattlerState<Self>,
_d: &BattlerState<Self>,
_r: u8,
_c: bool,
) -> DamageResult {
DamageResult { damage: 0, effectiveness: 1.0, is_miss: false }
}
fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
b.moves.first().cloned().unwrap()
}
fn apply_move_effect(
&self,
_e: MoveEffect,
_u: &mut BattlerState<Self>,
_t: &mut BattlerState<Self>,
) -> EffectResult {
EffectResult::NoEffect
}
fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
}
}
impl EffectProvider for EProviderNoSub {
type EffectStateKind = EKind;
fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
Some(&EFF_MOVE_NO_SUB_NS)
}
fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
None
}
fn turn_order_rank(
&self,
_state: &BattleState<Self>,
who: BattlerRef,
_action: &Self::Move,
) -> (i32, i32) {
if who.side == 0 { (0, 0) } else { (1, 0) }
}
}
fn eff_modify_damage_ns(
ctx: &mut BattleCtx<'_, EProviderNoSub>,
_r: RelayVar,
_t: BattlerRef,
_s: BattlerRef,
_e: EffectId,
) -> HandlerResult {
ctx.mv.damage = 80;
HandlerResult::Unchanged
}
static EFF_MOVE_NO_SUB_NS: Effect<EProviderNoSub> = Effect {
id: EffectId(2),
kind: EffectType::Move,
hooks: &[EventHook {
event: Event::ModifyDamage,
call: eff_modify_damage_ns,
order: 1000,
priority: 0,
sub_order: None,
}],
};
fn mon_eff(species: ESpecies) -> BattlerState<EProvider> {
let mut stats = EnumMap::new();
stats.set(EStat::Hp, 200);
BattlerState::new(species, 200, 200, stats, vec![EMove { power: 80 }])
}
fn mon_eff_ns(species: ESpecies) -> BattlerState<EProviderNoSub> {
let mut stats = EnumMap::new();
stats.set(EStat::Hp, 200);
BattlerState::new(species, 200, 200, stats, vec![EMove { power: 80 }])
}
fn run_player_turn(attacker: ESpecies) -> u16 {
let mut state = BattleState::new(vec![mon_eff(attacker)], vec![mon_eff(ESpecies::NoSub)]);
let mut effects: Vec<EffectState<EProvider>> = Vec::new();
let actions = [
BattleAction::<EProvider>::Fight { move_: EMove { power: 80 } },
BattleAction::<EProvider>::Fight { move_: EMove { power: 80 } },
];
let mut rng = EngineScriptedRng::new(vec![]);
StackDriver::execute_turn(&EProvider, &mut state, &mut effects, actions, &mut rng);
200 - state.opponent_battlers[0].hp
}
#[test]
fn effectiveness_fold_is_inert_with_no_subscriber() {
let mut state = BattleState::new(
vec![mon_eff_ns(ESpecies::NoSub)],
vec![mon_eff_ns(ESpecies::NoSub)],
);
let mut effects: Vec<EffectState<EProviderNoSub>> = Vec::new();
let actions = [
BattleAction::<EProviderNoSub>::Fight { move_: EMove { power: 80 } },
BattleAction::<EProviderNoSub>::Fight { move_: EMove { power: 80 } },
];
let mut rng = EngineScriptedRng::new(vec![]);
StackDriver::execute_turn(&EProviderNoSub, &mut state, &mut effects, actions, &mut rng);
let dmg = 200 - state.opponent_battlers[0].hp;
assert_eq!(dmg, 80, "no Effectiveness subscriber ⇒ damage unchanged (inert 1×)");
}
#[test]
fn effectiveness_subscriber_scale_2_1_doubles() {
assert_eq!(run_player_turn(ESpecies::Double), 160, "scale(2,1): 80 → 160");
}
#[test]
fn effectiveness_subscriber_scale_1_2_halves() {
assert_eq!(run_player_turn(ESpecies::Half), 40, "scale(1,2): 80 → 40");
}
#[test]
fn effectiveness_subscriber_scale_0_1_zeroes() {
assert_eq!(run_player_turn(ESpecies::Zero), 0, "scale(0,1): 80 → 0 (immune)");
}
}