use alloc::{
collections::VecDeque,
vec::Vec,
};
use anyhow::Result;
use battler_prng::{
PseudoRandomNumberGenerator,
rand_util,
};
use crate::{
WrapOptionError,
battle::{
Action,
BeforeMoveAction,
BeforeMoveActionInput,
Context,
CoreBattle,
CoreBattleEngineSpeedSortTieResolution,
Mon,
MonHandle,
MoveAction,
SpeedOrderable,
compare_priority,
speed_sort,
},
effect::EffectHandle,
};
#[derive(Clone)]
pub struct BattleQueue {
actions: VecDeque<Action>,
next_added_action_id: usize,
}
impl BattleQueue {
pub fn new() -> Self {
Self {
actions: VecDeque::new(),
next_added_action_id: 0,
}
}
pub fn add_action(context: &mut Context, action: Action) -> Result<usize> {
let (actions, id) = Self::resolve_action(context, action)?;
context.battle_mut().queue.actions.extend(actions);
Ok(id)
}
pub fn add_actions<I>(context: &mut Context, actions: I) -> Result<()>
where
I: Iterator<Item = Action>,
{
for action in actions {
Self::add_action(context, action)?;
}
Ok(())
}
fn sub_actions(action: &Action) -> Vec<Action> {
match action {
Action::Move(action) => {
let mut actions = Vec::from_iter([
Action::BeforeTurnMove(BeforeMoveAction::new(BeforeMoveActionInput {
id: action.id.clone(),
active_move_handle: None,
mon: action.mon_action.mon,
})),
Action::PriorityChargeMove(BeforeMoveAction::new(BeforeMoveActionInput {
id: action.id.clone(),
active_move_handle: None,
mon: action.mon_action.mon,
})),
]);
if action.mega {
actions.push(Action::MegaEvo(action.mon_action.clone()));
}
if action.ultra {
actions.push(Action::UltraBurst(action.mon_action.clone()));
}
if action.dyna {
actions.push(Action::Dynamax(action.mon_action.clone()));
}
if action.tera {
actions.push(Action::Terastallize(action.mon_action.clone()));
}
actions
}
Action::SwitchEvents(action) => {
Vec::from_iter([Action::BeforeSwitchEvents(action.clone())])
}
_ => Vec::new(),
}
}
fn resolve_action(context: &mut Context, action: Action) -> Result<(Vec<Action>, usize)> {
match action {
Action::Pass => Ok((Vec::new(), usize::MAX)),
_ => {
let id = context.battle().queue.next_added_action_id;
context.battle_mut().queue.next_added_action_id += 1;
let mut actions = Self::sub_actions(&action);
actions.insert(0, action);
let mut active_move_handle = None;
for action in &mut actions {
if let Some(action_id) = action.action_id_mut() {
*action_id = id;
}
CoreBattle::resolve_action(context, action)?;
match action {
Action::Move(action) => {
active_move_handle = Some(action.active_move_handle.wrap_expectation(
"expected active move after resolving move action",
)?);
}
Action::BeforeTurnMove(action) | Action::PriorityChargeMove(action) => {
action.active_move_handle = Some(active_move_handle.wrap_expectation(
"expected active move handle for move sub-action",
)?);
}
_ => (),
}
}
Ok((actions, id))
}
}
}
pub fn push(&mut self, action: Action) {
self.actions.push_back(action);
}
pub fn pop_front(&mut self) -> Option<Action> {
self.actions.pop_front()
}
pub fn peek(&self) -> Option<&Action> {
self.actions.front()
}
pub fn is_empty(&self) -> bool {
self.actions.is_empty()
}
pub fn find_action_mut<F>(&mut self, matcher: F) -> Option<&mut Action>
where
F: Fn(&Action) -> bool,
{
self.actions.iter_mut().find(|action| matcher(action))
}
pub fn sort(context: &mut Context) {
let prng = context.battle_mut().prng.as_mut();
let prng = unsafe {
core::mem::transmute::<
&mut dyn PseudoRandomNumberGenerator,
&mut dyn PseudoRandomNumberGenerator,
>(prng)
};
let tie_resolution = context.battle().engine_options.speed_sort_tie_resolution;
context
.battle_mut()
.queue
.sort_internal(prng, tie_resolution)
}
fn sort_internal(
&mut self,
prng: &mut dyn PseudoRandomNumberGenerator,
tie_resolution: CoreBattleEngineSpeedSortTieResolution,
) {
let actions = self.actions.make_contiguous();
speed_sort(actions, prng, tie_resolution);
}
pub fn any_move_this_turn(&self) -> bool {
self.actions.iter().any(|action| match action {
Action::Move(_) => true,
_ => false,
})
}
pub fn pending_move_this_turn(&self, mon: MonHandle) -> Option<MoveAction> {
self.actions
.iter()
.find_map(|action| match action {
Action::Move(move_action) => {
(move_action.mon_action.mon == mon).then_some(move_action)
}
_ => None,
})
.cloned()
}
pub fn prioritize_move(
context: &mut Context,
mon: MonHandle,
action_id: Option<usize>,
source_effect: Option<EffectHandle>,
source: Option<MonHandle>,
) -> Result<()> {
let index = context
.battle_mut()
.queue
.actions
.iter()
.position(|action| match action {
Action::Move(action) => {
action.mon_action.mon == mon
&& action_id.is_none_or(|action_id| action.action_id == action_id)
}
_ => false,
});
if let Some(index) = index {
let mut action = context.battle_mut().queue.actions.remove(index).unwrap();
if let Action::Move(move_action) = &mut action {
move_action.order = Some(6);
if let Some(active_move) = move_action.active_move_handle {
if let Some(source_effect) = source_effect {
context
.active_move_mut(active_move)?
.effect_state
.set_source_effect(source_effect);
}
if let Some(source) = source {
context
.active_move_mut(active_move)?
.effect_state
.set_source(source);
}
}
}
context.battle_mut().queue.actions.push_front(action);
}
Ok(())
}
pub fn deprioritize_move(
context: &mut Context,
mon: MonHandle,
action_id: Option<usize>,
) -> Result<()> {
const DEPRIORITIZED_MOVE_ORDER: u32 = 201;
let index = context
.battle_mut()
.queue
.actions
.iter()
.position(|action| match action {
Action::Move(action) => {
action.mon_action.mon == mon
&& action_id.is_none_or(|action_id| action.action_id == action_id)
}
_ => false,
});
if let Some(index) = index {
let mut action = context.battle_mut().queue.actions.remove(index).unwrap();
if let Action::Move(move_action) = &mut action {
move_action.order = Some(DEPRIORITIZED_MOVE_ORDER);
}
let insert_index = context
.battle_mut()
.queue
.actions
.iter()
.position(|action| action.order() > DEPRIORITIZED_MOVE_ORDER)
.unwrap_or(context.battle_mut().queue.actions.len());
context
.battle_mut()
.queue
.actions
.insert(insert_index, action);
}
Ok(())
}
pub fn pending_move_actions(&self) -> Vec<&MoveAction> {
self.actions
.iter()
.filter_map(|action| match action {
Action::Move(action) => Some(action),
_ => None,
})
.collect()
}
pub fn cancel_action(&mut self, mon: MonHandle) -> bool {
let before = self.actions.len();
let mut actions = VecDeque::new();
core::mem::swap(&mut actions, &mut self.actions);
actions = actions
.into_iter()
.filter_map(|mut action| {
(!action
.mon_action_mut()
.is_some_and(|action| action.mon == mon))
.then_some(action)
})
.collect();
core::mem::swap(&mut actions, &mut self.actions);
let after = self.actions.len();
before > after
}
pub fn cancel_move(&mut self, mon: MonHandle) -> bool {
let before = self.actions.len();
let mut actions = VecDeque::new();
core::mem::swap(&mut actions, &mut self.actions);
actions = actions
.into_iter()
.filter(|action| {
if let Action::Move(action) = action {
action.mon_action.mon != mon
} else {
true
}
})
.collect();
core::mem::swap(&mut actions, &mut self.actions);
let after = self.actions.len();
before > after
}
pub fn insert_action_into_sorted_position(
context: &mut Context,
action: Action,
) -> Result<usize> {
let (actions, id) = Self::resolve_action(context, action)?;
for action in actions {
Self::insert_resolved_action_into_sorted_position(context, action)?;
}
Ok(id)
}
fn insert_resolved_action_into_sorted_position(
context: &mut Context,
action: Action,
) -> Result<()> {
let prng = context.battle_mut().prng.as_mut();
let prng = unsafe {
core::mem::transmute::<
&mut dyn PseudoRandomNumberGenerator,
&mut dyn PseudoRandomNumberGenerator,
>(prng)
};
let tie_resolution = context.battle().engine_options.speed_sort_tie_resolution;
context
.battle_mut()
.queue
.insert_resolved_action_into_sorted_position_internal(action, prng, tie_resolution);
Ok(())
}
fn insert_resolved_action_into_sorted_position_internal(
&mut self,
action: Action,
prng: &mut dyn PseudoRandomNumberGenerator,
tie_resolution: CoreBattleEngineSpeedSortTieResolution,
) {
let mut min = None;
let mut max = None;
for (i, existing) in self.actions.iter().enumerate() {
let order = compare_priority(&action, existing);
if order.is_le() && min.is_none() {
min = Some(i);
}
if order.is_lt() && max.is_none() {
max = Some(i);
break;
}
}
match min {
Some(min) => {
let max = max.unwrap_or(self.actions.len());
if min == max {
self.actions.insert(min, action);
} else {
match tie_resolution {
CoreBattleEngineSpeedSortTieResolution::Keep => {
self.actions.insert(max, action)
}
CoreBattleEngineSpeedSortTieResolution::Reverse => {
self.actions.insert(min, action)
}
CoreBattleEngineSpeedSortTieResolution::Random => self.actions.insert(
rand_util::range(prng, min as u64, max as u64 + 1) as usize,
action,
),
}
}
}
None => {
self.actions.push_back(action);
}
}
}
pub fn update_mon_speeds(context: &mut Context) -> Result<()> {
let mut actions = VecDeque::new();
core::mem::swap(&mut actions, &mut context.battle_mut().queue.actions);
for action in &mut actions {
if let Some(action) = action.mon_action_mut() {
action.speed = Mon::action_speed(&mut context.mon_context(action.mon)?)? as u32;
}
}
core::mem::swap(&mut actions, &mut context.battle_mut().queue.actions);
Ok(())
}
}
#[cfg(test)]
mod queue_test {
use alloc::{
borrow::ToOwned,
format,
string::String,
vec,
vec::Vec,
};
use battler_data::Id;
use battler_prng::RealPseudoRandomNumberGenerator;
use crate::battle::{
Action,
BattleQueue,
CoreBattleEngineSpeedSortTieResolution,
ExperienceAction,
MonAction,
MonHandle,
MoveAction,
SwitchAction,
TeamAction,
};
fn team_action(mon: MonHandle, priority: i32) -> Action {
Action::Team(TeamAction {
mon_action: MonAction { mon, speed: 0 },
index: 0,
priority,
})
}
fn switch_action(switching_out: MonHandle, instant: bool, speed: u32) -> Action {
Action::Switch(SwitchAction {
instant,
mon_action: MonAction {
mon: switching_out,
speed,
},
switching_out,
position: 0,
})
}
fn move_action(id: Id, priority: i32, speed: u32, sub_priority: i32) -> Action {
Action::Move(MoveAction {
action_id: usize::MAX,
id,
upgraded_id: None,
mon_action: MonAction {
mon: MonHandle::from(0),
speed,
},
target: None,
original_target: None,
mega: false,
z_move: false,
ultra: false,
dyna: false,
tera: false,
priority,
sub_priority,
order: None,
active_move_handle: None,
})
}
fn mega_evo_action(mon: MonHandle, speed: u32) -> Action {
Action::MegaEvo(MonAction { mon, speed })
}
fn experience_action(mon: MonHandle, exp: u32) -> Action {
Action::Experience(ExperienceAction {
mon,
player_index: 0,
mon_index: 0,
active: true,
exp,
})
}
fn sort(queue: &mut BattleQueue, seed: Option<u64>) {
let mut prng = RealPseudoRandomNumberGenerator::new(seed);
queue.actions.make_contiguous();
queue.sort_internal(&mut prng, CoreBattleEngineSpeedSortTieResolution::Random);
}
fn insert_resolved_action_into_sorted_position(
queue: &mut BattleQueue,
action: Action,
seed: Option<u64>,
) {
let mut prng = RealPseudoRandomNumberGenerator::new(seed);
queue.insert_resolved_action_into_sorted_position_internal(
action,
&mut prng,
CoreBattleEngineSpeedSortTieResolution::Random,
);
}
fn battle_queue_actions_to_string_for_test(queue: &BattleQueue) -> Vec<String> {
queue
.actions
.iter()
.map(|action| match action {
Action::Start => "start".to_owned(),
Action::End(_) => "end".to_owned(),
Action::Pass => "pass".to_owned(),
Action::BeforeTurn => "beforeturn".to_owned(),
Action::Residual => "residual".to_owned(),
Action::Team(action) => format!("team {}", action.mon_action.mon),
Action::Switch(action) => format!("switch {}", action.mon_action.mon),
Action::BeforeSwitchEvents(action) => {
format!("beforeswitchevents {}", action.mon_action.mon)
}
Action::SwitchEvents(action) => format!("switchevents {}", action.mon_action.mon),
Action::Move(action) => format!("move {}", action.id),
Action::BeforeTurnMove(action) => {
format!("beforeturnmove {}", action.mon_action.mon)
}
Action::PriorityChargeMove(action) => {
format!("prioritychargemove {}", action.mon_action.mon)
}
Action::MegaEvo(action) => format!("megaevo {}", action.mon),
Action::UltraBurst(action) => format!("ultra {}", action.mon),
Action::Dynamax(action) => format!("dynamax {}", action.mon),
Action::Terastallize(action) => format!("tera {}", action.mon),
Action::Experience(action) => format!("experience {}", action.mon),
Action::LevelUp(action) => format!("levelup {}", action.mon),
Action::LearnMove(action) => format!("learnmove {}", action.mon),
Action::Escape(action) => format!("escape {}", action.mon_action.mon),
Action::Forfeit(action) => format!("forfeit {}", action.player),
Action::Item(action) => format!("item {}", action.item),
Action::Shift(action) => {
format!("shift {} {}", action.mon_action.mon, action.position)
}
Action::Select(action) => {
format!("select {}", action.mon_action.mon)
}
Action::OutsideEffect(action) => format!("outsideeffect {}", action.order),
})
.collect()
}
#[test]
fn sorts_actions_with_no_ties() {
let mut queue = BattleQueue::new();
queue.push(Action::Start);
queue.push(Action::BeforeTurn);
queue.push(Action::Residual);
queue.push(Action::Pass);
queue.push(move_action(Id::from("m1"), 0, 100, 0));
queue.push(move_action(Id::from("m2"), 5, 100, 0));
queue.push(move_action(Id::from("m3"), 0, 200, 0));
queue.push(move_action(Id::from("m4"), -1, 400, 0));
queue.push(switch_action(MonHandle::from(1), false, 10));
queue.push(switch_action(MonHandle::from(7), true, 10));
queue.push(switch_action(MonHandle::from(2), false, 20));
queue.push(team_action(MonHandle::from(3), -5));
queue.push(team_action(MonHandle::from(4), -1));
queue.push(mega_evo_action(MonHandle::from(5), 10));
queue.push(mega_evo_action(MonHandle::from(6), 20));
sort(&mut queue, None);
pretty_assertions::assert_eq!(
battle_queue_actions_to_string_for_test(&queue),
vec![
"team 4",
"team 3",
"start",
"switch 7",
"beforeturn",
"switch 2",
"switch 1",
"megaevo 6",
"megaevo 5",
"move m2",
"move m3",
"move m1",
"pass",
"move m4",
"residual",
]
);
}
#[test]
fn sorts_actions_with_random_ties() {
let mut queue = BattleQueue::new();
queue.push(move_action(Id::from("m1"), 0, 100, 0));
queue.push(move_action(Id::from("m2"), 0, 100, 0));
queue.push(move_action(Id::from("m3"), 0, 100, 0));
queue.push(move_action(Id::from("m4"), 1, 100, 0));
queue.push(move_action(Id::from("m5"), 1, 100, 0));
queue.push(move_action(Id::from("m6"), -1, 100, 0));
queue.push(move_action(Id::from("m7"), -1, 100, 0));
queue.push(switch_action(MonHandle::from(1), false, 10));
queue.push(switch_action(MonHandle::from(2), false, 10));
queue.push(mega_evo_action(MonHandle::from(3), 10));
queue.push(mega_evo_action(MonHandle::from(4), 10));
sort(&mut queue, Some(0));
pretty_assertions::assert_eq!(
battle_queue_actions_to_string_for_test(&queue),
vec![
"switch 1",
"switch 2",
"megaevo 3",
"megaevo 4",
"move m5",
"move m4",
"move m1",
"move m2",
"move m3",
"move m6",
"move m7",
]
);
sort(&mut queue, Some(1234567));
pretty_assertions::assert_eq!(
battle_queue_actions_to_string_for_test(&queue),
vec![
"switch 2",
"switch 1",
"megaevo 4",
"megaevo 3",
"move m4",
"move m5",
"move m3",
"move m1",
"move m2",
"move m6",
"move m7",
]
);
sort(&mut queue, Some(987654321));
pretty_assertions::assert_eq!(
battle_queue_actions_to_string_for_test(&queue),
vec![
"switch 2",
"switch 1",
"megaevo 3",
"megaevo 4",
"move m4",
"move m5",
"move m3",
"move m1",
"move m2",
"move m7",
"move m6",
]
);
sort(&mut queue, Some(1902372845324));
pretty_assertions::assert_eq!(
battle_queue_actions_to_string_for_test(&queue),
vec![
"switch 2",
"switch 1",
"megaevo 4",
"megaevo 3",
"move m5",
"move m4",
"move m1",
"move m3",
"move m2",
"move m7",
"move m6",
]
);
sort(&mut queue, Some(897234982374932874));
pretty_assertions::assert_eq!(
battle_queue_actions_to_string_for_test(&queue),
vec![
"switch 1",
"switch 2",
"megaevo 3",
"megaevo 4",
"move m5",
"move m4",
"move m2",
"move m3",
"move m1",
"move m6",
"move m7",
]
);
}
#[test]
fn inserts_action_into_sorted_position_with_random_ties() {
let mut source_queue = BattleQueue::new();
source_queue.push(move_action(Id::from("m1"), 0, 100, 0));
source_queue.push(move_action(Id::from("m2"), 0, 100, 0));
source_queue.push(move_action(Id::from("m3"), 0, 100, 0));
source_queue.push(move_action(Id::from("m4"), 1, 100, 0));
source_queue.push(move_action(Id::from("m5"), 1, 100, 0));
source_queue.push(move_action(Id::from("m6"), -1, 100, 0));
source_queue.push(move_action(Id::from("m7"), -1, 100, 0));
source_queue.push(switch_action(MonHandle::from(1), false, 10));
source_queue.push(switch_action(MonHandle::from(2), false, 10));
source_queue.push(mega_evo_action(MonHandle::from(3), 10));
source_queue.push(mega_evo_action(MonHandle::from(4), 10));
sort(&mut source_queue, Some(0));
pretty_assertions::assert_eq!(
battle_queue_actions_to_string_for_test(&source_queue),
vec![
"switch 1",
"switch 2",
"megaevo 3",
"megaevo 4",
"move m5",
"move m4",
"move m1",
"move m2",
"move m3",
"move m6",
"move m7",
]
);
let mut queue = source_queue.clone();
insert_resolved_action_into_sorted_position(
&mut queue,
move_action(Id::from("m8"), 0, 200, 0),
None,
);
pretty_assertions::assert_eq!(
battle_queue_actions_to_string_for_test(&queue),
vec![
"switch 1",
"switch 2",
"megaevo 3",
"megaevo 4",
"move m5",
"move m4",
"move m8",
"move m1",
"move m2",
"move m3",
"move m6",
"move m7",
]
);
queue = source_queue.clone();
insert_resolved_action_into_sorted_position(
&mut queue,
move_action(Id::from("m8"), 0, 100, 0),
Some(0),
);
pretty_assertions::assert_eq!(
battle_queue_actions_to_string_for_test(&queue),
vec![
"switch 1",
"switch 2",
"megaevo 3",
"megaevo 4",
"move m5",
"move m4",
"move m8",
"move m1",
"move m2",
"move m3",
"move m6",
"move m7",
]
);
queue = source_queue.clone();
insert_resolved_action_into_sorted_position(
&mut queue,
move_action(Id::from("m8"), 0, 100, 0),
Some(1),
);
pretty_assertions::assert_eq!(
battle_queue_actions_to_string_for_test(&queue),
vec![
"switch 1",
"switch 2",
"megaevo 3",
"megaevo 4",
"move m5",
"move m4",
"move m1",
"move m8",
"move m2",
"move m3",
"move m6",
"move m7",
]
);
queue = source_queue.clone();
insert_resolved_action_into_sorted_position(
&mut queue,
move_action(Id::from("m8"), 0, 100, 0),
Some(2),
);
pretty_assertions::assert_eq!(
battle_queue_actions_to_string_for_test(&queue),
vec![
"switch 1",
"switch 2",
"megaevo 3",
"megaevo 4",
"move m5",
"move m4",
"move m1",
"move m2",
"move m8",
"move m3",
"move m6",
"move m7",
]
);
queue = source_queue.clone();
insert_resolved_action_into_sorted_position(
&mut queue,
move_action(Id::from("m8"), 0, 100, 0),
Some(5),
);
pretty_assertions::assert_eq!(
battle_queue_actions_to_string_for_test(&queue),
vec![
"switch 1",
"switch 2",
"megaevo 3",
"megaevo 4",
"move m5",
"move m4",
"move m1",
"move m2",
"move m3",
"move m8",
"move m6",
"move m7",
]
);
}
#[test]
fn finds_existing_action() {
let mut queue = BattleQueue::new();
queue.push(move_action(Id::from("m1"), 0, 100, 0));
queue.push(move_action(Id::from("m2"), 0, 100, 0));
queue.push(experience_action(MonHandle::from(0), 100));
queue.push(experience_action(MonHandle::from(1), 100));
sort(&mut queue, Some(0));
match queue.find_action_mut(|action| match action {
Action::Experience(action) => action.mon == MonHandle::from(0),
_ => false,
}) {
Some(Action::Experience(action)) => action.exp += 200,
_ => assert!(
false,
"find_action_mut did not produce the correct experience action"
),
}
assert_matches::assert_matches!(queue.find_action_mut(|action| match action {
Action::Experience(action) => action.mon == MonHandle::from(0),
_ => false,
}), Some(Action::Experience(action)) => assert_eq!(action.exp, 300));
}
}