use super::rng::BattleRng;
use super::{BattleAction, BattleProvider, BattleState, BattlerRef};
pub trait BattleAiProvider: BattleProvider {
fn score_action(
&self,
st: &BattleState<Self>,
me: BattlerRef,
action: &BattleAction<Self>,
rng: &mut dyn BattleRng,
) -> i32
where
Self: Sized;
fn legal_actions(&self, st: &BattleState<Self>, me: BattlerRef) -> Vec<BattleAction<Self>>
where
Self: Sized;
}
pub struct BattleAi;
impl BattleAi {
pub fn choose<P: BattleAiProvider>(
provider: &P,
st: &BattleState<P>,
me: BattlerRef,
rng: &mut dyn BattleRng,
) -> BattleAction<P> {
let actions = provider.legal_actions(st, me);
if actions.is_empty() {
return BattleAction::Nothing;
}
let mut best_score = i32::MIN;
let mut scores = Vec::with_capacity(actions.len());
for action in &actions {
let s = provider.score_action(st, me, action, rng);
if s > best_score {
best_score = s;
}
scores.push(s);
}
let tied: Vec<usize> = scores
.iter()
.enumerate()
.filter(|(_, &s)| s == best_score)
.map(|(i, _)| i)
.collect();
let chosen = if tied.len() == 1 {
tied[0]
} else {
let pick = rng.range(tied.len() as u32) as usize;
tied[pick.min(tied.len() - 1)]
};
actions[chosen].clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::battle::rng::ScriptedRng;
use crate::battle::{
BattleProvider, BattlerState, DamageResult, EffectResult, EnumMap, MoveEffect,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AStat {
Hp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AStatus {
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AType {
Normal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ASpecies {
Mon,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct AMove {
weight: i32,
}
struct AiMock;
impl BattleProvider for AiMock {
type Monster = ();
type Move = AMove;
type Ability = ();
type Status = AStatus;
type Stat = AStat;
type Species = ASpecies;
type Type = AType;
type Item = ();
fn calculate_damage(
&self,
_move_: &Self::Move,
_attacker: &BattlerState<Self>,
_defender: &BattlerState<Self>,
_random: u8,
_is_critical: bool,
) -> DamageResult {
DamageResult {
damage: 0,
effectiveness: 1.0,
is_miss: false,
}
}
fn select_move(
&self,
battler: &BattlerState<Self>,
_state: &BattleState<Self>,
) -> Self::Move {
battler.moves.first().copied().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())
}
}
impl BattleAiProvider for AiMock {
fn score_action(
&self,
_st: &BattleState<Self>,
_me: BattlerRef,
action: &BattleAction<Self>,
_rng: &mut dyn BattleRng,
) -> i32 {
match action {
BattleAction::Fight { move_ } => move_.weight,
_ => i32::MIN,
}
}
fn legal_actions(
&self,
st: &BattleState<Self>,
me: BattlerRef,
) -> Vec<BattleAction<Self>> {
let party = if me.side == 0 {
&st.player_battlers
} else {
&st.opponent_battlers
};
party[me.slot as usize]
.moves
.iter()
.map(|m| BattleAction::Fight { move_: *m })
.collect()
}
}
fn state_with_moves(weights: &[i32]) -> BattleState<AiMock> {
let moves: Vec<AMove> = weights.iter().map(|&w| AMove { weight: w }).collect();
let opp = BattlerState::new(ASpecies::Mon, 100, 100, EnumMap::new(), vec![AMove { weight: 0 }]);
let me = BattlerState::new(ASpecies::Mon, 100, 100, EnumMap::new(), moves);
BattleState::new(vec![opp], vec![me])
}
fn weight_of(action: &BattleAction<AiMock>) -> i32 {
match action {
BattleAction::Fight { move_ } => move_.weight,
_ => panic!("expected Fight"),
}
}
#[test]
fn choose_picks_highest_scored_action() {
let provider = AiMock;
let st = state_with_moves(&[1, 9, 4, 2]);
let mut rng = ScriptedRng::new(vec![0, 0, 0, 0]);
let chosen = BattleAi::choose(&provider, &st, BattlerRef::OPPONENT, &mut rng);
assert_eq!(weight_of(&chosen), 9, "highest-weighted move wins");
}
#[test]
fn unique_best_draws_no_rng() {
let provider = AiMock;
let st = state_with_moves(&[1, 9, 4, 2]);
let mut rng = ScriptedRng::new(vec![7, 7, 7]);
let _ = BattleAi::choose(&provider, &st, BattlerRef::OPPONENT, &mut rng);
assert_eq!(rng.consumed(), 0, "no tie → no rng draw");
}
#[test]
fn ties_resolved_deterministically_via_rng() {
let provider = AiMock;
let st = state_with_moves(&[5, 5, 5]);
let mut rng0 = ScriptedRng::new(vec![0]);
let c0 = BattleAi::choose(&provider, &st, BattlerRef::OPPONENT, &mut rng0);
assert_eq!(rng0.consumed(), 1, "tie draws exactly one byte");
let mut rng1 = ScriptedRng::new(vec![1]);
let c1 = BattleAi::choose(&provider, &st, BattlerRef::OPPONENT, &mut rng1);
let mut rng2 = ScriptedRng::new(vec![2]);
let c2 = BattleAi::choose(&provider, &st, BattlerRef::OPPONENT, &mut rng2);
assert_eq!(weight_of(&c0), 5);
assert_eq!(weight_of(&c1), 5);
assert_eq!(weight_of(&c2), 5);
let mut rng1b = ScriptedRng::new(vec![1]);
let c1b = BattleAi::choose(&provider, &st, BattlerRef::OPPONENT, &mut rng1b);
assert_eq!(weight_of(&c1), weight_of(&c1b), "deterministic under same rng");
}
#[test]
fn empty_legal_actions_falls_back_to_nothing() {
let provider = AiMock;
let st = state_with_moves(&[]); let mut rng = ScriptedRng::new(vec![0]);
let chosen = BattleAi::choose(&provider, &st, BattlerRef::OPPONENT, &mut rng);
assert!(matches!(chosen, BattleAction::Nothing));
}
}