diff --git a/benches/distance_transform.rs b/benches/distance_transform.rs index f1b0777..aaa7622 100644 --- a/benches/distance_transform.rs +++ b/benches/distance_transform.rs @@ -9,7 +9,21 @@ mod benches { #[bench] fn bench_chebyshev_distance_transform(b: &mut Bencher) { - let terrain = LocalRoomTerrain::new_from_bits(Box::new([0; 2500])); - b.iter(|| black_box(chebyshev_distance_transform_from_terrain(&terrain))); + let mut terrain = LocalRoomTerrain::new_from_bits(Box::new([0; 2500])); + b.iter(|| { + black_box(chebyshev_distance_transform_from_terrain(&*black_box( + &mut terrain, + ))) + }); + } + + #[bench] + fn bench_manhattan_distance_transform(b: &mut Bencher) { + let mut terrain = LocalRoomTerrain::new_from_bits(Box::new([0; 2500])); + b.iter(|| { + black_box(manhattan_distance_transform_from_terrain(&*black_box( + &mut terrain, + ))) + }); } } diff --git a/src/algorithms/distance_transform.rs b/src/algorithms/distance_transform.rs index 141ac42..378c001 100644 --- a/src/algorithms/distance_transform.rs +++ b/src/algorithms/distance_transform.rs @@ -1,11 +1,12 @@ // Heavily based on https://github.com/Screeps-Tutorials/Screeps-Tutorials/blob/Master/basePlanningAlgorithms/distanceTransform.js use screeps::{ - self, - constants::{extra::ROOM_SIZE, Direction}, - local::{LocalCostMatrix, LocalRoomTerrain, RoomXY}, + constants::extra::ROOM_SIZE, + local::{LocalCostMatrix, LocalRoomTerrain, RoomCoordinate, RoomXY}, }; +use crate::room_coordinate::{range_exclusive, range_inclusive}; + /// Provides a Cost Matrix with values equal to the Chebyshev distance from any /// wall terrain. This does *not* calculate based on constructed walls, only /// terrain walls. @@ -29,67 +30,304 @@ pub fn chebyshev_distance_transform_from_terrain( /// This allows for calculating the distance transform from an arbitrary set of /// positions. Other position values in the initial Cost Matrix should be /// initialized to 255 (u8::MAX) to ensure the calculations work correctly. -pub fn chebyshev_distance_transform_from_cost_matrix( - initial_cm: LocalCostMatrix, +pub fn chebyshev_distance_transform_from_cost_matrix(mut cm: LocalCostMatrix) -> LocalCostMatrix { + let zero = RoomCoordinate::new(0).unwrap(); + let one = RoomCoordinate::new(1).unwrap(); + let forty_eight = RoomCoordinate::new(ROOM_SIZE - 2).unwrap(); + let forty_nine = RoomCoordinate::new(ROOM_SIZE - 1).unwrap(); + // Pass 1: Top-to-Bottom, Left-to-Right + + // Phase A: first column + range_inclusive(one, forty_nine) + .map(|y| RoomXY { x: zero, y }) + .fold(cm.get(RoomXY { x: zero, y: zero }), |top, xy| { + let val = cm.get(xy).min(top.saturating_add(1)); + cm.set(xy, val); + val + }); + + // Phase B: the rest + range_inclusive(one, forty_nine) + .zip(range_inclusive(zero, forty_eight)) + .for_each(|(current_x, left_x)| { + let initial_top = cm + .get(RoomXY { + x: current_x, + y: zero, + }) + .min( + cm.get(RoomXY { x: left_x, y: zero }) + .min(cm.get(RoomXY { x: left_x, y: one })) + .saturating_add(1), + ); + cm.set( + RoomXY { + x: current_x, + y: zero, + }, + initial_top, + ); + let final_top = range_exclusive(zero, forty_nine) + .map(|y| { + (RoomXY { x: current_x, y }, unsafe { + [ + RoomCoordinate::unchecked_new(y.u8() - 1), + y, + RoomCoordinate::unchecked_new(y.u8() + 1), + ] + }) + }) + .fold(initial_top, |top, (current_xy, lefts)| { + let val = lefts + .into_iter() + .map(|y| RoomXY { x: left_x, y }) + .map(|xy| cm.get(xy)) + .min() + .unwrap() + .min(top) + .saturating_add(1) + .min(cm.get(current_xy)); + cm.set(current_xy, val); + val + }); + cm.set( + RoomXY { + x: current_x, + y: forty_nine, + }, + cm.get(RoomXY { + x: current_x, + y: forty_nine, + }) + .min( + final_top + .min(cm.get(RoomXY { + x: left_x, + y: forty_eight, + })) + .min(cm.get(RoomXY { + x: left_x, + y: forty_nine, + })) + .saturating_add(1), + ), + ); + }); + + // Pass 2: Bottom-to-Top, Right-to-Left + + // Phase A: last column + range_inclusive(zero, forty_eight) + .map(|y| RoomXY { x: forty_nine, y }) + .rfold( + cm.get(RoomXY { + x: forty_nine, + y: forty_nine, + }), + |bottom, xy| { + let val = cm.get(xy).min(bottom.saturating_add(1)); + cm.set(xy, val); + val + }, + ); + + // Phase B: the rest + range_inclusive(zero, forty_eight) + .rev() + .zip(range_inclusive(one, forty_nine).rev()) + .for_each(|(current_x, right_x)| { + let initial_bottom = cm + .get(RoomXY { + x: current_x, + y: forty_nine, + }) + .min( + cm.get(RoomXY { + x: right_x, + y: forty_nine, + }) + .min(cm.get(RoomXY { + x: right_x, + y: forty_eight, + })) + .saturating_add(1), + ); + cm.set( + RoomXY { + x: current_x, + y: forty_nine, + }, + initial_bottom, + ); + let final_bottom = range_exclusive(zero, forty_nine) + .map(|y| { + (RoomXY { x: current_x, y }, unsafe { + [ + RoomCoordinate::unchecked_new(y.u8() - 1), + y, + RoomCoordinate::unchecked_new(y.u8() + 1), + ] + }) + }) + .rfold(initial_bottom, |bottom, (current_xy, rights)| { + let val = rights + .into_iter() + .map(|y| RoomXY { x: right_x, y }) + .map(|xy| cm.get(xy)) + .min() + .unwrap() + .min(bottom) + .saturating_add(1) + .min(cm.get(current_xy)); + cm.set(current_xy, val); + val + }); + cm.set( + RoomXY { + x: current_x, + y: zero, + }, + cm.get(RoomXY { + x: current_x, + y: zero, + }) + .min( + final_bottom + .min(cm.get(RoomXY { + x: right_x, + y: zero, + })) + .min(cm.get(RoomXY { x: right_x, y: one })) + .saturating_add(1), + ), + ); + }); + + cm +} + +/// Provides a Cost Matrix with values equal to the Manhattan distance from any +/// wall terrain. This does *not* calculate based on constructed walls, only +/// terrain walls. +pub fn manhattan_distance_transform_from_terrain( + room_terrain: &LocalRoomTerrain, ) -> LocalCostMatrix { - // Copy the initial cost matrix into the output cost matrix - let mut cm = initial_cm.clone(); + let mut initial_cm = LocalCostMatrix::new(); + for (xy, cm_val) in initial_cm.iter_mut() { + *cm_val = match room_terrain.get_xy(xy) { + screeps::constants::Terrain::Wall => 0, + _ => u8::MAX, + }; + } + manhattan_distance_transform_from_cost_matrix(initial_cm) +} + +/// Provides a Cost Matrix with values equal to the Manhattan distance from any +/// position in the provided initial Cost Matrix with a value set to 0. +/// +/// This allows for calculating the distance transform from an arbitrary set of +/// positions. Other position values in the initial Cost Matrix should be +/// initialized to 255 (u8::MAX) to ensure the calculations work correctly. +pub fn manhattan_distance_transform_from_cost_matrix(mut cm: LocalCostMatrix) -> LocalCostMatrix { + let zero = RoomCoordinate::new(0).unwrap(); + let one = RoomCoordinate::new(1).unwrap(); + let forty_eight = RoomCoordinate::new(ROOM_SIZE - 2).unwrap(); + let forty_nine = RoomCoordinate::new(ROOM_SIZE - 1).unwrap(); // Pass 1: Top-to-Bottom, Left-to-Right - for x in 0..ROOM_SIZE { - for y in 0..ROOM_SIZE { - let current_position = unsafe { RoomXY::unchecked_new(x, y) }; - - // The distance to the closest wall is the minimum of the current position value - // and all of its neighbors. However, since we're going TTB:LTR, we - // can ignore tiles we know we haven't visited yet: TopRight, Right, - // BottomRight, and Bottom. We could include them and their default - // max values should get ignored, but why waste the processing cycles? - let min_value = [ - Direction::Top, - Direction::TopLeft, - Direction::Left, - Direction::BottomLeft, - ] - .into_iter() - .filter_map(|dir| current_position.checked_add_direction(dir)) - .map(|position| cm.get(position)) - .min() - .map(|x| x.saturating_add(1)) - .map(|x| x.min(cm.get(current_position))) - .unwrap_or_else(|| cm.get(current_position)); - - cm.set(current_position, min_value); - } - } + // Phase A: first column + range_inclusive(one, forty_nine) + .map(|y| RoomXY { x: zero, y }) + .fold(cm.get(RoomXY { x: zero, y: zero }), |top, xy| { + let val = cm.get(xy).min(top.saturating_add(1)); + cm.set(xy, val); + val + }); + + // Phase B: the rest + range_inclusive(one, forty_nine) + .zip(range_inclusive(zero, forty_eight)) + .for_each(|(current_x, left_x)| { + let initial_top = cm + .get(RoomXY { + x: current_x, + y: zero, + }) + .min(cm.get(RoomXY { x: left_x, y: zero }).saturating_add(1)); + cm.set( + RoomXY { + x: current_x, + y: zero, + }, + initial_top, + ); + range_inclusive(one, forty_nine) + .map(|y| (RoomXY { x: current_x, y }, RoomXY { x: left_x, y })) + .fold(initial_top, |top, (current_xy, left_xy)| { + let val = cm + .get(left_xy) + .min(top) + .saturating_add(1) + .min(cm.get(current_xy)); + cm.set(current_xy, val); + val + }); + }); // Pass 2: Bottom-to-Top, Right-to-Left - for x in (0..ROOM_SIZE).rev() { - for y in (0..ROOM_SIZE).rev() { - let current_position = unsafe { RoomXY::unchecked_new(x, y) }; - - // The same logic as with Pass 1 applies here, we're just going BTT:RTL instead, - // so the neighbors we ignore are: BottomLeft, Left, TopLeft, and - // Top. - let min_value = [ - Direction::Bottom, - Direction::Right, - Direction::BottomRight, - Direction::TopRight, - ] - .into_iter() - .filter_map(|dir| current_position.checked_add_direction(dir)) - .map(|position| cm.get(position)) - .min() - .map(|x| x.saturating_add(1)) - .map(|x| x.min(cm.get(current_position))) - .unwrap_or_else(|| cm.get(current_position)); - - cm.set(current_position, min_value); - } - } + // Phase A: last column + range_inclusive(zero, forty_eight) + .map(|y| RoomXY { x: forty_nine, y }) + .rfold( + cm.get(RoomXY { + x: forty_nine, + y: forty_nine, + }), + |bottom, xy| { + let val = cm.get(xy).min(bottom.saturating_add(1)); + cm.set(xy, val); + val + }, + ); + + // Phase B: the rest + range_inclusive(zero, forty_eight) + .rev() + .zip(range_inclusive(one, forty_nine).rev()) + .for_each(|(current_x, right_x)| { + let initial_bottom = cm + .get(RoomXY { + x: current_x, + y: forty_nine, + }) + .min( + cm.get(RoomXY { + x: right_x, + y: forty_nine, + }) + .saturating_add(1), + ); + cm.set( + RoomXY { + x: current_x, + y: forty_nine, + }, + initial_bottom, + ); + range_inclusive(zero, forty_eight) + .map(|y| (RoomXY { x: current_x, y }, RoomXY { x: right_x, y })) + .rfold(initial_bottom, |bottom, (current_xy, right_xy)| { + let val = cm + .get(right_xy) + .min(bottom) + .saturating_add(1) + .min(cm.get(current_xy)); + cm.set(current_xy, val); + val + }); + }); cm }