aboutsummaryrefslogtreecommitdiff
path: root/azalea/src
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2022-11-12 23:54:05 -0600
committerGitHub <noreply@github.com>2022-11-12 23:54:05 -0600
commit6eee543a3367d38a6f0e9bffb457a2bd76a8f9cc (patch)
treea5e493ccd7ec24293b8d866242c3836146517122 /azalea/src
parentfa57d03627aa20b1df44caed7cb025b6db1d9b53 (diff)
downloadazalea-drasl-6eee543a3367d38a6f0e9bffb457a2bd76a8f9cc.tar.xz
Pathfinder (#25)
Pathfinding is very much not done, but it works enough and I want to get this merged. TODO: fast replanning, goals that aren't a single node, falling moves (it should be able to play the dropper), parkour moves
Diffstat (limited to 'azalea/src')
-rwxr-xr-x[-rw-r--r--]azalea/src/bot.rs35
-rwxr-xr-x[-rw-r--r--]azalea/src/lib.rs84
-rw-r--r--azalea/src/pathfinder/mod.rs208
-rw-r--r--azalea/src/pathfinder/moves.rs191
-rw-r--r--azalea/src/pathfinder/mtdstarlite.rs453
-rwxr-xr-x[-rw-r--r--]azalea/src/prelude.rs2
6 files changed, 924 insertions, 49 deletions
diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs
index 566ab1e7..961f093d 100644..100755
--- a/azalea/src/bot.rs
+++ b/azalea/src/bot.rs
@@ -1,7 +1,8 @@
use crate::{Client, Event};
use async_trait::async_trait;
+use azalea_core::Vec3;
use parking_lot::Mutex;
-use std::sync::Arc;
+use std::{f64::consts::PI, sync::Arc};
#[derive(Default, Clone)]
pub struct Plugin {
@@ -14,20 +15,22 @@ pub struct State {
}
pub trait BotTrait {
- fn jump(&self);
+ fn jump(&mut self);
+ fn look_at(&mut self, pos: &Vec3);
}
impl BotTrait for azalea_client::Client {
/// Queue a jump for the next tick.
- fn jump(&self) {
- let player_lock = self.player.lock();
- let mut dimension_lock = self.dimension.lock();
-
- let mut player_entity = player_lock
- .entity_mut(&mut dimension_lock)
- .expect("Player must exist");
+ fn jump(&mut self) {
+ self.set_jumping(true);
+ let state = self.plugins.get::<Plugin>().unwrap().state.clone();
+ *state.jumping_once.lock() = true;
+ }
- player_entity.jumping = true;
+ /// Turn the bot's head to look at the coordinate in the world.
+ fn look_at(&mut self, pos: &Vec3) {
+ let (y_rot, x_rot) = direction_looking_at(self.entity().pos(), pos);
+ self.set_rotation(y_rot, x_rot);
}
}
@@ -38,10 +41,18 @@ impl crate::Plugin for Plugin {
if *self.state.jumping_once.lock() {
if bot.jumping() {
*self.state.jumping_once.lock() = false;
- } else {
- bot.set_jumping(true);
+ bot.set_jumping(false);
}
}
}
}
}
+
+fn direction_looking_at(current: &Vec3, target: &Vec3) -> (f32, f32) {
+ // borrowed from mineflayer's Bot.lookAt because i didn't want to do math
+ let delta = target - current;
+ let y_rot = (PI - f64::atan2(-delta.x, -delta.z)) * (180.0 / PI);
+ let ground_distance = f64::sqrt(delta.x * delta.x + delta.z * delta.z);
+ let x_rot = f64::atan2(delta.y, ground_distance) * -(180.0 / PI);
+ (y_rot as f32, x_rot as f32)
+}
diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs
index 83ad8d36..89754409 100644..100755
--- a/azalea/src/lib.rs
+++ b/azalea/src/lib.rs
@@ -51,7 +51,7 @@
//! account,
//! address: "localhost",
//! state: State::default(),
-//! plugins: vec![],
+//! plugins: plugins![],
//! handle,
//! })
//! .await
@@ -76,39 +76,15 @@
//! [`azalea_client`]: https://crates.io/crates/azalea-client
mod bot;
+pub mod pathfinder;
pub mod prelude;
-use async_trait::async_trait;
pub use azalea_client::*;
+pub use azalea_core::{BlockPos, Vec3};
use azalea_protocol::ServerAddress;
-use std::future::Future;
+use std::{future::Future, sync::Arc};
use thiserror::Error;
-/// Plugins can keep their own personal state, listen to events, and add new functions to Client.
-#[async_trait]
-pub trait Plugin: Send + Sync + PluginClone + 'static {
- async fn handle(self: Box<Self>, event: Event, bot: Client);
-}
-
-/// An internal trait that allows Plugin to be cloned.
-#[doc(hidden)]
-pub trait PluginClone {
- fn clone_box(&self) -> Box<dyn Plugin>;
-}
-impl<T> PluginClone for T
-where
- T: 'static + Plugin + Clone,
-{
- fn clone_box(&self) -> Box<dyn Plugin> {
- Box::new(self.clone())
- }
-}
-impl Clone for Box<dyn Plugin> {
- fn clone(&self) -> Self {
- self.clone_box()
- }
-}
-
pub type HandleFn<Fut, S> = fn(Client, Event, S) -> Fut;
/// The options that are passed to [`azalea::start`].
@@ -127,9 +103,14 @@ where
pub address: A,
/// The account that's going to join the server.
pub account: Account,
- /// A list of plugins that are going to be used. Plugins are external
- /// crates that add extra functionality to Azalea.
- pub plugins: Vec<Box<dyn Plugin>>,
+ /// The plugins that are going to be used. Plugins are external crates that
+ /// add extra functionality to Azalea. You should use the [`plugins`] macro
+ /// for this field.
+ ///
+ /// ```rust,no_run
+ /// plugins![azalea_pathfinder::Plugin::default()]
+ /// ```
+ pub plugins: Plugins,
/// A struct that contains the data that you want your bot to remember
/// across events.
///
@@ -177,7 +158,7 @@ pub enum Error {
/// account,
/// address: "localhost",
/// state: State::default(),
-/// plugins: vec![Box::new(autoeat::Plugin::default())],
+/// plugins: plugins![azalea_pathfinder::Plugin::default()],
/// handle,
/// }).await;
/// ```
@@ -193,24 +174,53 @@ pub async fn start<
Err(_) => return Err(Error::InvalidAddress),
};
- let (bot, mut rx) = Client::join(&options.account, address).await?;
+ let (mut bot, mut rx) = Client::join(&options.account, address).await?;
+
+ let mut plugins = options.plugins;
+ plugins.add(bot::Plugin::default());
+ plugins.add(pathfinder::Plugin::default());
+ bot.plugins = Arc::new(plugins);
let state = options.state;
- let bot_plugin = bot::Plugin::default();
while let Some(event) = rx.recv().await {
- for plugin in &options.plugins {
- let plugin = plugin.clone();
+ let cloned_plugins = (*bot.plugins).clone();
+ for plugin in cloned_plugins.into_iter() {
tokio::spawn(plugin.handle(event.clone(), bot.clone()));
}
tokio::spawn(bot::Plugin::handle(
- Box::new(bot_plugin.clone()),
+ Box::new(bot.plugins.get::<bot::Plugin>().unwrap().clone()),
+ event.clone(),
+ bot.clone(),
+ ));
+ tokio::spawn(pathfinder::Plugin::handle(
+ Box::new(bot.plugins.get::<pathfinder::Plugin>().unwrap().clone()),
event.clone(),
bot.clone(),
));
+
tokio::spawn((options.handle)(bot.clone(), event.clone(), state.clone()));
}
Ok(())
}
+
+/// A helper macro that generates a [`Plugins`] struct from a list of objects
+/// that implement [`Plugin`].
+///
+/// ```rust,no_run
+/// plugins![azalea_pathfinder::Plugin::default()];
+/// ```
+#[macro_export]
+macro_rules! plugins {
+ ($($plugin:expr),*) => {
+ {
+ let mut plugins = azalea::Plugins::new();
+ $(
+ plugins.add($plugin);
+ )*
+ plugins
+ }
+ };
+}
diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs
new file mode 100644
index 00000000..d62b3e05
--- /dev/null
+++ b/azalea/src/pathfinder/mod.rs
@@ -0,0 +1,208 @@
+mod moves;
+mod mtdstarlite;
+
+use crate::{prelude::*, SprintDirection, WalkDirection};
+use crate::{Client, Event};
+use async_trait::async_trait;
+use azalea_core::{BlockPos, CardinalDirection};
+use azalea_world::entity::EntityData;
+use mtdstarlite::Edge;
+pub use mtdstarlite::MTDStarLite;
+use parking_lot::Mutex;
+use std::collections::VecDeque;
+use std::sync::Arc;
+
+#[derive(Default, Clone)]
+pub struct Plugin {
+ pub state: State,
+}
+
+#[derive(Default, Clone)]
+pub struct State {
+ // pathfinder: Option<MTDStarLite<Node, f32>>,
+ pub path: Arc<Mutex<VecDeque<Node>>>,
+}
+
+#[async_trait]
+impl crate::Plugin for Plugin {
+ async fn handle(self: Box<Self>, event: Event, mut bot: Client) {
+ if let Event::Tick = event {
+ let mut path = self.state.path.lock();
+
+ if !path.is_empty() {
+ tick_execute_path(&mut bot, &mut path);
+ }
+ }
+ }
+}
+
+pub trait Trait {
+ fn goto(&self, goal: impl Goal);
+}
+
+impl Trait for azalea_client::Client {
+ fn goto(&self, goal: impl Goal) {
+ let start = Node {
+ pos: BlockPos::from(self.entity().pos()),
+ vertical_vel: VerticalVel::None,
+ };
+ let end = goal.goal_node();
+ println!("start: {:?}, end: {:?}", start, end);
+
+ let possible_moves: Vec<&dyn moves::Move> = vec![
+ &moves::ForwardMove(CardinalDirection::North),
+ &moves::ForwardMove(CardinalDirection::East),
+ &moves::ForwardMove(CardinalDirection::South),
+ &moves::ForwardMove(CardinalDirection::West),
+ //
+ &moves::AscendMove(CardinalDirection::North),
+ &moves::AscendMove(CardinalDirection::East),
+ &moves::AscendMove(CardinalDirection::South),
+ &moves::AscendMove(CardinalDirection::West),
+ //
+ &moves::DescendMove(CardinalDirection::North),
+ &moves::DescendMove(CardinalDirection::East),
+ &moves::DescendMove(CardinalDirection::South),
+ &moves::DescendMove(CardinalDirection::West),
+ //
+ &moves::DiagonalMove(CardinalDirection::North),
+ &moves::DiagonalMove(CardinalDirection::East),
+ &moves::DiagonalMove(CardinalDirection::South),
+ &moves::DiagonalMove(CardinalDirection::West),
+ ];
+
+ let successors = |node: &Node| {
+ let mut edges = Vec::new();
+
+ let dimension = self.dimension.read();
+ for possible_move in possible_moves.iter() {
+ edges.push(Edge {
+ target: possible_move.next_node(&node),
+ cost: possible_move.cost(&dimension, node),
+ });
+ }
+ edges
+ };
+
+ let mut pf = MTDStarLite::new(
+ start,
+ end,
+ |n| goal.heuristic(n),
+ successors,
+ successors,
+ |n| goal.success(n),
+ );
+
+ let start = std::time::Instant::now();
+ let p = pf.find_path();
+ let end = std::time::Instant::now();
+ println!("path: {:?}", p);
+ println!("time: {:?}", end - start);
+
+ let state = self
+ .plugins
+ .get::<Plugin>()
+ .expect("Pathfinder plugin not installed!")
+ .state
+ .clone();
+ // convert the Option<Vec<Node>> to a VecDeque<Node>
+ *state.path.lock() = p.expect("no path").into_iter().collect();
+ }
+}
+
+fn tick_execute_path(bot: &mut Client, path: &mut VecDeque<Node>) {
+ let target = if let Some(target) = path.front() {
+ target
+ } else {
+ return;
+ };
+ let center = target.pos.center();
+ // println!("going to {center:?} (at {pos:?})", pos = bot.entity().pos());
+ bot.look_at(&center);
+ bot.sprint(SprintDirection::Forward);
+ // check if we should jump
+ if target.pos.y > bot.entity().pos().y.floor() as i32 {
+ bot.jump();
+ }
+
+ if target.is_reached(&bot.entity()) {
+ println!("ok target {target:?} reached");
+ path.pop_front();
+ if path.is_empty() {
+ bot.walk(WalkDirection::None);
+ }
+ // tick again, maybe we already reached the next node!
+ tick_execute_path(bot, path);
+ }
+}
+
+/// Information about our vertical velocity
+#[derive(Eq, PartialEq, Hash, Clone, Copy, Debug)]
+pub enum VerticalVel {
+ None,
+ /// No vertical velocity, but we're not on the ground
+ NoneMidair,
+ // less than 3 blocks (no fall damage)
+ FallingLittle,
+}
+
+#[derive(Eq, PartialEq, Hash, Clone, Copy, Debug)]
+pub struct Node {
+ pub pos: BlockPos,
+ pub vertical_vel: VerticalVel,
+}
+
+pub trait Goal {
+ fn heuristic(&self, n: &Node) -> f32;
+ fn success(&self, n: &Node) -> bool;
+ // TODO: this should be removed and mtdstarlite should stop depending on
+ // being given a goal node
+ fn goal_node(&self) -> Node;
+}
+
+impl Node {
+ /// Returns whether the entity is at the node and should start going to the
+ /// next node.
+ pub fn is_reached(&self, entity: &EntityData) -> bool {
+ println!(
+ "entity.delta.y: {} {:?}=={:?}, self.vertical_vel={:?}",
+ entity.delta.y,
+ BlockPos::from(entity.pos()),
+ self.pos,
+ self.vertical_vel
+ );
+ BlockPos::from(entity.pos()) == self.pos
+ && match self.vertical_vel {
+ VerticalVel::NoneMidair => entity.delta.y > -0.1 && entity.delta.y < 0.1,
+ VerticalVel::None => entity.on_ground,
+ VerticalVel::FallingLittle => entity.delta.y < -0.1,
+ }
+ }
+}
+
+pub struct BlockPosGoal {
+ pub pos: BlockPos,
+}
+impl Goal for BlockPosGoal {
+ fn heuristic(&self, n: &Node) -> f32 {
+ let dx = (self.pos.x - n.pos.x) as f32;
+ let dy = (self.pos.y - n.pos.y) as f32;
+ let dz = (self.pos.z - n.pos.z) as f32;
+ dx * dx + dy * dy + dz * dz
+ }
+ fn success(&self, n: &Node) -> bool {
+ n.pos == self.pos
+ }
+ fn goal_node(&self) -> Node {
+ Node {
+ pos: self.pos,
+ vertical_vel: VerticalVel::None,
+ }
+ }
+}
+
+impl From<BlockPos> for BlockPosGoal {
+ fn from(pos: BlockPos) -> Self {
+ Self { pos }
+ }
+}
diff --git a/azalea/src/pathfinder/moves.rs b/azalea/src/pathfinder/moves.rs
new file mode 100644
index 00000000..ed98b70d
--- /dev/null
+++ b/azalea/src/pathfinder/moves.rs
@@ -0,0 +1,191 @@
+use super::{Node, VerticalVel};
+use azalea_core::{BlockPos, CardinalDirection};
+use azalea_physics::collision::{self, BlockWithShape};
+use azalea_world::Dimension;
+
+/// whether this block is passable
+fn is_block_passable(pos: &BlockPos, dim: &Dimension) -> bool {
+ if let Some(block) = dim.get_block_state(pos) {
+ block.shape() == &collision::empty_shape()
+ } else {
+ false
+ }
+}
+
+/// whether this block has a solid hitbox (i.e. we can stand on it)
+fn is_block_solid(pos: &BlockPos, dim: &Dimension) -> bool {
+ if let Some(block) = dim.get_block_state(pos) {
+ block.shape() == &collision::block_shape()
+ } else {
+ false
+ }
+}
+
+/// Whether this block and the block above are passable
+fn is_passable(pos: &BlockPos, dim: &Dimension) -> bool {
+ is_block_passable(pos, dim) && is_block_passable(&pos.up(1), dim)
+}
+
+/// Whether we can stand in this position. Checks if the block below is solid,
+/// and that the two blocks above that are passable.
+fn is_standable(pos: &BlockPos, dim: &Dimension) -> bool {
+ is_block_solid(&pos.down(1), dim) && is_passable(&pos, dim)
+}
+
+const JUMP_COST: f32 = 0.5;
+const WALK_ONE_BLOCK_COST: f32 = 1.0;
+
+pub trait Move {
+ fn cost(&self, dim: &Dimension, node: &Node) -> f32;
+ /// Returns by how much the entity's position should be changed when this move is executed.
+ fn offset(&self) -> BlockPos;
+ fn next_node(&self, node: &Node) -> Node {
+ Node {
+ pos: node.pos + self.offset(),
+ vertical_vel: VerticalVel::None,
+ }
+ }
+}
+
+pub struct ForwardMove(pub CardinalDirection);
+impl Move for ForwardMove {
+ fn cost(&self, dim: &Dimension, node: &Node) -> f32 {
+ if is_standable(&(node.pos + self.offset()), dim) && node.vertical_vel == VerticalVel::None
+ {
+ WALK_ONE_BLOCK_COST
+ } else {
+ f32::INFINITY
+ }
+ }
+ fn offset(&self) -> BlockPos {
+ BlockPos::new(self.0.x(), 0, self.0.z())
+ }
+}
+
+pub struct AscendMove(pub CardinalDirection);
+impl Move for AscendMove {
+ fn cost(&self, dim: &Dimension, node: &Node) -> f32 {
+ if node.vertical_vel == VerticalVel::None
+ && is_block_passable(&node.pos.up(2), dim)
+ && is_standable(&(node.pos + self.offset()), dim)
+ {
+ WALK_ONE_BLOCK_COST + JUMP_COST
+ } else {
+ f32::INFINITY
+ }
+ }
+ fn offset(&self) -> BlockPos {
+ BlockPos::new(self.0.x(), 1, self.0.z())
+ }
+ fn next_node(&self, node: &Node) -> Node {
+ Node {
+ pos: node.pos + self.offset(),
+ vertical_vel: VerticalVel::None,
+ }
+ }
+}
+pub struct DescendMove(pub CardinalDirection);
+impl Move for DescendMove {
+ fn cost(&self, dim: &Dimension, node: &Node) -> f32 {
+ // check whether 3 blocks vertically forward are passable
+ if node.vertical_vel == VerticalVel::None
+ && is_standable(&(node.pos + self.offset()), dim)
+ && is_block_passable(&(node.pos + self.offset().up(2)), dim)
+ {
+ WALK_ONE_BLOCK_COST
+ } else {
+ f32::INFINITY
+ }
+ }
+ fn offset(&self) -> BlockPos {
+ BlockPos::new(self.0.x(), -1, self.0.z())
+ }
+ fn next_node(&self, node: &Node) -> Node {
+ Node {
+ pos: node.pos + self.offset(),
+ vertical_vel: VerticalVel::None,
+ }
+ }
+}
+pub struct DiagonalMove(pub CardinalDirection);
+impl Move for DiagonalMove {
+ fn cost(&self, dim: &Dimension, node: &Node) -> f32 {
+ if node.vertical_vel != VerticalVel::None {
+ return f32::INFINITY;
+ }
+ if !is_passable(
+ &BlockPos::new(node.pos.x + self.0.x(), node.pos.y, node.pos.z + self.0.z()),
+ dim,
+ ) && !is_passable(
+ &BlockPos::new(
+ node.pos.x + self.0.right().x(),
+ node.pos.y,
+ node.pos.z + self.0.right().z(),
+ ),
+ dim,
+ ) {
+ return f32::INFINITY;
+ }
+ if !is_standable(&(node.pos + self.offset()), dim) {
+ return f32::INFINITY;
+ }
+ WALK_ONE_BLOCK_COST * 1.4
+ }
+ fn offset(&self) -> BlockPos {
+ let right = self.0.right();
+ BlockPos::new(self.0.x() + right.x(), 0, self.0.z() + right.z())
+ }
+ fn next_node(&self, node: &Node) -> Node {
+ Node {
+ pos: node.pos + self.offset(),
+ vertical_vel: VerticalVel::None,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use azalea_block::BlockState;
+ use azalea_core::ChunkPos;
+ use azalea_world::Chunk;
+
+ #[test]
+ fn test_is_passable() {
+ let mut dim = Dimension::default();
+ dim.set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()))
+ .unwrap();
+ dim.set_block_state(&BlockPos::new(0, 0, 0), BlockState::Stone);
+ dim.set_block_state(&BlockPos::new(0, 1, 0), BlockState::Air);
+
+ assert_eq!(is_block_passable(&BlockPos::new(0, 0, 0), &dim), false);
+ assert_eq!(is_block_passable(&BlockPos::new(0, 1, 0), &dim), true);
+ }
+
+ #[test]
+ fn test_is_solid() {
+ let mut dim = Dimension::default();
+ dim.set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()))
+ .unwrap();
+ dim.set_block_state(&BlockPos::new(0, 0, 0), BlockState::Stone);
+ dim.set_block_state(&BlockPos::new(0, 1, 0), BlockState::Air);
+
+ assert_eq!(is_block_solid(&BlockPos::new(0, 0, 0), &dim), true);
+ assert_eq!(is_block_solid(&BlockPos::new(0, 1, 0), &dim), false);
+ }
+
+ #[test]
+ fn test_is_standable() {
+ let mut dim = Dimension::default();
+ dim.set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()))
+ .unwrap();
+ dim.set_block_state(&BlockPos::new(0, 0, 0), BlockState::Stone);
+ dim.set_block_state(&BlockPos::new(0, 1, 0), BlockState::Air);
+ dim.set_block_state(&BlockPos::new(0, 2, 0), BlockState::Air);
+ dim.set_block_state(&BlockPos::new(0, 3, 0), BlockState::Air);
+
+ assert!(is_standable(&BlockPos::new(0, 1, 0), &dim));
+ assert!(!is_standable(&BlockPos::new(0, 0, 0), &dim));
+ assert!(!is_standable(&BlockPos::new(0, 2, 0), &dim));
+ }
+}
diff --git a/azalea/src/pathfinder/mtdstarlite.rs b/azalea/src/pathfinder/mtdstarlite.rs
new file mode 100644
index 00000000..8a40ec37
--- /dev/null
+++ b/azalea/src/pathfinder/mtdstarlite.rs
@@ -0,0 +1,453 @@
+//! An implementation of Moving Target D* Lite as described in
+//! <http://idm-lab.org/bib/abstracts/papers/aamas10a.pdf>
+//!
+//! Future optimization attempt ideas:
+//! - Use a different priority queue (e.g. fibonacci heap)
+//! - Use FxHash instead of the default hasher
+//! - Have `par` be a raw pointer
+//! - Try borrowing vs copying the Node in several places (like state_mut)
+//! - Store edge costs in their own map
+
+use priority_queue::DoublePriorityQueue;
+use std::{collections::HashMap, fmt::Debug, hash::Hash, ops::Add};
+
+/// Nodes are coordinates.
+pub struct MTDStarLite<
+ N: Eq + Hash + Copy + Debug,
+ W: PartialOrd + Default + Copy + num_traits::Bounded + Debug,
+ HeuristicFn: Fn(&N) -> W,
+ SuccessorsFn: Fn(&N) -> Vec<Edge<N, W>>,
+ PredecessorsFn: Fn(&N) -> Vec<Edge<N, W>>,
+ SuccessFn: Fn(&N) -> bool,
+> {
+ /// Returns a rough estimate of how close we are to the goal. Lower = closer.
+ pub heuristic: HeuristicFn,
+ /// Returns the nodes that can be reached from the given node.
+ pub successors: SuccessorsFn,
+ /// Returns the nodes that would direct us to the given node. If the graph
+ /// isn't directed (i.e. you can always return to the previous node), this
+ /// can be the same as `successors`.
+ pub predecessors: PredecessorsFn,
+ /// Returns true if the given node is at the goal.
+ /// A simple implementation is to check if the given node is equal to the goal.
+ pub success: SuccessFn,
+
+ start: N,
+ goal: N,
+
+ old_start: N,
+ old_goal: N,
+
+ k_m: W,
+ open: DoublePriorityQueue<N, Priority<W>>,
+ node_states: HashMap<N, NodeState<N, W>>,
+ updated_edge_costs: Vec<ChangedEdge<N, W>>,
+
+ /// This only exists so it can be referenced by `state()` when there's no state.
+ default_state: NodeState<N, W>,
+}
+
+impl<
+ N: Eq + Hash + Copy + Debug,
+ W: PartialOrd + Add<Output = W> + Default + Copy + num_traits::Bounded + Debug,
+ HeuristicFn: Fn(&N) -> W,
+ SuccessorsFn: Fn(&N) -> Vec<Edge<N, W>>,
+ PredecessorsFn: Fn(&N) -> Vec<Edge<N, W>>,
+ SuccessFn: Fn(&N) -> bool,
+ > MTDStarLite<N, W, HeuristicFn, SuccessorsFn, PredecessorsFn, SuccessFn>
+{
+ fn calculate_key(&self, n: &N) -> Priority<W> {
+ let s = self.state(n);
+ let min_score = if s.g < s.rhs { s.g } else { s.rhs };
+ Priority(
+ if min_score == W::max_value() {
+ min_score
+ } else {
+ min_score + (self.heuristic)(n) + self.k_m
+ },
+ min_score,
+ )
+ }
+
+ pub fn new(
+ start: N,
+ goal: N,
+ heuristic: HeuristicFn,
+ successors: SuccessorsFn,
+ predecessors: PredecessorsFn,
+ success: SuccessFn,
+ ) -> Self {
+ let open = DoublePriorityQueue::default();
+ let k_m = W::default();
+
+ let known_nodes = vec![start, goal];
+
+ let mut pf = MTDStarLite {
+ heuristic,
+ successors,
+ predecessors,
+ success,
+
+ start,
+ goal,
+
+ old_start: start,
+ old_goal: goal,
+
+ k_m,
+ open,
+ node_states: HashMap::new(),
+ updated_edge_costs: Vec::new(),
+
+ default_state: NodeState::default(),
+ };
+
+ for n in &known_nodes {
+ *pf.state_mut(n) = NodeState::default();
+ }
+ (*pf.state_mut(&start)).rhs = W::default();
+ pf.open.push(start, pf.calculate_key(&start));
+
+ pf
+ }
+
+ fn update_state(&mut self, n: &N) {
+ let u = self.state_mut(n);
+ if u.g != u.rhs {
+ if self.open.get(n).is_some() {
+ self.open.change_priority(n, self.calculate_key(n));
+ } else {
+ self.open.push(*n, self.calculate_key(n));
+ }
+ } else if self.open.get(n).is_some() {
+ self.open.remove(n);
+ }
+ }
+
+ fn compute_cost_minimal_path(&mut self) {
+ while {
+ if let Some((_, top_key)) = self.open.peek_min() {
+ (top_key < &self.calculate_key(&self.goal)) || {
+ let goal_state = self.state(&self.goal);
+ goal_state.rhs > goal_state.g
+ }
+ } else {
+ false
+ }
+ } {
+ let (u_node, k_old) = self.open.pop_min().unwrap();
+ let k_new = self.calculate_key(&u_node);
+ if k_old < k_new {
+ self.open.change_priority(&u_node, k_new);
+ continue;
+ }
+ let u = self.state_mut(&u_node);
+ if u.g > u.rhs {
+ u.g = u.rhs;
+ self.open.remove(&u_node);
+ for edge in (self.successors)(&u_node) {
+ let s_node = edge.target;
+ let s = self.state(&s_node);
+ let u = self.state(&u_node);
+ if s_node != self.start && (s.rhs > u.g + edge.cost) {
+ let s_rhs = u.g + edge.cost;
+ let s = self.state_mut(&s_node);
+ s.par = Some(u_node);
+ s.rhs = s_rhs;
+ self.update_state(&s_node);
+ }
+ }
+ } else {
+ u.g = W::max_value();
+ let u_edge = Edge {
+ target: u_node,
+ cost: W::default(),
+ };
+ for edge in (self.successors)(&u_node)
+ .iter()
+ .chain([&u_edge].into_iter())
+ {
+ let s_node = edge.target;
+ let s = self.state(&s_node);
+ if s_node != self.start && s.par == Some(u_node) {
+ let mut min_pred = u_node;
+ let mut min_score = W::max_value();
+
+ for edge in (self.predecessors)(&s_node) {
+ let s = self.state(&edge.target);
+ let score = s.g + edge.cost;
+ if score < min_score {
+ min_score = score;
+ min_pred = edge.target;
+ }
+ }
+
+ let s = self.state_mut(&s_node);
+ s.rhs = min_score;
+ if s.rhs == W::max_value() {
+ s.par = None;
+ } else {
+ s.par = Some(min_pred);
+ }
+ }
+ self.update_state(&s_node);
+ }
+ }
+ }
+ }
+
+ pub fn find_path(&mut self) -> Option<Vec<N>> {
+ if (self.success)(&self.start) {
+ return None;
+ }
+
+ //
+ self.k_m = self.k_m + (self.heuristic)(&self.old_goal);
+
+ if self.old_start != self.start {
+ self.optimized_deletion();
+ }
+
+ while let Some(edge) = self.updated_edge_costs.pop() {
+ let (u_node, v_node) = (edge.predecessor, edge.successor);
+ // update the edge cost c(u, v);
+ if edge.old_cost > edge.cost {
+ let u_g = self.state(&u_node).g;
+ if v_node != self.start && self.state(&v_node).rhs > u_g + edge.cost {
+ let v = self.state_mut(&v_node);
+ v.par = Some(u_node);
+ v.rhs = u_g + edge.cost;
+ }
+ } else if v_node != self.start && self.state(&v_node).par == Some(u_node) {
+ let mut min_pred = u_node;
+ let mut min_score = W::max_value();
+
+ for edge in (self.predecessors)(&v_node) {
+ let s = self.state(&edge.target);
+ let score = s.g + edge.cost;
+ if score < min_score {
+ min_score = score;
+ min_pred = edge.target;
+ }
+ }
+
+ let v = self.state_mut(&v_node);
+ v.rhs = min_score;
+ if v.rhs == W::max_value() {
+ v.par = None;
+ } else {
+ v.par = Some(min_pred);
+ }
+ self.update_state(&v_node);
+ }
+ }
+ //
+
+ self.old_start = self.start;
+ self.old_goal = self.goal;
+
+ self.compute_cost_minimal_path();
+ if self.state(&self.goal).rhs == W::max_value() {
+ // no path exists
+ return None;
+ }
+
+ let mut reverse_path = vec![self.goal];
+
+ // identify a path from sstart to sgoal using the parent pointers
+ let mut target = self.state(&self.goal).par;
+ while !(Some(self.start) == target) {
+ let this_target = if let Some(this_target) = target {
+ this_target
+ } else {
+ break;
+ };
+ // hunter follows path from start to goal;
+ reverse_path.push(this_target);
+ target = self.state(&this_target).par;
+ }
+
+ // if hunter caught target {
+ // return None;
+ // }
+
+ let path: Vec<N> = reverse_path.into_iter().rev().collect();
+
+ Some(path)
+ }
+
+ fn optimized_deletion(&mut self) {
+ let start = self.start;
+ self.state_mut(&start).par = None;
+
+ let mut min_pred = self.old_start;
+ let mut min_score = W::max_value();
+
+ for edge in (self.predecessors)(&self.old_start) {
+ let s = self.state(&edge.target);
+ let score = s.g + edge.cost;
+ if score < min_score {
+ min_score = score;
+ min_pred = edge.target;
+ }
+ }
+
+ let old_start = self.old_start;
+ let s = self.state_mut(&old_start);
+ s.rhs = min_score;
+ if s.rhs == W::max_value() {
+ s.par = None;
+ } else {
+ s.par = Some(min_pred);
+ }
+ self.update_state(&old_start);
+ }
+
+ fn state(&self, n: &N) -> &NodeState<N, W> {
+ self.node_states.get(n).unwrap_or(&self.default_state)
+ }
+
+ fn state_mut(&mut self, n: &N) -> &mut NodeState<N, W> {
+ self.node_states.entry(*n).or_default()
+ }
+}
+
+#[derive(PartialEq, Debug)]
+pub struct Priority<W>(W, W)
+where
+ W: PartialOrd + Debug;
+
+impl<W: PartialOrd + Debug> PartialOrd for Priority<W> {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ if self.0 < other.0 {
+ Some(std::cmp::Ordering::Less)
+ } else if self.0 > other.0 {
+ Some(std::cmp::Ordering::Greater)
+ } else if self.1 < other.1 {
+ Some(std::cmp::Ordering::Less)
+ } else if self.1 > other.1 {
+ Some(std::cmp::Ordering::Greater)
+ } else {
+ Some(std::cmp::Ordering::Equal)
+ }
+ }
+}
+impl<W: PartialOrd + Debug> Ord for Priority<W> {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ self.partial_cmp(other)
+ .expect("Partial compare should not fail for Priority")
+ }
+}
+impl<W: PartialOrd + Debug> Eq for Priority<W> {}
+
+#[derive(Debug)]
+pub struct NodeState<N: Eq + Hash + Copy + Debug, W: Default + num_traits::Bounded + Debug> {
+ pub g: W,
+ pub rhs: W,
+ // future possible optimization: try making this a pointer
+ pub par: Option<N>,
+}
+
+impl<N: Eq + Hash + Copy + Debug, W: Default + num_traits::Bounded + Debug> Default
+ for NodeState<N, W>
+{
+ fn default() -> Self {
+ NodeState {
+ g: W::max_value(),
+ rhs: W::max_value(),
+ par: None,
+ }
+ }
+}
+
+pub struct Edge<N: Eq + Hash + Copy, W: PartialOrd + Copy> {
+ pub target: N,
+ pub cost: W,
+}
+
+pub struct ChangedEdge<N: Eq + Hash + Clone, W: PartialOrd + Copy> {
+ pub predecessor: N,
+ pub successor: N,
+ pub old_cost: W,
+ pub cost: W,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_mtdstarlite() {
+ let maze = [
+ [0, 1, 0, 0, 0],
+ [0, 1, 0, 1, 0],
+ [0, 0, 0, 1, 0],
+ [0, 1, 0, 1, 0],
+ [0, 0, 1, 0, 0],
+ ];
+ let width = maze[0].len();
+ let height = maze.len();
+
+ let goal = (4, 4);
+
+ let heuristic = |n: &(usize, usize)| -> usize {
+ ((n.0 as isize - goal.0 as isize).abs() + (n.1 as isize - goal.1 as isize).abs())
+ as usize
+ };
+ let successors = |n: &(usize, usize)| -> Vec<Edge<(usize, usize), usize>> {
+ let mut successors = Vec::with_capacity(4);
+ let (x, y) = *n;
+
+ if x > 0 && maze[y][x - 1] == 0 {
+ successors.push(Edge {
+ target: ((x - 1, y)),
+ cost: 1,
+ });
+ }
+ if x < width - 1 && maze[y][x + 1] == 0 {
+ successors.push(Edge {
+ target: ((x + 1, y)),
+ cost: 1,
+ });
+ }
+ if y > 0 && maze[y - 1][x] == 0 {
+ successors.push(Edge {
+ target: ((x, y - 1)),
+ cost: 1,
+ });
+ }
+ if y < height - 1 && maze[y + 1][x] == 0 {
+ successors.push(Edge {
+ target: ((x, y + 1)),
+ cost: 1,
+ });
+ }
+
+ successors
+ };
+ let predecessors =
+ |n: &(usize, usize)| -> Vec<Edge<(usize, usize), usize>> { successors(n) };
+
+ let mut pf = MTDStarLite::new((0, 0), goal, heuristic, successors, predecessors, |n| {
+ n == &goal
+ });
+ let path = pf.find_path().unwrap();
+ assert_eq!(
+ path,
+ vec![
+ (0, 1),
+ (0, 2),
+ (1, 2),
+ (2, 2),
+ (2, 1),
+ (2, 0),
+ (3, 0),
+ (4, 0),
+ (4, 1),
+ (4, 2),
+ (4, 3),
+ (4, 4),
+ ]
+ );
+ }
+}
diff --git a/azalea/src/prelude.rs b/azalea/src/prelude.rs
index c09d85e2..9fa1ac1a 100644..100755
--- a/azalea/src/prelude.rs
+++ b/azalea/src/prelude.rs
@@ -1,4 +1,6 @@
//! The Azalea prelude. Things that are necessary for a bare-bones bot are re-exported here.
pub use crate::bot::BotTrait;
+pub use crate::pathfinder::Trait;
+pub use crate::plugins;
pub use azalea_client::{Account, Client, Event};