oxygengine_overworld/resources/
board.rs

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_count: usize,
253    // {chunk location: island index}
254    islands_map: HashMap<ChunkLocation, usize>,
255    // [from portal location?, to portal location?, island index, linear cost estimate]
256    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    // TODO: dear heavenly beings, reduce these micro-allocations!
309    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_count = islands.len();
378        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_count,
423            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    // TODO: dear heavenly beings, reduce these micro-allocations!
434    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    /// (cols, rows)
557    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        // NOTE: order matters! do not change it.
861        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        // NOTE: order matters! do not change it.
895        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    // TODO: dear heavenly beings, reduce these micro-allocations!
920    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    // TODO: dear heavenly beings, reduce these micro-allocations!
1092    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}