|
| 1 | +use snk_grid::{ |
| 2 | + color::Color, |
| 3 | + direction::{Direction, iter_directions}, |
| 4 | + grid::Grid, |
| 5 | + point::{Point, get_distance}, |
| 6 | +}; |
| 7 | +use std::{ |
| 8 | + collections::{BinaryHeap, HashSet}, |
| 9 | + rc::Rc, |
| 10 | +}; |
| 11 | + |
| 12 | +use crate::cost::Cost; |
| 13 | + |
| 14 | +// move a snake from a position to a point (that it reaches with its head) |
| 15 | +pub fn get_snake_path( |
| 16 | + grid: &Grid<Color>, |
| 17 | + starting_snake_head_to_tail: &[Point], |
| 18 | + to: Point, |
| 19 | + max_cost: Cost, |
| 20 | +) -> Option<(Vec<Direction>, Cost)> { |
| 21 | + let mut open_list: BinaryHeap<Node> = BinaryHeap::new(); |
| 22 | + let mut close_list: HashSet<Vec<Point>> = HashSet::new(); |
| 23 | + |
| 24 | + let snake_len = starting_snake_head_to_tail.len(); |
| 25 | + |
| 26 | + open_list.push(Node { |
| 27 | + snake: starting_snake_head_to_tail.iter().map(|p| *p).collect(), |
| 28 | + cost: Cost::zero(), |
| 29 | + f: Cost::zero(), |
| 30 | + parent: None, |
| 31 | + }); |
| 32 | + |
| 33 | + let mut loop_count = 0; |
| 34 | + |
| 35 | + while let Some(node) = open_list.pop() { |
| 36 | + loop_count += 1; |
| 37 | + debug_assert!(loop_count < 10_000, "invariant: loop out of control"); |
| 38 | + |
| 39 | + let node_cost = node.cost; |
| 40 | + let node_head = node.snake[0]; |
| 41 | + |
| 42 | + if to == node_head { |
| 43 | + // unwrap |
| 44 | + let mut path = Vec::new(); |
| 45 | + |
| 46 | + let mut u = &Rc::new(node); |
| 47 | + while let Some(ref parent) = u.parent { |
| 48 | + let dir: Direction = (u.snake[0] - parent.snake[0]).try_into().unwrap(); |
| 49 | + path.push(dir); |
| 50 | + |
| 51 | + u = &parent; |
| 52 | + } |
| 53 | + path.reverse(); |
| 54 | + |
| 55 | + debug_assert_eq!( |
| 56 | + path.iter() |
| 57 | + .fold(starting_snake_head_to_tail[0], |p, &dir| p + dir.to_point()), |
| 58 | + to, |
| 59 | + "path should lead to target" |
| 60 | + ); |
| 61 | + |
| 62 | + return Some((path, node_cost)); |
| 63 | + } |
| 64 | + |
| 65 | + let rc_parent = Rc::new(node); |
| 66 | + |
| 67 | + for dir in iter_directions() { |
| 68 | + let next_head = node_head + dir.to_point(); |
| 69 | + |
| 70 | + if !grid.is_inside_margin(next_head, 2) { |
| 71 | + continue; |
| 72 | + } |
| 73 | + |
| 74 | + if rc_parent |
| 75 | + .snake |
| 76 | + .iter() |
| 77 | + .take(snake_len - 1) |
| 78 | + .any(|p| *p == next_head) |
| 79 | + { |
| 80 | + continue; |
| 81 | + } |
| 82 | + |
| 83 | + let mut next_snake = Vec::with_capacity(snake_len); |
| 84 | + next_snake.push(next_head); |
| 85 | + next_snake.extend_from_slice(&rc_parent.snake[0..(snake_len - 1)]); |
| 86 | + |
| 87 | + if close_list.contains(&next_snake) { |
| 88 | + continue; |
| 89 | + } |
| 90 | + |
| 91 | + close_list.insert(next_snake.clone()); |
| 92 | + |
| 93 | + let cost = node_cost + grid.get_color(next_head).into(); |
| 94 | + let distance = get_distance(next_head, to); |
| 95 | + |
| 96 | + // best case: only empty cells from here |
| 97 | + let f = cost + Cost::from(Color::Empty) * (distance as u64); |
| 98 | + if f > max_cost { |
| 99 | + continue; |
| 100 | + } |
| 101 | + |
| 102 | + open_list.push(Node { |
| 103 | + snake: next_snake, |
| 104 | + cost, |
| 105 | + f, |
| 106 | + parent: Some(Rc::clone(&rc_parent)), |
| 107 | + }); |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + None |
| 112 | +} |
| 113 | + |
| 114 | +#[derive(Debug)] |
| 115 | +struct Node { |
| 116 | + pub cost: Cost, |
| 117 | + pub f: Cost, |
| 118 | + pub parent: Option<Rc<Node>>, |
| 119 | + pub snake: Vec<Point>, |
| 120 | +} |
| 121 | +impl Eq for Node {} |
| 122 | +impl PartialEq for Node { |
| 123 | + fn eq(&self, other: &Self) -> bool { |
| 124 | + self.snake.eq(&other.snake) |
| 125 | + } |
| 126 | +} |
| 127 | +impl Ord for Node { |
| 128 | + fn cmp(&self, other: &Self) -> std::cmp::Ordering { |
| 129 | + other |
| 130 | + .f |
| 131 | + .cmp(&self.f) |
| 132 | + // this act as tie-breaker, to make the binaryheap (and the whole alg) determinist |
| 133 | + .then(self.snake[0].x.cmp(&other.snake[0].x)) |
| 134 | + .then(self.snake[0].y.cmp(&other.snake[0].y)) |
| 135 | + } |
| 136 | +} |
| 137 | +impl PartialOrd for Node { |
| 138 | + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { |
| 139 | + Some(self.cmp(other)) |
| 140 | + } |
| 141 | +} |
| 142 | + |
| 143 | +#[cfg(test)] |
| 144 | +mod tests { |
| 145 | + use super::*; |
| 146 | + use crate::debug_world::{ |
| 147 | + assert_world_equal, get_full_path, get_last_snake, read_world, render_world, |
| 148 | + }; |
| 149 | + |
| 150 | + #[test] |
| 151 | + fn it_should_find_simple_path__() { |
| 152 | + let (grid, snake, _, poi) = read_world( |
| 153 | + r#" |
| 154 | + ·········· |
| 155 | + ···╶@█···· |
| 156 | + ·······x·· |
| 157 | + "#, |
| 158 | + ); |
| 159 | + let target = *poi.get(&'x').unwrap(); |
| 160 | + |
| 161 | + let (path, _cost) = get_snake_path(&grid, &snake, target, Cost::max()).unwrap(); |
| 162 | + |
| 163 | + assert_world_equal( |
| 164 | + &render_world(&grid, &get_full_path(&snake, &path), None), |
| 165 | + r#" |
| 166 | + ·········· |
| 167 | + ···╶┐█···· |
| 168 | + ····└──@·· |
| 169 | + "#, |
| 170 | + ); |
| 171 | + } |
| 172 | + |
| 173 | + #[test] |
| 174 | + fn it_should_find_path_out_of_labyrinth() { |
| 175 | + let (grid, snake, _, poi) = read_world( |
| 176 | + r#" |
| 177 | + ··╶─@··············································· |
| 178 | + ██████████████████████████████████████████████████·█ |
| 179 | + █··················································█ |
| 180 | + █·██████████████████████████████████████████████████ |
| 181 | + █··················································█ |
| 182 | + ██████████████████████████████████████████████████·█ |
| 183 | + █x·················································█ |
| 184 | + ████████████████████████████████████████████████████ |
| 185 | + "#, |
| 186 | + ); |
| 187 | + let target = *poi.get(&'x').unwrap(); |
| 188 | + |
| 189 | + let (path, _cost) = get_snake_path(&grid, &snake, target, Cost::max()).unwrap(); |
| 190 | + |
| 191 | + assert_world_equal( |
| 192 | + &render_world(&grid, &get_full_path(&snake, &path), None), |
| 193 | + r#" |
| 194 | + ··╶───────────────────────────────────────────────┐· |
| 195 | + ██████████████████████████████████████████████████│█ |
| 196 | + █┌────────────────────────────────────────────────┘█ |
| 197 | + █│██████████████████████████████████████████████████ |
| 198 | + █└────────────────────────────────────────────────┐█ |
| 199 | + ██████████████████████████████████████████████████│█ |
| 200 | + █@────────────────────────────────────────────────┘█ |
| 201 | + ████████████████████████████████████████████████████ |
| 202 | + "#, |
| 203 | + ); |
| 204 | + } |
| 205 | + |
| 206 | + #[test] |
| 207 | + fn it_should_be_able_to_coil_the_snake() { |
| 208 | + let (grid, snake, _, poi) = read_world( |
| 209 | + r#" |
| 210 | + ····x····· |
| 211 | + ████·█···· |
| 212 | + █@──┐█···· |
| 213 | + █╷┌┐│█···· |
| 214 | + █└┘└┘█···· |
| 215 | + ██████···· |
| 216 | + ·········· |
| 217 | + "#, |
| 218 | + ); |
| 219 | + let target = *poi.get(&'x').unwrap(); |
| 220 | + |
| 221 | + let (path, _cost) = get_snake_path(&grid, &snake, target, Cost::max()).unwrap(); |
| 222 | + |
| 223 | + assert_world_equal( |
| 224 | + &render_world(&grid, &get_last_snake(&snake, &path), None), |
| 225 | + r#" |
| 226 | + ····@····· |
| 227 | + ████│█···· |
| 228 | + █╷··│█···· |
| 229 | + █│┌┐│█···· |
| 230 | + █└┘└┘█···· |
| 231 | + ██████···· |
| 232 | + ·········· |
| 233 | + "#, |
| 234 | + ); |
| 235 | + } |
| 236 | + |
| 237 | + #[test] |
| 238 | + fn it_should_avoid_self_colliding_the_snake() { |
| 239 | + let (grid, snake, _, poi) = read_world( |
| 240 | + r#" |
| 241 | + ·┌──────────┐··· |
| 242 | + ·│··┌┐······│··· |
| 243 | + ·│┌─┘└─@····│··· |
| 244 | + ·│└─────────┘··· |
| 245 | + ·╵·······x······ |
| 246 | + "#, |
| 247 | + ); |
| 248 | + let target = *poi.get(&'x').unwrap(); |
| 249 | + |
| 250 | + let (path, _cost) = get_snake_path(&grid, &snake, target, Cost::max()).unwrap(); |
| 251 | + |
| 252 | + assert_world_equal( |
| 253 | + &render_world(&grid, &get_last_snake(&snake, &path), None), |
| 254 | + r#" |
| 255 | + ······┌─────┐··· |
| 256 | + ····┌┐└────┐│··· |
| 257 | + ··┌─┘└─────┘│··· |
| 258 | + ··└───╴·····│··· |
| 259 | + ·········@──┘··· |
| 260 | + "#, |
| 261 | + ); |
| 262 | + } |
| 263 | + |
| 264 | + #[test] |
| 265 | + fn it_should_coil_to_exit() { |
| 266 | + let (grid, snake, _, poi) = read_world( |
| 267 | + r#" |
| 268 | + ······x········· |
| 269 | + ····█████······· |
| 270 | + ····█···█······· |
| 271 | + ····█···█······· |
| 272 | + ····██·██······· |
| 273 | + ·····█·█········ |
| 274 | + ·····█·@───╴···· |
| 275 | + ·····███········ |
| 276 | + "#, |
| 277 | + ); |
| 278 | + let target = *poi.get(&'x').unwrap(); |
| 279 | + |
| 280 | + let (path, _cost) = get_snake_path(&grid, &snake, target, Cost::max()).unwrap(); |
| 281 | + |
| 282 | + assert_world_equal( |
| 283 | + &render_world(&grid, &get_full_path(&snake, &path), None), |
| 284 | + r#" |
| 285 | + ······@──┐······ |
| 286 | + ····█████│······ |
| 287 | + ····█┌─┐█│······ |
| 288 | + ····█└┌┘█│······ |
| 289 | + ····██│██│······ |
| 290 | + ·····█│█┌┘······ |
| 291 | + ·····█└─┘──╴···· |
| 292 | + ·····███········ |
| 293 | + "#, |
| 294 | + ); |
| 295 | + } |
| 296 | +} |
0 commit comments