1use oxygengine_core::{id::ID, Scalar};
2use oxygengine_navigation::resources::{
3 Error as NavError, NavGrid, NavGridConnection, NavIslandPortal, NavIslands,
4 NavIslandsConnection,
5};
6use serde::{Deserialize, Serialize};
7use std::{
8 cmp::Ordering,
9 collections::{HashMap, HashSet},
10 ops::Range,
11};
12
13pub type BoardToken = ID<Location>;
14
15#[derive(Debug, Clone)]
16pub enum BoardError {
17 ChunkAlreadyExists(BoardLocation),
18 ChunkDoesNotExists(BoardLocation),
19 ChunkLocationOutOfBounds(ChunkLocation),
20 LocationOccupied(ChunkLocation),
21 LocationUnoccupied(ChunkLocation),
22 LocationUnavailable(ChunkLocation),
23 TokenDoesNotExists(BoardToken),
24 CannotTraverse(usize, usize),
25 ThereisNoBuiltNavigation,
26 ChunkIsTooSmallForNavigation(usize, usize),
27 ThereAreNoChunksForNavigation,
28 ThereAreNoConnectionsForNavigation,
29 ChunkNavigation(NavError),
30 ChunkLocationsAreOnSeparateIslands(ChunkLocation, ChunkLocation),
31 ChunkPathNotFound(ChunkLocation, ChunkLocation),
32 PathNotFound(Location, Location),
33 IslandNotFoundInChunk(ChunkLocation),
34}
35
36#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub enum BoardDirection {
38 North,
39 NorthEast,
40 East,
41 SouthEast,
42 South,
43 SouthWest,
44 West,
45 NorthWest,
46}
47
48impl BoardDirection {
49 pub fn from_coord(mut x: isize, mut y: isize) -> Option<Self> {
50 x = x.max(-1).min(1);
51 y = y.max(-1).min(1);
52 match (x, y) {
53 (0, -1) => Some(Self::North),
54 (1, -1) => Some(Self::NorthEast),
55 (1, 0) => Some(Self::East),
56 (1, 1) => Some(Self::SouthEast),
57 (0, 1) => Some(Self::South),
58 (-1, 1) => Some(Self::SouthWest),
59 (-1, 0) => Some(Self::West),
60 (-1, -1) => Some(Self::West),
61 _ => None,
62 }
63 }
64
65 pub fn into_coords(self) -> (isize, isize) {
66 match self {
67 Self::North => (0, -1),
68 Self::NorthEast => (1, -1),
69 Self::East => (1, 0),
70 Self::SouthEast => (1, 1),
71 Self::South => (0, 1),
72 Self::SouthWest => (-1, 1),
73 Self::West => (-1, 0),
74 Self::NorthWest => (-1, -1),
75 }
76 }
77}
78
79#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct BoardCollision {
81 #[serde(default)]
82 pub token: Option<BoardToken>,
83 #[serde(default)]
84 pub traverse_block: Option<usize>,
85 #[serde(default)]
86 pub tile_block: bool,
87}
88
89impl BoardCollision {
90 pub fn is_any(&self) -> bool {
91 self.token.is_some() || self.traverse_block.is_some() || self.tile_block
92 }
93}
94
95#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
96pub struct BoardLocation {
97 pub col: isize,
98 pub row: isize,
99}
100
101impl BoardLocation {
102 pub fn as_tuple(&self) -> (isize, isize) {
103 (self.col, self.row)
104 }
105
106 pub fn as_array(&self) -> [isize; 2] {
107 [self.col, self.row]
108 }
109}
110
111impl From<(isize, isize)> for BoardLocation {
112 fn from((col, row): (isize, isize)) -> Self {
113 Self { col, row }
114 }
115}
116
117impl From<[isize; 2]> for BoardLocation {
118 fn from([col, row]: [isize; 2]) -> Self {
119 Self { col, row }
120 }
121}
122
123#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
124pub struct ChunkLocation {
125 pub col: usize,
126 pub row: usize,
127}
128
129impl ChunkLocation {
130 pub fn as_tuple(&self) -> (usize, usize) {
131 (self.col, self.row)
132 }
133
134 pub fn as_array(&self) -> [usize; 2] {
135 [self.col, self.row]
136 }
137}
138
139impl From<(usize, usize)> for ChunkLocation {
140 fn from((col, row): (usize, usize)) -> Self {
141 Self { col, row }
142 }
143}
144
145impl From<[usize; 2]> for ChunkLocation {
146 fn from([col, row]: [usize; 2]) -> Self {
147 Self { col, row }
148 }
149}
150
151#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
152pub struct Location {
153 pub world: BoardLocation,
154 pub chunk: ChunkLocation,
155}
156
157impl From<(BoardLocation, ChunkLocation)> for Location {
158 fn from((world, chunk): (BoardLocation, ChunkLocation)) -> Self {
159 Self { world, chunk }
160 }
161}
162
163#[derive(Debug, Default, Clone)]
164pub struct BoardTraverseRules {
165 map: HashMap<usize, HashSet<usize>>,
166}
167
168impl BoardTraverseRules {
169 pub fn add(&mut self, from: usize, to: usize) {
170 if from == to {
171 return;
172 }
173 self.map.entry(from).or_default().insert(to);
174 }
175
176 pub fn with(mut self, from: usize, to: usize) -> Self {
177 self.add(from, to);
178 self
179 }
180
181 pub fn add_both_ways(&mut self, from: usize, to: usize) {
182 self.add(from, to);
183 self.add(to, from);
184 }
185
186 pub fn with_both_ways(mut self, from: usize, to: usize) -> Self {
187 self.add_both_ways(from, to);
188 self
189 }
190
191 pub fn add_product(&mut self, values: &[usize]) {
192 for a in values {
193 for b in values {
194 self.add(*a, *b);
195 }
196 }
197 }
198
199 pub fn with_product(mut self, values: &[usize]) -> Self {
200 self.add_product(values);
201 self
202 }
203
204 pub fn remove(&mut self, from: usize, to: Option<usize>) {
205 if let Some(to) = to {
206 if let Some(values) = self.map.get_mut(&from) {
207 values.remove(&to);
208 if values.is_empty() {
209 self.map.remove(&from);
210 }
211 }
212 } else if self.map.remove(&from).is_some() {
213 for values in self.map.values_mut() {
214 values.remove(&from);
215 }
216 }
217 }
218
219 pub fn can_traverse(&self, from: usize, to: usize) -> bool {
220 self.map.is_empty()
221 || from == to
222 || self
223 .map
224 .get(&from)
225 .map(|values| values.contains(&to))
226 .unwrap_or_default()
227 }
228
229 pub fn can_sequence_traverse(&self, iter: impl Iterator<Item = usize>) -> bool {
230 let mut last = None;
231 for next in iter {
232 let value = last.unwrap_or(next);
233 if !self.can_traverse(value, next) {
234 return false;
235 }
236 last = Some(next);
237 }
238 true
239 }
240}
241
242#[derive(Debug, Copy, Clone, PartialEq, Eq)]
243pub enum BoardIgnoreOccupancy<'a> {
244 Never,
245 ForTokens(&'a [BoardToken]),
246 Always,
247}
248
249#[derive(Debug, Clone)]
250struct ChunkNavigation {
251 grid: NavGrid,
252 islands_map: HashMap<ChunkLocation, usize>,
255 islands_portals_costs: Vec<(Option<ChunkLocation>, Option<ChunkLocation>, usize, Scalar)>,
257}
258
259#[derive(Debug, Clone)]
260pub struct BoardChunk {
261 cols: usize,
262 rows: usize,
263 tile_values: Vec<Option<usize>>,
264 occupancy: Vec<Option<BoardToken>>,
265 tokens: HashMap<BoardToken, ChunkLocation>,
266 navigation: Option<ChunkNavigation>,
267}
268
269impl BoardChunk {
270 pub fn size(&self) -> (usize, usize) {
271 (self.cols, self.rows)
272 }
273
274 pub fn read_values(&self) -> &[Option<usize>] {
275 &self.tile_values
276 }
277
278 pub fn write_values(&mut self) -> &mut [Option<usize>] {
279 &mut self.tile_values
280 }
281
282 pub fn tile_value(&self, location: ChunkLocation) -> Result<Option<usize>, BoardError> {
283 if location.col >= self.cols || location.row >= self.rows {
284 return Err(BoardError::ChunkLocationOutOfBounds(location));
285 }
286 let index = location.row * self.cols + location.col;
287 Ok(self.tile_values[index])
288 }
289
290 pub fn occupancy(&self, location: ChunkLocation) -> Result<Option<BoardToken>, BoardError> {
291 if location.col >= self.cols || location.row >= self.rows {
292 return Err(BoardError::ChunkLocationOutOfBounds(location));
293 }
294 let index = location.row * self.cols + location.col;
295 Ok(self.occupancy[index])
296 }
297
298 pub fn token_location(&self, token: BoardToken) -> Option<ChunkLocation> {
299 self.tokens.get(&token).cloned()
300 }
301
302 pub fn tokens(&self) -> impl Iterator<Item = (BoardToken, ChunkLocation)> + '_ {
303 self.tokens
304 .iter()
305 .map(|(token, location)| (*token, *location))
306 }
307
308 pub fn rebuild_navigation(
310 &mut self,
311 traverse_rules: &BoardTraverseRules,
312 ) -> Result<(), BoardError> {
313 if self.cols <= 1 && self.rows <= 1 {
314 return Err(BoardError::ChunkIsTooSmallForNavigation(
315 self.cols, self.rows,
316 ));
317 }
318 let mut connections =
319 Vec::with_capacity((self.cols - 1) * self.rows + (self.rows - 1) * self.cols);
320 for col in 0..(self.cols - 1) {
321 for row in 0..self.rows {
322 let ca = (col, row).into();
323 let cb = (col + 1, row).into();
324 let va = match self.tile_value(ca)? {
325 Some(v) => v,
326 None => continue,
327 };
328 let vb = match self.tile_value(cb)? {
329 Some(v) => v,
330 None => continue,
331 };
332 if traverse_rules.can_traverse(va, vb) {
333 connections.push(NavGridConnection {
334 from: (ca.col, ca.row),
335 to: (cb.col, cb.row),
336 });
337 }
338 if traverse_rules.can_traverse(vb, va) {
339 connections.push(NavGridConnection {
340 from: (cb.col, cb.row),
341 to: (ca.col, ca.row),
342 });
343 }
344 }
345 }
346 for col in 0..self.cols {
347 for row in 0..(self.rows - 1) {
348 let ca = (col, row).into();
349 let cb = (col, row + 1).into();
350 let va = match self.tile_value(ca)? {
351 Some(v) => v,
352 None => continue,
353 };
354 let vb = match self.tile_value(cb)? {
355 Some(v) => v,
356 None => continue,
357 };
358 if traverse_rules.can_traverse(va, vb) {
359 connections.push(NavGridConnection {
360 from: (ca.col, ca.row),
361 to: (cb.col, cb.row),
362 });
363 }
364 if traverse_rules.can_traverse(vb, va) {
365 connections.push(NavGridConnection {
366 from: (cb.col, cb.row),
367 to: (ca.col, ca.row),
368 });
369 }
370 }
371 }
372 let grid = match NavGrid::with_connections(self.cols, self.rows, connections) {
373 Ok(grid) => grid,
374 Err(error) => return Err(BoardError::ChunkNavigation(error)),
375 };
376 let islands = grid.find_islands();
377 let islands_map = islands
379 .iter()
380 .enumerate()
381 .flat_map(|(island, coords)| {
382 coords
383 .iter()
384 .map(move |coord| (ChunkLocation::from(*coord), island))
385 })
386 .collect::<HashMap<_, _>>();
387 let islands_portals_costs = islands
388 .into_iter()
389 .enumerate()
390 .map(|(index, island)| {
391 let island = island
392 .into_iter()
393 .filter_map(|coord| {
394 let index = coord.1 * self.cols + coord.0;
395 if (coord.0 == 0
396 || coord.1 == 0
397 || coord.0 == self.cols - 1
398 || coord.1 == self.rows - 1)
399 && self.tile_values.get(index).unwrap().is_some()
400 {
401 Some(ChunkLocation::from((coord.0, coord.1)))
402 } else {
403 None
404 }
405 })
406 .collect::<Vec<_>>();
407 (index, island)
408 })
409 .filter(|(_, island)| !island.is_empty())
410 .flat_map(|(index, island)| {
411 let cost = island.len() as Scalar;
412 let mut result = Vec::with_capacity(island.len() * 2);
413 for c in island {
414 result.push((Some(c), None, index, cost));
415 result.push((None, Some(c), index, cost));
416 }
417 result
418 })
419 .collect::<Vec<_>>();
420 self.navigation = Some(ChunkNavigation {
421 grid,
422 islands_map,
424 islands_portals_costs,
425 });
426 Ok(())
427 }
428
429 pub fn clear_navigation(&mut self) {
430 self.navigation = None;
431 }
432
433 pub fn find_path(
435 &self,
436 from: ChunkLocation,
437 to: ChunkLocation,
438 ignore_occupancy: BoardIgnoreOccupancy,
439 ) -> Result<Vec<ChunkLocation>, BoardError> {
440 let navigation = self
441 .navigation
442 .as_ref()
443 .ok_or(BoardError::ThereisNoBuiltNavigation)?;
444 if navigation.islands_map.get(&from) != navigation.islands_map.get(&to) {
445 return Err(BoardError::ChunkLocationsAreOnSeparateIslands(from, to));
446 }
447 let path = navigation
448 .grid
449 .find_path((from.col, from.row), (to.col, to.row))
450 .ok_or(BoardError::ChunkPathNotFound(from, to))?
451 .into_iter()
452 .map(|coord| {
453 let location = ChunkLocation::from(coord);
454 let success = match ignore_occupancy {
455 BoardIgnoreOccupancy::Never => self.occupancy(location)?.is_none(),
456 BoardIgnoreOccupancy::ForTokens(tokens) => self
457 .occupancy(location)?
458 .map(|t| tokens.contains(&t))
459 .unwrap_or(true),
460 BoardIgnoreOccupancy::Always => true,
461 };
462 if success {
463 Ok(location)
464 } else {
465 Err(BoardError::LocationOccupied(location))
466 }
467 })
468 .collect::<Result<Vec<_>, _>>()?;
469 if path.is_empty() {
470 Err(BoardError::ChunkPathNotFound(from, to))
471 } else {
472 Ok(path)
473 }
474 }
475
476 fn acquire_token(&mut self, location: ChunkLocation) -> Result<BoardToken, BoardError> {
477 if location.col >= self.cols || location.row >= self.rows {
478 return Err(BoardError::ChunkLocationOutOfBounds(location));
479 }
480 let index = location.row * self.cols + location.col;
481 if self.tile_values[index].is_none() {
482 return Err(BoardError::LocationUnavailable(location));
483 }
484 if self.occupancy[index].is_some() {
485 return Err(BoardError::LocationOccupied(location));
486 }
487 let token = BoardToken::new();
488 self.occupancy[index] = Some(token);
489 self.tokens.insert(token, location);
490 Ok(token)
491 }
492
493 fn release_token(&mut self, token: BoardToken) -> Option<ChunkLocation> {
494 if let Some(location) = self.tokens.remove(&token) {
495 let index = location.row * self.cols + location.col;
496 self.occupancy[index] = None;
497 return Some(location);
498 }
499 None
500 }
501
502 fn occupy_location(
503 &mut self,
504 location: ChunkLocation,
505 token: BoardToken,
506 ) -> Result<(), BoardError> {
507 if location.col >= self.cols || location.row >= self.rows {
508 return Err(BoardError::ChunkLocationOutOfBounds(location));
509 }
510 let index = location.row * self.cols + location.col;
511 if self.tile_values[index].is_none() {
512 return Err(BoardError::LocationUnavailable(location));
513 }
514 if self.occupancy[index].is_some() {
515 return Err(BoardError::LocationOccupied(location));
516 }
517 self.occupancy[index] = Some(token);
518 self.tokens.insert(token, location);
519 Ok(())
520 }
521
522 fn free_location(&mut self, location: ChunkLocation) -> Result<(), BoardError> {
523 if location.col >= self.cols || location.row >= self.rows {
524 return Err(BoardError::ChunkLocationOutOfBounds(location));
525 }
526 let index = location.row * self.cols + location.col;
527 if let Some(token) = self.occupancy[index] {
528 self.occupancy[index] = None;
529 self.tokens.remove(&token);
530 return Ok(());
531 }
532 Err(BoardError::LocationUnoccupied(location))
533 }
534}
535
536#[derive(Debug, Clone)]
537pub struct Board {
538 chunk_cols: usize,
539 chunk_rows: usize,
540 pub traverse_rules: BoardTraverseRules,
541 chunks: HashMap<BoardLocation, BoardChunk>,
542 navigation: Option<NavIslands<(BoardLocation, usize), ChunkLocation>>,
543}
544
545impl Board {
546 pub fn new(chunk_cols: usize, chunk_rows: usize, traverse_rules: BoardTraverseRules) -> Self {
547 Self {
548 chunk_cols: chunk_cols.max(1),
549 chunk_rows: chunk_rows.max(1),
550 traverse_rules,
551 chunks: Default::default(),
552 navigation: None,
553 }
554 }
555
556 pub fn chunk_size(&self) -> (usize, usize) {
558 (self.chunk_cols, self.chunk_rows)
559 }
560
561 pub fn location_move(&self, mut location: Location, x: isize, y: isize) -> Location {
562 match x.cmp(&0) {
563 Ordering::Greater => {
564 location.chunk.col += x as usize;
565 if location.chunk.col >= self.chunk_cols {
566 let times = location.chunk.col / self.chunk_cols;
567 location.world.col += times as isize;
568 location.chunk.col %= self.chunk_cols;
569 }
570 }
571 Ordering::Less => {
572 let v = location.chunk.col as isize + x;
573 if v < 0 {
574 let v = -v;
575 let times = 1 + v / self.chunk_cols as isize;
576 location.world.col -= times;
577 location.chunk.col = self.chunk_cols - (v as usize % self.chunk_cols);
578 } else {
579 location.chunk.col = v as usize;
580 }
581 }
582 _ => {}
583 }
584 match y.cmp(&0) {
585 Ordering::Greater => {
586 location.chunk.row += y as usize;
587 if location.chunk.row >= self.chunk_rows {
588 let times = location.chunk.row / self.chunk_rows;
589 location.world.row += times as isize;
590 location.chunk.row %= self.chunk_rows;
591 }
592 }
593 Ordering::Less => {
594 let v = location.chunk.row as isize + y;
595 if v < 0 {
596 let v = -v;
597 let times = 1 + v / self.chunk_rows as isize;
598 location.world.row -= times;
599 location.chunk.row = self.chunk_rows - (v as usize % self.chunk_rows);
600 } else {
601 location.chunk.row = v as usize;
602 }
603 }
604 _ => {}
605 }
606 location
607 }
608
609 pub fn locations_around(
610 &self,
611 location: Location,
612 range: usize,
613 ) -> impl Iterator<Item = Location> + '_ {
614 let range = range as isize;
615 (-range..=range)
616 .flat_map(move |y| (-range..=range).map(move |x| (x, y)))
617 .filter(|(x, y)| *x != 0 || *y != 0)
618 .map(move |(x, y)| self.location_move(location, x, y))
619 }
620
621 pub fn locations_around_cardinal(
622 &self,
623 location: Location,
624 range: usize,
625 ) -> impl Iterator<Item = Location> + '_ {
626 let range = range as isize;
627 (-range..=range)
628 .filter(|index| *index != 0)
629 .map(move |index| self.location_move(location, index, 0))
630 .chain(
631 (-range..=range)
632 .filter(|index| *index != 0)
633 .map(move |index| self.location_move(location, 0, index)),
634 )
635 }
636
637 pub fn location_relative(&self, from: Location, to: Location) -> (isize, isize) {
638 let wx = (to.world.col - from.world.col) * self.chunk_cols as isize;
639 let wy = (to.world.row - from.world.row) * self.chunk_rows as isize;
640 let cx = to.chunk.col as isize - from.chunk.col as isize;
641 let cy = to.chunk.row as isize - from.chunk.row as isize;
642 (wx + cx, wy + cy)
643 }
644
645 pub fn locations_in_range(&self, a: Location, b: Location, range: usize) -> bool {
646 let (dx, dy) = self.location_relative(a, b);
647 dx.unsigned_abs() <= range && dy.unsigned_abs() <= range
648 }
649
650 pub fn create_chunk(&mut self, location: BoardLocation) -> Result<(), BoardError> {
651 if self.chunks.contains_key(&location) {
652 return Err(BoardError::ChunkAlreadyExists(location));
653 }
654 let size = self.chunk_cols * self.chunk_rows;
655 self.chunks.insert(
656 location,
657 BoardChunk {
658 cols: self.chunk_cols,
659 rows: self.chunk_rows,
660 tile_values: vec![None; size],
661 tokens: Default::default(),
662 occupancy: vec![None; size],
663 navigation: None,
664 },
665 );
666 Ok(())
667 }
668
669 pub fn ensure_chunk(&mut self, location: BoardLocation) -> Result<(), BoardError> {
670 if !self.chunks.contains_key(&location) {
671 return self.create_chunk(location);
672 }
673 Ok(())
674 }
675
676 pub fn destroy_chunk(&mut self, location: BoardLocation) -> Result<(), BoardError> {
677 if self.chunks.remove(&location).is_some() {
678 Ok(())
679 } else {
680 Err(BoardError::ChunkDoesNotExists(location))
681 }
682 }
683
684 pub fn has_chunk(&self, location: BoardLocation) -> bool {
685 self.chunks.contains_key(&location)
686 }
687
688 pub fn chunk(&self, location: BoardLocation) -> Option<&BoardChunk> {
689 self.chunks.get(&location)
690 }
691
692 pub fn chunk_mut(&mut self, location: BoardLocation) -> Option<&mut BoardChunk> {
693 self.chunks.get_mut(&location)
694 }
695
696 pub fn tile_value(&self, location: Location) -> Result<Option<usize>, BoardError> {
697 match self.chunk(location.world) {
698 Some(chunk) => chunk.tile_value(location.chunk),
699 None => Err(BoardError::ChunkDoesNotExists(location.world)),
700 }
701 }
702
703 pub fn tile_values(
704 &self,
705 range: Range<Location>,
706 ) -> impl Iterator<Item = (Location, usize)> + '_ {
707 let (dx, dy) = self.location_relative(range.start, range.end);
708 let count = if dx > 0 && dy > 0 {
709 dx as usize * dy as usize
710 } else {
711 0
712 };
713 (0..count).filter_map(move |index| {
714 let cols = dx as usize;
715 let x = index % cols;
716 let y = index / cols;
717 let location = self.location_move(range.start, x as isize, y as isize);
718 self.tile_value(location)
719 .ok()
720 .and_then(|v| v)
721 .map(|value| (location, value))
722 })
723 }
724
725 pub fn occupancy(&self, location: Location) -> Result<Option<BoardToken>, BoardError> {
726 match self.chunk(location.world) {
727 Some(chunk) => chunk.occupancy(location.chunk),
728 None => Err(BoardError::ChunkDoesNotExists(location.world)),
729 }
730 }
731
732 pub fn token_location(&self, token: BoardToken) -> Option<Location> {
733 self.chunks
734 .iter()
735 .find_map(|(wloc, chunk)| Some((*wloc, chunk.token_location(token)?).into()))
736 }
737
738 pub fn tokens(&self) -> impl Iterator<Item = (BoardToken, Location)> + '_ {
739 self.chunks.iter().flat_map(|(wloc, chunk)| {
740 chunk
741 .tokens()
742 .map(|(token, cloc)| (token, (*wloc, cloc).into()))
743 })
744 }
745
746 pub fn tokens_in_range(
747 &self,
748 location: Location,
749 range: usize,
750 ) -> impl Iterator<Item = (BoardToken, Location)> + '_ {
751 self.tokens()
752 .filter(move |(_, loc)| self.locations_in_range(location, *loc, range))
753 }
754
755 pub fn tile_collision(
756 &self,
757 location: Location,
758 direction: BoardDirection,
759 ) -> Result<(Location, BoardCollision), BoardError> {
760 let tile_value = match self.tile_value(location)? {
761 Some(value) => value,
762 None => return Err(BoardError::LocationUnavailable(location.chunk)),
763 };
764 let (x, y) = direction.into_coords();
765 let loc = self.location_move(location, x, y);
766 let value = self.tile_value(loc)?;
767 let token = self.occupancy(loc)?;
768 let tile_block = value.is_none();
769 let traverse_block = value.and_then(|value| {
770 if self.traverse_rules.can_traverse(tile_value, value) {
771 Some(value)
772 } else {
773 None
774 }
775 });
776 let collision = BoardCollision {
777 token,
778 traverse_block,
779 tile_block,
780 };
781 Ok((loc, collision))
782 }
783
784 pub fn tile_collisions(
785 &self,
786 location: Location,
787 include_diagonals: bool,
788 ) -> impl Iterator<Item = (BoardDirection, Location, BoardCollision)> + '_ {
789 let directions = if include_diagonals {
790 &[
791 BoardDirection::North,
792 BoardDirection::NorthEast,
793 BoardDirection::East,
794 BoardDirection::SouthEast,
795 BoardDirection::South,
796 BoardDirection::SouthWest,
797 BoardDirection::West,
798 BoardDirection::NorthWest,
799 ][..]
800 } else {
801 &[
802 BoardDirection::North,
803 BoardDirection::East,
804 BoardDirection::South,
805 BoardDirection::West,
806 ][..]
807 };
808 directions
809 .iter()
810 .filter_map(move |direction| {
811 Some((
812 *direction,
813 self.tile_collision(location, *direction)
814 .ok()
815 .filter(|(_, collision)| collision.is_any())?,
816 ))
817 })
818 .map(|(direction, (location, collision))| (direction, location, collision))
819 }
820
821 pub fn acquire_token(&mut self, location: Location) -> Result<BoardToken, BoardError> {
822 match self.chunks.get_mut(&location.world) {
823 Some(chunk) => chunk.acquire_token(location.chunk),
824 None => Err(BoardError::ChunkDoesNotExists(location.world)),
825 }
826 }
827
828 pub fn release_token(&mut self, token: BoardToken) -> Option<Location> {
829 for (wloc, chunk) in &mut self.chunks {
830 if let Some(cloc) = chunk.release_token(token) {
831 return Some((*wloc, cloc).into());
832 }
833 }
834 None
835 }
836
837 pub fn move_token(&mut self, token: BoardToken, x: isize, y: isize) -> Result<(), BoardError> {
838 if x == 0 && y == 0 {
839 return Ok(());
840 }
841 let from = match self.token_location(token) {
842 Some(from) => from,
843 None => return Err(BoardError::TokenDoesNotExists(token)),
844 };
845 let to = self.location_move(from, x, y);
846 if self.occupancy(to)?.is_some() {
847 return Err(BoardError::LocationOccupied(to.chunk));
848 }
849 let from_value = match self.tile_value(from)? {
850 Some(value) => value,
851 None => return Err(BoardError::LocationUnavailable(to.chunk)),
852 };
853 let to_value = match self.tile_value(to)? {
854 Some(value) => value,
855 None => return Err(BoardError::LocationUnavailable(to.chunk)),
856 };
857 if !self.traverse_rules.can_traverse(from_value, to_value) {
858 return Err(BoardError::CannotTraverse(from_value, to_value));
859 }
860 self.free_location(from)?;
862 self.occupy_location(to, token)?;
863 Ok(())
864 }
865
866 pub fn move_step_token(
867 &mut self,
868 token: BoardToken,
869 direction: BoardDirection,
870 ) -> Result<(), BoardError> {
871 let (x, y) = direction.into_coords();
872 self.move_token(token, x, y)
873 }
874
875 pub fn move_sequence_token(
876 &mut self,
877 token: BoardToken,
878 iter: impl Iterator<Item = BoardDirection>,
879 ) -> Result<(), BoardError> {
880 for direction in iter {
881 self.move_step_token(token, direction)?;
882 }
883 Ok(())
884 }
885
886 pub fn teleport_token(&mut self, token: BoardToken, to: Location) -> Result<(), BoardError> {
887 let from = match self.token_location(token) {
888 Some(from) => from,
889 None => return Err(BoardError::TokenDoesNotExists(token)),
890 };
891 if from == to {
892 return Ok(());
893 }
894 self.free_location(from)?;
896 self.occupy_location(to, token)?;
897 Ok(())
898 }
899
900 pub fn swap_tokens(&mut self, a: BoardToken, b: BoardToken) -> Result<(), BoardError> {
901 if a == b {
902 return Ok(());
903 }
904 let from = match self.token_location(a) {
905 Some(from) => from,
906 None => return Err(BoardError::TokenDoesNotExists(a)),
907 };
908 let to = match self.token_location(b) {
909 Some(to) => to,
910 None => return Err(BoardError::TokenDoesNotExists(b)),
911 };
912 self.free_location(from)?;
913 self.free_location(to)?;
914 self.occupy_location(from, b)?;
915 self.occupy_location(to, a)?;
916 Ok(())
917 }
918
919 pub fn rebuild_navigation(&mut self) -> Result<(), BoardError> {
921 macro_rules! impl_collect_connections {
922 (
923 $connections:expr,
924 $chunk:expr,
925 $from_navigation:expr,
926 $from_board_location:expr,
927 $to_board_location:expr,
928 $direction:expr,
929 ) => {
930 if let Some(neighbor) = self.chunks.get(&$to_board_location) {
931 let to_navigation = match neighbor.navigation.as_ref() {
932 Some(navigation) => navigation,
933 None => continue,
934 };
935 let count = match $direction {
936 BoardDirection::North | BoardDirection::South => self.chunk_cols,
937 BoardDirection::East | BoardDirection::West => self.chunk_rows,
938 _ => unreachable!(),
939 };
940 $connections.reserve(count);
941 for index in 0..count {
942 let from_chunk_location = match $direction {
943 BoardDirection::North => ChunkLocation { col: index, row: 0 },
944 BoardDirection::South => ChunkLocation {
945 col: index,
946 row: self.chunk_rows - 1,
947 },
948 BoardDirection::West => ChunkLocation { col: 0, row: index },
949 BoardDirection::East => ChunkLocation {
950 col: self.chunk_cols - 1,
951 row: index,
952 },
953 _ => unreachable!(),
954 };
955 let from_value =
956 match $chunk.tile_value(from_chunk_location).ok().and_then(|v| v) {
957 Some(value) => value,
958 None => continue,
959 };
960 let from_island =
961 match $from_navigation.islands_map.get(&from_chunk_location) {
962 Some(value) => *value,
963 None => continue,
964 };
965 let to_chunk_location = match $direction {
966 BoardDirection::North => ChunkLocation {
967 col: index,
968 row: self.chunk_rows - 1,
969 },
970 BoardDirection::South => ChunkLocation { col: index, row: 0 },
971 BoardDirection::West => ChunkLocation {
972 col: self.chunk_rows - 1,
973 row: index,
974 },
975 BoardDirection::East => ChunkLocation { col: 0, row: index },
976 _ => unreachable!(),
977 };
978 let to_value =
979 match neighbor.tile_value(to_chunk_location).ok().and_then(|v| v) {
980 Some(value) => value,
981 None => continue,
982 };
983 let to_island = match to_navigation.islands_map.get(&to_chunk_location) {
984 Some(value) => *value,
985 None => continue,
986 };
987 if self.traverse_rules.can_traverse(from_value, to_value) {
988 $connections.push(NavIslandsConnection {
989 from: NavIslandPortal {
990 island: (*$from_board_location, from_island),
991 portal: Some(from_chunk_location),
992 },
993 to: NavIslandPortal {
994 island: ($to_board_location, to_island),
995 portal: Some(to_chunk_location),
996 },
997 distance: 0.0,
998 });
999 }
1000 }
1001 }
1002 };
1003 }
1004
1005 if self.chunks.is_empty() {
1006 return Err(BoardError::ThereAreNoChunksForNavigation);
1007 }
1008 let mut connections = vec![];
1009 for (from_board_location, chunk) in &self.chunks {
1010 let from_navigation = match chunk.navigation.as_ref() {
1011 Some(navigation) => navigation,
1012 None => continue,
1013 };
1014 let to_board_location = BoardLocation {
1015 col: from_board_location.col,
1016 row: from_board_location.row - 1,
1017 };
1018 impl_collect_connections!(
1019 connections,
1020 chunk,
1021 from_navigation,
1022 from_board_location,
1023 to_board_location,
1024 BoardDirection::North,
1025 );
1026 let to_board_location = BoardLocation {
1027 col: from_board_location.col,
1028 row: from_board_location.row + 1,
1029 };
1030 impl_collect_connections!(
1031 connections,
1032 chunk,
1033 from_navigation,
1034 from_board_location,
1035 to_board_location,
1036 BoardDirection::South,
1037 );
1038 let to_board_location = BoardLocation {
1039 col: from_board_location.col - 1,
1040 row: from_board_location.row,
1041 };
1042 impl_collect_connections!(
1043 connections,
1044 chunk,
1045 from_navigation,
1046 from_board_location,
1047 to_board_location,
1048 BoardDirection::West,
1049 );
1050 let to_board_location = BoardLocation {
1051 col: from_board_location.col + 1,
1052 row: from_board_location.row,
1053 };
1054 impl_collect_connections!(
1055 connections,
1056 chunk,
1057 from_navigation,
1058 from_board_location,
1059 to_board_location,
1060 BoardDirection::East,
1061 );
1062 if connections.is_empty() {
1063 continue;
1064 }
1065 connections.reserve(from_navigation.islands_portals_costs.len());
1066 for (from, to, island, cost) in &from_navigation.islands_portals_costs {
1067 connections.push(NavIslandsConnection {
1068 from: NavIslandPortal {
1069 island: (*from_board_location, *island),
1070 portal: *from,
1071 },
1072 to: NavIslandPortal {
1073 island: (*from_board_location, *island),
1074 portal: *to,
1075 },
1076 distance: *cost,
1077 });
1078 }
1079 }
1080 if connections.is_empty() {
1081 return Err(BoardError::ThereAreNoConnectionsForNavigation);
1082 }
1083 self.navigation = Some(NavIslands::new(connections, false));
1084 Ok(())
1085 }
1086
1087 pub fn clear_navigation(&mut self) {
1088 self.navigation = None;
1089 }
1090
1091 pub fn find_path(
1093 &self,
1094 from: Location,
1095 to: Location,
1096 ignore_occupancy: BoardIgnoreOccupancy,
1097 ) -> Result<Vec<Location>, BoardError> {
1098 let from_chunk = self
1099 .chunks
1100 .get(&from.world)
1101 .ok_or(BoardError::ChunkDoesNotExists(from.world))?;
1102 let from_navigation = from_chunk
1103 .navigation
1104 .as_ref()
1105 .ok_or(BoardError::ThereisNoBuiltNavigation)?;
1106 let from_island = *from_navigation
1107 .islands_map
1108 .get(&from.chunk)
1109 .ok_or(BoardError::IslandNotFoundInChunk(from.chunk))?;
1110 let to_chunk = self
1111 .chunks
1112 .get(&to.world)
1113 .ok_or(BoardError::ChunkDoesNotExists(to.world))?;
1114 let to_navigation = to_chunk
1115 .navigation
1116 .as_ref()
1117 .ok_or(BoardError::ThereisNoBuiltNavigation)?;
1118 let to_island = *to_navigation
1119 .islands_map
1120 .get(&to.chunk)
1121 .ok_or(BoardError::IslandNotFoundInChunk(to.chunk))?;
1122 if from.world == to.world && from_island == to_island {
1123 return Ok(from_chunk
1124 .find_path(from.chunk, to.chunk, ignore_occupancy)?
1125 .into_iter()
1126 .map(|c| Location {
1127 world: from.world,
1128 chunk: c,
1129 })
1130 .collect());
1131 }
1132 let navigation = self
1133 .navigation
1134 .as_ref()
1135 .ok_or(BoardError::ThereisNoBuiltNavigation)?;
1136 let path = navigation
1137 .find_path(
1138 &NavIslandPortal {
1139 island: (from.world, from_island),
1140 portal: None,
1141 },
1142 &NavIslandPortal {
1143 island: (to.world, to_island),
1144 portal: None,
1145 },
1146 )
1147 .ok_or(BoardError::PathNotFound(from, to))?
1148 .1;
1149 let mut result = vec![];
1150 for pair in path.chunks_exact(2) {
1151 if pair[0].island != pair[1].island {
1152 continue;
1153 }
1154 let board_location = pair[0].island.0;
1155 let chunk = self
1156 .chunks
1157 .get(&board_location)
1158 .ok_or(BoardError::ChunkDoesNotExists(board_location))?;
1159 match (&pair[0].portal, &pair[1].portal) {
1160 (None, Some(portal)) => result.extend(
1161 chunk
1162 .find_path(from.chunk, *portal, ignore_occupancy)?
1163 .into_iter()
1164 .map(|c| Location {
1165 world: board_location,
1166 chunk: c,
1167 }),
1168 ),
1169 (Some(portal), None) => result.extend(
1170 chunk
1171 .find_path(*portal, to.chunk, ignore_occupancy)?
1172 .into_iter()
1173 .map(|c| Location {
1174 world: board_location,
1175 chunk: c,
1176 }),
1177 ),
1178 (Some(from_portal), Some(to_portal)) => result.extend(
1179 chunk
1180 .find_path(*from_portal, *to_portal, ignore_occupancy)?
1181 .into_iter()
1182 .map(|c| Location {
1183 world: board_location,
1184 chunk: c,
1185 }),
1186 ),
1187 (None, None) => continue,
1188 }
1189 }
1190 if result.len() <= 1 {
1191 Err(BoardError::PathNotFound(from, to))
1192 } else {
1193 Ok(result)
1194 }
1195 }
1196
1197 fn occupy_location(&mut self, location: Location, token: BoardToken) -> Result<(), BoardError> {
1198 match self.chunks.get_mut(&location.world) {
1199 Some(chunk) => chunk.occupy_location(location.chunk, token),
1200 None => Err(BoardError::ChunkDoesNotExists(location.world)),
1201 }
1202 }
1203
1204 fn free_location(&mut self, location: Location) -> Result<(), BoardError> {
1205 match self.chunks.get_mut(&location.world) {
1206 Some(chunk) => chunk.free_location(location.chunk),
1207 None => Err(BoardError::ChunkDoesNotExists(location.world)),
1208 }
1209 }
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214 use super::*;
1215
1216 #[test]
1217 fn test_navigation() {
1218 let traverse_rules = BoardTraverseRules::default();
1219 let mut board = Board::new(3, 3, traverse_rules.clone());
1220 let a = (0, 0).into();
1221 let b = (0, 1).into();
1222 board.create_chunk(a).unwrap();
1223 {
1224 let chunk = board.chunk_mut(a).unwrap();
1225 chunk.write_values().copy_from_slice(&[
1226 Some(1),
1227 Some(1),
1228 Some(1),
1229 Some(1),
1230 None,
1231 Some(1),
1232 Some(1),
1233 None,
1234 Some(1),
1235 ]);
1236 chunk.rebuild_navigation(&traverse_rules).unwrap();
1237 }
1238 board.create_chunk(b).unwrap();
1239 {
1240 let chunk = board.chunk_mut(b).unwrap();
1241 chunk.write_values().copy_from_slice(&[
1242 Some(1),
1243 None,
1244 Some(1),
1245 Some(1),
1246 None,
1247 Some(1),
1248 Some(1),
1249 Some(1),
1250 Some(1),
1251 ]);
1252 chunk.rebuild_navigation(&traverse_rules).unwrap();
1253 }
1254 board.rebuild_navigation().unwrap();
1255 let a = Location::from(((0, 0).into(), (0, 0).into()));
1256 let b = Location::from(((0, 1).into(), (1, 2).into()));
1257 let path = board.find_path(a, b, BoardIgnoreOccupancy::Always).unwrap();
1258 assert_eq!(
1259 path,
1260 vec![
1261 a,
1262 Location::from(((0, 0).into(), (0, 1).into())),
1263 Location::from(((0, 0).into(), (0, 2).into())),
1264 Location::from(((0, 1).into(), (0, 0).into())),
1265 Location::from(((0, 1).into(), (0, 1).into())),
1266 Location::from(((0, 1).into(), (0, 2).into())),
1267 b,
1268 ]
1269 );
1270 let token = board
1271 .acquire_token(Location::from(((0, 0).into(), (0, 1).into())))
1272 .unwrap();
1273 let path = board
1274 .find_path(a, b, BoardIgnoreOccupancy::ForTokens(&[token]))
1275 .unwrap();
1276 assert_eq!(
1277 path,
1278 vec![
1279 a,
1280 Location::from(((0, 0).into(), (0, 1).into())),
1281 Location::from(((0, 0).into(), (0, 2).into())),
1282 Location::from(((0, 1).into(), (0, 0).into())),
1283 Location::from(((0, 1).into(), (0, 1).into())),
1284 Location::from(((0, 1).into(), (0, 2).into())),
1285 b,
1286 ]
1287 );
1288 let path = board.find_path(a, b, BoardIgnoreOccupancy::Never);
1289 assert!(matches!(
1290 path,
1291 Err(BoardError::LocationOccupied(ChunkLocation {
1292 col: 0,
1293 row: 1
1294 }))
1295 ));
1296 }
1297}